Js asynchronous loading. The script is small and dependent? We are writing a script to organize step-by-step asynchronous loading in jQuery

With the increase in Internet connection speed and the increase in power not only of desktop, but also mobile devices Web pages are becoming heavier. The number and size of connected files is growing: JavaScript files, css files, images, third-party site widgets, iframes. On this moment The specifics of how browsers work is such that when loading a js file, rendering is blocked until the script is executed. Modern browsers V background will parse the document and download scripts and styles, but rendering will be blocked. Comparison network parameters For different browsers can be viewed at browserscope.org. We cannot eliminate blocking completely, but we can optimize the server and client parts of the application so that render blocking takes the shortest amount of time.

Server side solutions:
- Reduce the size of transferred files
- Use CDN
- Place static files on separate domain or under a domain, thus increasing the number of simultaneous browser connections.
- Enable compression of transferred files (gzip)

Solutions for the client side:
- Reduce the number of requests.
- Cache files on the client side using Expires and Etags headers.
- Use publicly available CDNs (Google CDN, Yandex CDN). So there is a possibility that the file from the public CDN will already be stored in the browser cache.

One way to optimize site loading speed is to load files asynchronously, which does not block rendering.

JavaScript asynchronous loading script:

(function() ( var s = document.createElement("script"); s.type = "text/javascript"; s.async = true; s.src = " File URL"; document.getElementsByTagName("head").appendChild(script); ))();

If JavaScript needs to be executed after the entire page has loaded, including content, images, style files, and external scripts, then the onload event needs to be added to the loader.

If (window.addEventListener) ( window.addEventListener("load", async_load, false); ) else if (window.attachEvent) ( window.attachEvent("onload", async_load); )

JavaScript asynchronous loading script taking into account the onload event (function() ( function async_load())( var s = document.createElement("script"); s.type = "text/javascript"; s.async = true; s.src = "File URL"; document.getElementsByTagName("head").appendChild(script); if (window.addEventListener) ( window.addEventListener("load", async_load, false); ) else if (window.attachEvent) ( window .attachEvent("onload", async_load); ) ))();

But this is an isolated case when downloading a single file is required. Often in practice many files are connected.

Script for asynchronously loading multiple plug-in JavaScript files (function() ( function async_load())( [ "URL_file_1.js", "URL_file_2.js", "URL_file_3.js" ].forEach(function(src) ( var s = document.createElement ("script"); s.type = "text/javascript"; s.src = src; document.getElementsByTagName("head").appendChild(script)); addEventListener) ( window.addEventListener("load", async_load, false); ) else if (window.attachEvent) ( window.attachEvent("onload", async_load); ) ))();

But there is a minus in this implementation - the scripts will be loaded in random order and, accordingly, they will be executed randomly in time. This asynchronous loading script is ideal if JavaScript execution files do not depend on one another and do not depend on the DOM. Otherwise, its use may lead to errors on the page or unexpected execution results. For sequential execution, but asynchronous downloading, you need to specify async=false, then the files will be downloaded in random order, but executed in turn.

HTML 5. Asynchronous loading JavaScript

The HTML 5 standard supports asynchronous loading of JavaScript. This can be done by adding keyword async or defer. For example:

A script that is connected with the defer attribute will be executed without disturbing the order of execution in relation to other scripts and its execution will occur after full load and parsing the page, but before DOMContentLoaded is called.

A script that is connected with the async attribute will be executed as soon as possible after it has been fully loaded, but it does not wait for the document to be parsed before loading the window object. Browsers do not guarantee that scripts will be executed in the same order in which they are connected.

Libraries for asynchronous JavaScript loading

RequireJS is a JavaScript loading module. Optimized for browsers, but it can be used in other environments such as Node, Rhino.

Require(["script"], function(script) ( console.log("start after load script.js"); ));

extsrc.js is a library that runs scripts to be executed after the page is loaded and displayed to the user. Works correctly with document.write.

yepnope.js - allows for asynchronous loading of JavaScript and CSS files.

Yepnope([ "script.js", "style.css" ]);

Easy way to download JavaScript scripts

