Asynchronous loading of scripts. We are creating a test project. Support and modern browser engines

Sometimes there is a need for step-by-step asynchronous loading of data. 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 connect 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 full load"anything and everything", 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 example 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
    • template.css - test project 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 anything. 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.

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 let's 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"));
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.

Greetings, friends! Did you know that JavaScript loading is one of the biggest bottlenecks in website performance? Today my main task is to explain what a script is and how it affects the speed and performance of the site.

A browser that loads a script tag stops rendering the page until the script has loaded and executed. The page is blocked and the browser does not respond to user actions for a couple of seconds. The delay time depends on several factors:

  • configurations,
  • Internet connection speed,
  • file size and others...

For this reason, the Google PageSpeed ​​Insights website speed analyzer recommends removing from the top of the page JavaScript code, blocking its display. A good practice is to place scripts at the bottom of the site, for example before the closing tag or set up asynchronous loading.

If the script code affects the display of the top part of the site, do not place it in separate file, and embed directly in HTML.

JS can change the site's content and even redirect to a different URL. In this case, connecting the script at the end of the document will lead to the effect of “twitching” the page, loading new or changing existing elements at the top.

Applying the async and defer attributes to the script tag

Let's figure out what asynchronous and deferred work in JavaScript is and what the fundamental difference is between the async and defer attributes. But first, let's look at the sequence of document processing when normal connection script tag.

1 < src = "example.js" >

IN clear example I will use the following symbols:

— page processing
— script loading
— script execution

Thus, the processing sequence occurs according to the following scheme:

Parsing the HTML code is interrupted while the script is loading and executing, after which it continues. There is a delay in displaying the web page.

defer attribute

The defer attribute allows the browser to start downloading js files in parallel without stopping further processing of the page. They are executed after full analysis object model document (from the English Document Object Model, abbreviated DOM), with the browser guaranteeing consistency based on the order in which the files are included.

1 < defer src = "example.js" >

async attribute

Support for the async attribute appeared in HTML5, it allows the browser to download js files in parallel and execute them immediately after downloading, without waiting for the rest of the page to be processed.

1 < async src = "example.js" >

Processing sequence diagram:

This is an asynchronous download. This attribute is recommended for use in scripts that do not have a significant impact on the display of the document. These include statistics collection counters ( Google Analytics, Yandex Metrica), advertising network codes (Yandex Advertising Network, Google AdSense), buttons social networks and so on.

Website loading speed is one of the ranking factors in Google.

Asynchronous JavaScript connection Reduces page loading time due to the absence of delay. Along with this, I recommend compressing and merging js files into one, for example, using the . Users like fast sites 😎

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 loading script like Google/Adsense does it

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 online, 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 tag BODY insert the 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.


) 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 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, deferred 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.

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 propose to consider 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:
But there is another problem, let me explain with an example:
$("img").click(function() (
});
});






$(document).ready(function() (

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);