It turns out that in practice, achieving optimal cross-browser loading of JavaScript scripts that do not block display is difficult, and sometimes impossible. Most optimal way is added to the end of the document before the closing body tag. Due to restrictions different browsers and HTML itself, such a loading option that does not block display can be considered the simplest.

HTML is designed in such a way that the web page is loaded sequentially line by line, loading in turn all the elements included in the html code. And if one of them is unavailable (for example, javaScript from an external site is not read), then further loading of the site stops.

When unreadable JS is at the top of the page, the visitor may not see anything at all.

Therefore, when you use JavaScript from third-party sites on your website, for example to display ads, it is highly advisable to load them asynchronously. In this case, third-party JS will not delay the loading and display of your site.

Unfortunately, not all advertising networks provide the ability to download scripts asynchronously. Therefore, in this article I will tell you how to change synchronous loading code to asynchronous. If the owner of this script does not provide this option.

Standard synchronous JS loading

Typically, calling a script from an external server looks like this:

Asynchronous script loading like Google/Adsense does

I got the idea from Google/Adsense. In order for the script to load asynchronously from the rest of the HTML code, you need to add async to the call code.

And now for the code to load asynchronously, our script call from the external server should look like this:

As you can see, everything is simple. True, this will only work in browsers that support the HTML5 standard. At the time of writing this article, there are an absolute majority of such browsers.

The proposed option is not 100% universal. Many scripts simply stop working after making these changes. According to reviews on the Internet, this method does not work if the element is used in the script document.write.

Reliable asynchronous loading option

If the first suggested option does not work for you. Then take advantage the following recommendations. This is essentially lazy loading. Since the real script call occurs at the very end HTML pages, that is, when all the necessary content of the page is already on the screen.

In the place on the page where you want to display the result JavaScript work you need to create an empty div block:

Then at the end of the page before the closing BODY tag we insert a script for asynchronous loading:

// JavaScript that needs to be loaded asynchronously // move it to real position display document.getElementById("script_block_0").appendChild(document.getElementById("script_ad_0")); // show document.getElementById("script_ad_0").style.display = "block";

If there are several advertising blocks, then for each of them you need to repeat everything, creating unique individual DIV blocks. Don't forget to change the class names and DIV IDs. In my example, it is enough to change the number zero, in the new block it needs to be replaced with 1, and so on.

This option is more complex, but it works everywhere except for very ancient browsers such as Internet Explorer 6. Which, fortunately, is almost never found on user computers.

The reason for writing this post was that more than once I had to notice that inserting button code into a page various services(for example: VKontakte, Facebook, Twitter, Odnoklassniki) led to a noticeable slowdown in page loading and display. It's about about the case when connecting external javascript of these social services is used.
If we use simple static graphic buttons, there are no problems, because This is a minimum of graphics and scripts that are located locally (you can see an example of the implementation here http://pervushin.com/social-button-for-blog.html). But we only see social icons. services, there are no statistics (how many people “liked” our page). Those. if we want to see statistics, we will have to connect external scripts. And here it’s worth keeping in mind that how many of these buttons we have connected, the browser is forced to download so many external scripts, i.e. This additional connections to external servers.

To show what happens if there are scripts in the section on the page, I suggest looking at a number of test examples. I will use FireFox 3.6 and FireBug.

So:
1) The simplest page with one style file, two scripts and three images:













And here is the loading diagram for it:

Please note that all images are loaded only after the longest javascript file has been loaded.
I deliberately made loading dummy_css.css and dummy_js.js quite long. It's just two files:

dummy_css.php

html,body(
margin:0;
padding:0;
}
.img_container(
margin:0 auto;width:500px;
}

dummy_js.php


var param=1;

So, you can see that the js file blocks the loading of all other graphics.

2) Everything is almost the same, but dummy_ js. js is loaded from an external host:

The situation is similar to the previous one:

3) Let's try to swap the css and js files in the head section (css now comes after js):







Let's look at the loading diagram:

Js still blocks images from loading, no matter what host it is loaded from.

4) Let’s increase the css loading time to 4 seconds (html code as in the case of N3):

5) Another interesting case: css is located before js, but css takes longer to load















Here the css is already blocking the loading of images...

6) Move one js inside< body>
















It can be seen that dummy_ js. js blocks loading only the third image, which is located in the html code after it. But if the css takes longer to load, then it will block the loading of graphics. It's not hard to imagine that plugging in external scripts can greatly slow down page loading and rendering, especially if remote server For some reason it takes a long time to respond.

Placing external scripts before

What immediately suggests itself... All scripts whose loading is not critical for the page should be placed right before the closing tag. But this can be done for those scripts in which all the logic is internal and there are no external calls from html code. If, from the html code, some functions are called from scripts that have not yet been loaded, then such situations must be handled in a special way, because you need to track the download.

But there is another problem, let me explain with an example:




$("img").click(function() (
alert($(this).attr("src"));
});
});






If the js before it takes a long time to load, then clicking on the pictures until this script is fully loaded will not lead to anything, because $(document).ready() will only work after js is fully loaded. So if there is some logic on the pages that involves event processing, then this method is not suitable.

So what is needed is a way to load scripts non-blockingly...

Create async.js:



script.src = "dummy_js.js";


and connect it:











$(document).ready(function() (
$("img").click(function() (
alert($(this).attr("src"));
});
});






If the async.js call is placed in , and not before , then the diagram will look like this:

But if it is still more convenient to place async.js in the code, then you should slightly change the contents of async.js:

$(document).ready(function() (
var script = document.createElement("script");
script.src = "dummy_js.js";
document.getElementsByTagName("head") .appendChild(script);
}
);

So, the issue of asynchronous loading has been resolved, but the issue of synchronizing the loading of external scripts and the code that uses them remains open. Let's consider it in practice, connecting social network buttons.

Sometimes there is a need for step-by-step asynchronous data loading. And it could be anything. Starting from loading dependent directories (for example, country -> cities), ending with combining and processing information with various sources(for example, you are using several different services, and each of them spends different time for data generation).

And now a little simpler.

Problems with step-by-step asynchronous data loading in jQuery (ajax)

If you need to link several sequentially executed ajax requests in jQuery, then the most simple solution will - make a call to the following function at the end of each success handler. This is not only quite simple, but also does not cause any problems. For example,

// First request $.ajax(url, ( ... success: function (result) ( // Some code... // Calling the next handler nextLoader(); ) ));

And now, let’s say, in front of you stands a little more difficult task. For example, you need to run a function only after several asynchronous requests have completed (for example, getting bubbles for a site). Of course, they can also be connected in a straight chain, as shown above. But, at the same time, the very essence of asynchrony begins to be lost. And it may also turn out that you are building a dependency between completely unrelated blocks. Agree that the TCI indicator and whois data are not entirely dependent on each other.

Nevertheless, despite all the subtleties, such a problem can be solved quite quickly, albeit a little clumsily.

But what will you do if your task turns out to be even more difficult? When you need to distribute all operations and data loading into stages. For example, you are creating a dialogue template. The first stage will involve obtaining data for all fields (drop-down lists, groups of checkboxes, etc.). At the second stage, handlers for the created fields will be defined. And at the third stage, after “everything and everything” has been fully loaded, open access to various buttons and other interface elements (so that playful hands don’t create a lot of problems for you). Purely visually it might look like this:

You must admit that organizing calls to all functions in one chain is not only not an easy task, but also not very pleasant. In addition, imagine that you need to add several fields and various handlers. What will happen? Your head will start to boil from searching for the right places “where to insert”. Here you already need some kind of mechanism that will allow you to arrange all launches in stages.

We are writing a script to organize step-by-step asynchronous loading in jQuery

The first step is to decide on the completely logical question “why not use ready-made libraries?” As such, there is no answer to this question. Everything very much depends on the task. Since the word "library" usually means:

  • lack of controllability of the code (you cannot change the internal logic of the library; and if you do, then any change may come back to haunt you in the future)
  • the need to study it basic properties and principles (usually this comes down to reading all possible documentation and a bunch of different tests)
  • restrictions on jQuery versions(if complex kernel mechanisms are used, then it is possible to link to the version of the jQuery libraries)
  • possible compatibility problems with other scripts (it is far from certain that the library will behave correctly with other libraries)
  • excessive richness of functionality, and as a result the presence of “additional restrictions”
  • etc.

Note: Do not be misled, because libraries have great amount pros. If you remember, jQuery is the same library. The article focuses on the risks and additional time investment that may be required of you.

That is why you must first decide on your tasks (number, frequency of occurrence), time (how much you are willing to spend on solving the problem), what you are willing to sacrifice (is the ability to quickly correct something important to you; is the size of libraries important; how much? restrictions suit you), preferences (for example, you basically do not use libraries). And only after you answer these questions, try to answer the question “why is it worth or not worth using ready-made libraries?”

If you answered yourself that you want to use libraries, then, as an option, you can download a ready-made test case with a ready-made script at the end of the article.

If you are full of enthusiasm and determination, then we will proceed to further actions.

Compiling requirements for the script

Before implementing anything, it is necessary to draw up small requirements so as not to get sidetracked during implementation. This results in a list like this:

  • Connecting and setting up the script should happen automatically (you only need to connect the js file)
  • Reconnecting the script file should not cause a crash
  • Use only standard jQuery mechanisms
  • The interface should be simple and understandable
  • You can create several independent chains of stages
  • You should always be able to quickly adjust the actions of the script
  • The script should not impose complex restrictions on your code (maximum - the need to add a notification function about its execution)

Note: Always formulate requirements so that during implementation you do not start doing something that is not required at all. And be sure to include “so clear” points in it, so that these points continue to remain “so clear.”

Making a test project

The project structure will look like this:

  • css - directory with styles
  • images - catalog with pictures (for visual effect)
    • ajax-loader.gif - loading image
  • js - directory with scripts
    • jquery-ajax-stage.js - script for staged loading
  • data.json - test data
  • index.html - home page

Note: When creating the structure of the test project, try to bring it as close as possible to the structure of the real project. This will help you catch structure-related problems during development.

File with test data - data.json

This file is needed only to make sure that loading data via ajax will not affect it in any way. You can fill it with any json. For example, like this:

( data: "Lots of data")

Note: Try to make the test cases as similar as possible to the real ones. Even if the function calls are meaningless.

Style file - template.css

This file is needed only to display all the styles separately. Since styles have nothing to do with the development of the script itself. Yes, and it's just considered good practice. The styles themselves:

Left-table ( float: left; padding-right: 10px; ) .right-table ( float: left; padding-right: 10px; ) table.table-loader ( border-spacing: 0px; border-collapse: collapse; width : 300px; ) table.table-loader tr td ( padding: 5px; border: 1px solid #ccc; )

Script file for staged asynchronous loading - jquery-ajax-stage.js

The implementation of step-by-step asynchronous loading itself.

(function (parentObject) ( // Protection against re-definition if(parentObject.eventManager) return; // Define the object parentObject.eventManager = ( // Add a stage addStage: function (el, stageName) ( var elCount = $(el). length; // Check the correctness of the parameters if(elCount > 0 && !!stageName) ( // Such a stage already exists if (($(el).get(0).eventStages = $(el).get(0).eventStages || ())) return; // Define "nfg $(el).get(0).eventStages = ( waiterCount: 0, // Counter of objects waiting for data onEvent: // in this array all functions to be called will be stored); ) ), // Remove the stage removeStage: function (el, stageName) ( var elCount = $(el).length; // Check the correctness of the parameters if(elCount > 0 && !!stageName) ( // Such a stage is found if (( $(el).get(0).eventStages = $(el).get(0).eventStages || ())) ( delete ($(el).get(0).eventStages = $(el).get (0).eventStages || ()); ($(el).get(0).eventStages = $(el).get(0).eventStages || ()) = null ) ) ), // Increase counter of executed functions for the stage addStageWaiter: function (el, stageName) ( var elCount = $(el).length, stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || ()); // Check the correctness of the parameters if(elCount > 0 && !!stageName && stage) ( // Increase the load counter stage.waiterCount++; ) ), // Decrease the counter of executed functions for the stage // I.e. notify that the function has been executed removeStageWaiter: function (el, stageName) ( var elCount = $(el).length, stage = ($(el).get(0).eventStages = $(el).get(0).eventStages || ()); // Check the correctness of the parameters if(elCount > 0 && !!stageName && stage) ( // Decrease the load counter stage.waiterCount--; // Check the state of the stage this.checkStage(el, stageName); ) ), // Check the state of the stage checkStage: function (el, stageName) ( var elCount = $(el).length, stage = ($(el).get(0).eventStages = $(el).get(0) .eventStages || ()); // Check the correctness of the parameters if(elCount > 0 && !!stageName && stage) ( if (stage.waiterCount 0) ( // FIFO queue - first in, first out, as in the stage store. onEvent.shift() (); ) ) ) ), // Add a function call that will run when the entire onLoadStage stage is executed: function (el, stageName, funcHandler) ( var elCount = $(el).length, stage = ($ (el).get(0).eventStages = $(el).get(0).eventStages || ()); // Check the parameters are correct if(elCount > 0 && !!stageName && stage && typeof (funcHandler) = == "function") ( // Add a handler stage.onEvent.push(funcHandler); // Check the stage state this.checkStage(el, stageName); ) ) ); ))(window);

Test page - index.html

The test page turned out to be quite large (about 160 lines), so only part of the code will be given. Full version you can always find the file in the zip archive.

We include all the necessary files:

Let's create a couple of test blocks. The block includes a launch button and a table where data will be recorded step by step.

Start downloading

Let's define a function that will create test functions for the stages:

function getFakeLoader(storage, currStage, startDate, selector) ( return function () ( setTimeout(function () ( $.ajax("data.json?callback=?", ( contentType: "text/plain; charset=utf-8 ", dataType: "jsonp", jsonpCallback: function (result) ( $(selector).html("Time from start: " + ((+new Date() - startDate)/1000.0).toFixed(2) + " seconds "); // Write the loading time window.eventManager.removeStageWaiter(storage, currStage); // Decrease the counter ) )); ), Math.random() * (3000) + 1000 // Delay from 1 to 4 seconds); ; )

We create a function that will describe the logic for each test block:

function formTestFor(selector) ( $(selector + " .start-process").click(function () ( var startDate = new Date(); // For beauty, add pictures setLoaderImgs(selector); // Define the stages window.eventManager .addStage($(selector + " .table-loader"), "1"); window.eventManager.addStage($(selector + " .table-loader"), "2"); window.eventManager.addStage($ (selector + " .table-loader"), "3"); // Block the button until the end of the stage $(selector + " .start-process").attr("disabled", "disabled"); waiting for 3 loads per stage // In fact, these actions should occur in the places where the stage functions are launched window.eventManager.addStageWaiter($(selector + " .table-loader"), "1"); ($(selector + " .table-loader"), "3"); // Now create in reverse order(for clarity) loading functions // In fact, these actions should occur in places where the functions are defined window.eventManager.onLoadStage($(selector + " .table-loader"), "2", getFakeLoader($(selector + " .table- loader"), "3", startDate, selector + " .row-3 .td-1")); ... window.eventManager.onLoadStage($(selector + " .table-loader"), "1", getFakeLoader($(selector + " .table-loader"), "2", startDate, selector + " .row -2 .td-3")); // Add a regular text output of the finished stages window.eventManager.onLoadStage($(selector + " .table-loader"), "1", function () ( $(selector + " .row-1-end td").html ("The first stage ended in " + ((+new Date() - startDate)/1000.0).toFixed(2) + " seconds. The next stage begins." )); ... window.eventManager.onLoadStage($(selector + " .table-loader"), "3", function () ( $(selector + " .row-3-end td").html("The third stage has ended for " + ((+new Date() - startDate)/1000.0).toFixed(2) + " seconds.
Loading is complete"); )); // After the third stage is completed, unlock the onLoadStage button window.eventManager.onLoadStage($(selector + " .table-loader"), "3", function () ( // Unlock the button $(selector + " .start-process").attr("disabled", null )); // Now run the first stage functions getFakeLoader($(selector + " .table-loader"), "1", startDate, selector + " .row-1 .td-1")(); getFakeLoader($(selector + " .table-loader"), "1", startDate, selector + " .row-1 .td-2")(); getFakeLoader($(selector + " .table-loader"), "1", startDate, selector + " .row-1 .td-3")(); // Observe... ));

Now we collect all the files into the project and move on to initial testing and demonstration.

Let's see the result

Open the index.html file in the browser. The following interface should be displayed:

As you can see, we have two buttons available for launching parallel processes. After clicking on them, the download for the corresponding blocks should begin. And it will look something like this:

After both processes have completed their execution, look at the time and make sure that the stages occurred in the right order:

Having completed the initial testing, we move on to checking the requirements:

  • Connecting and setting up the script should happen automatically (you only need to connect the js file) - Yes
  • Reconnecting the script file should not cause a crash - Yes
  • Use only standard jQuery mechanisms - Yes (only selector functionality was used)
  • The interface should be simple and understandable - Yes
  • You can create several independent chains of stages - Yes
  • You should always be able to quickly correct the actions of the script - Yes (the script is written quite simply; the main part consists of checking the input data)
  • The script should not impose complex restrictions on your code (maximum - the need to add a function to notify about its completion) - Yes (inside the getFakeLoader functions, only the function to notify about its completion is called)

Now you have a simple and clear script that will help you quickly organize the step-by-step execution of functions, be it loading data or simply performing operations.

) I wrote about the effect JavaScript files have on the Critical Rendering Path (CRP).


JavaScript is a blocking resource for the parser. This means that JavaScript blocks parsing of the HTML document itself. When the parser reaches the ‹script› tag (it doesn’t matter whether it’s internal or external), it stops, picks up the file (if it’s external) and runs it.

This behavior can be problematic if we load multiple JavaScript files on a page, as it increases first render time even if the document doesn't actually depend on those files.


Luckily, the element has two attributes, async and defer, which give us control over how external files are loaded and executed.

Normal execution

Before understanding the difference between these two attributes, let's look at what happens in their absence. As stated earlier, by default JavaScript files interrupt parsing of the HTML document until they are received and executed.
Let's take an example where the element is located somewhere in the middle of the page:


... ... ....

This is what will happen when the parser processes the document:

HTML parsing is paused until the script is downloaded and executed, thereby increasing the amount of time until the first render.

async attribute

Async is used to indicate to the browser that the script Maybe be executed asynchronously.
The HTML parser does not need to stop when it reaches the tag to load and execute. Execution can occur after the script is received in parallel with document parsing.



The attribute is available only for files connected externally. If external file has this attribute, it can be loaded while the HTML document is still being parsed. The parser will pause to execute the script as soon as the script file is loaded.



defer attribute

The defer attribute tells the browser that the script should be executed after the HTML document has been completely parsed.



As with asynchronous script loading, the file can be loaded while the HTML document is being parsed. However, even if the script file is fully loaded before the parser finishes, it will not be executed until the parser finishes running.



Asynchronous, delayed or normal execution?

So, when should you use asynchronous, lazy, or normal JavaScript execution? As always, it depends on the situation and there are several questions that can help you make the right decision.

Where is the element located?

Asynchronous and lazy execution are most important when the element is not at the very end of the document. HTML documents are parsed in order, from opening to closing. If the external JavaScript file is placed immediately before the closing tag, then using async and defer becomes less appropriate, since the parser will have parsed most of the document by then and the JavaScript files will no longer have an effect on it.

Is the script self-sufficient?

For files that do not depend on other files and/or do not have any dependencies, the async attribute will be most useful. Since we don't care when the file is executed, asynchronous loading is the most suitable option.

Does the script rely on a fully parsed DOM?

In many cases, the script file contains functions that interact with the DOM. Or perhaps there is a dependency on another file on the page. In such cases, the DOM must be completely parsed before the script is executed. Typically, such a file is placed at the bottom of the page to ensure that everything has been parsed for it to work. However, in a situation where for some reason the file must be located in a different location, the defer attribute can be useful.

The script is small and dependent?

Finally, if the script is relatively small and/or depends on other files, then it may be worth defining it inline. Although the inline code blocks parsing of the HTML document, it should not be much of a problem if the document is small in size. Also, if it depends on other files, some minor locking may be needed.

Support and modern browser engines

Support for the async and defer attributes is very common:




It's worth noting that the behavior of these attributes may vary slightly between JavaScript engines. For example, in V8 (used in Chromium), an attempt is made to parse all scripts, regardless of their attributes, into a separate dedicated thread for script execution. Thus, the "parser blocking" nature of JavaScript files should be minimized by default.