📜 ⬆️ ⬇️

Immersion in the dark waters of loading scripts

image
Literally a few hours ago, HTML5 Rocks had a wonderful article about the current state of affairs regarding the loading of scripts on the page. I present to you her translation. Amendments can send in private messages.

Introduction


In this article I want to teach you how to load JavaScript into a browser and execute it.

No, wait, come back! I know that it sounds ordinary and simple, but remember that it happens in the browser, where theoretically simple turns into a set of quirks defined by heredity. Knowing these quirks will help you choose the fastest, least disruptive way to download scripts. If you are in a hurry, you can go directly to the quick reference guide at the end of the article.

For starters, here’s how the specification defines the different ways to load and execute scripts:
')
What is written in the WHATWG about loading scripts

Like all WHATWG specifications, at first glance this specification looks like the effects of a cluster bomb at a Scrabble factory. But, after reading it for 5 times and wiping the blood from your eyes, you begin to find it quite interesting:

My first script connection


<script src="//other-domain.com/1.js"></script> <script src="2.js"></script> 

Oh, blissful simplicity. In this case, the browser will download both scripts in parallel and execute them as soon as possible, keeping the specified order. “2.js” will not be executed until “1.js” is executed (or cannot do this), “1.js” is not executed until the previous script or style is executed, etc. etc.

Unfortunately, browsers block further page rendering while this all happens. Since the days of the “first century of the web”, this is due to the DOM API, which allows strings to be added to content that is parsed by the parser, for example using document.write . More modern browsers will continue to scan and parse the document in the background and download the necessary third-party content (js, images, css, etc.), but the drawing will still be blocked.

This is why gurus and productivity experts advise placing script elements at the end of the document, because it blocks the least amount of content. Unfortunately, this means that your script will not be seen by the browser until all HTML has been downloaded, and the loading of CSS, images and iframes is already running. Modern browsers are smart enough to give priority to JavaScript over the visual part, but we can do better.

Thanks, IE! (no, I am without sarcasm)


 <script src="//other-domain.com/1.js" defer></script> <script src="2.js" defer></script> 

Microsoft found these performance problems and entered “defer” in Internet Explorer 4. Basically, it says the following: “I promise not to insert anything into the parser using things like document.write . If I break this promise, you can punish me in any way that is acceptable to you. ” This attribute was introduced in HTML4 and it also appeared in other browsers.

In the example above, the browser will download both scripts in parallel and execute them right before DOMContentLoaded is DOMContentLoaded , the order is preserved.

Like a cluster bomb in a sheep factory, “defer” became a hairy mess. In addition to “src” and “defer”, as well as script tags and dynamically loaded scripts, we have 6 script addition patterns. Naturally, browsers have not agreed on the order in which they should be performed. Mozilla wonderfully described this problem back in 2009.

WHATWG made this behavior explicit by declaring that “defer” would have no effect on scripts that were dynamically added or did not have “src”. Otherwise, scripts with “defer” should be launched in the specified order after the document has been parsed.

Thanks, IE! (well, now with sarcasm)

One was given - the other was taken away. Unfortunately, there is a nasty bug in IE4-9 that can trigger the execution of scripts in the wrong order . Here is what happens:

1.js
 console.log('1'); document.getElementsByTagName('p')[0].innerHTML = 'Changing some content'; console.log('2'); 

2.js
 console.log('3'); 

Suppose that there is a paragraph on the page, the expected order of the logs is [1, 2, 3], but in IE9 and lower the result will be [1, 3, 2]. Some DOM operations force IE to pause the execution of the current script and to begin executing other scripts in the queue before continuing.

However, even in implementations without a bug, such as IE10 and other browsers, script execution will be delayed until the entire document is loaded and parsed. This is useful if you are waiting for DOMContentLoaded in any case, but if you want to get a real performance boost, then soon you will start using listeners and bootstrapping ...

HTML5 to the rescue


 <script src="//other-domain.com/1.js" async></script> <script src="2.js" async></script> 

HTML5 gave us a new attribute “async”, which assumes that you also do not use document.write, but at the same time do not expect the end of parsing the document. The browser will download both scripts in parallel and execute them as soon as possible.

Unfortunately, since they will try to execute as soon as possible, “2.js” can be executed earlier than “1.js”. This is great if they are independent of each other. For example, if "1.js" is a tracking script that has nothing to do with "2.js". But if “1.js” is a CDN copy of jQuery, on which “2.js” depends, then your page will be covered with errors, like after a cluster bomb in ... I don't know ... here I didn't invent anything.

I know we need a javascript library!


The Holy Grail contains a set of scripts that load immediately, without blocking the rendering of the page and run as quickly as possible, in the order in which we added them. Unfortunately, HTML hates you and will not allow you to do this.

The problem was solved with the help of javascript in different manners. Some methods require you to make changes to JavaScript, to wrap everything in a callback that the library will call in the correct order (for example, RequireJS ). Others used XHR for parallel loading, and then eval() in the correct order, which does not work for scripts on another domain, if there is no CORS header and there is no browser support for it. Some even used super-magic hacks, as was done in the latest LabJS.

Hacks deceived the browser in every way so that it loads the resource, causing an event at the end of the download, but did not start its execution. In LabJS, the script was first added with an incorrect mime type, for example
 .      ,   ,     mime-,    ,             .     ,   ,  ,  HTML5 ,         . 

, , JavaScript- , . , ? , ? ? ? .

DOM !
, HTML5, .
The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset…
" ":
[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });

. , , mime-, , . , , , HTML5 , .

, , JavaScript- , . , ? , ? ? ? .

DOM !
, HTML5, .
The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset…
" ":
[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });

. , , mime-, , . , , , HTML5 , .

, , JavaScript- , . , ? , ? ? ? .

DOM !
, HTML5, .
The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset…
" ":
[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });
. , , mime-, , . , , , HTML5 , .

, , JavaScript- , . , ? , ? ? ? .

DOM !
, HTML5, .
The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset…
" ":
[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });
. , , mime-, , . , , , HTML5 , .

, , JavaScript- , . , ? , ? ? ? .

DOM !
, HTML5, .
The async IDL attribute controls whether the element will execute asynchronously or not. If the element's "force-async" flag is set, then, on getting, the async IDL attribute must return true, and on setting, the "force-async" flag must first be unset…
" ":
[ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; document.head.appendChild(script); });

Scripts that are created and added dynamically, asynchronous by default , they do not block rendering and are executed immediately after loading, which means that they may appear in the wrong order. However, we can explicitly mark them asynchronous:
 [ '//other-domain.com/1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); }); 

This will give our scripts a combination with behavior that cannot be achieved in pure HTML. Explicitly specified asynchronous, the scripts are added to the queue for execution, the same as they were in our first example in pure HTML. However, dynamically created, they will be executed outside the document parsing, which will not block rendering until they are loaded (do not confuse non-synchronous script loading with synchronous XHR, which is never a good thing).

The script above should be embedded in the head pages, starting the download queue as early as possible, without breaking the gradual rendering, and starting to execute as early as possible, in the order you specified. "2.js" can be freely downloaded to "1.js", but it will not be executed until "1.js" is successfully downloaded and executed or can not do any of this. Hooray! Asynchronous loading, but performing in order!

Loading scripts with this method is supported wherever the async attribute is supported , with the exception of Safari 5.0 (5.1 is fine). In addition, all versions of Firefox and Opera, which do not support the async attribute, still run dynamically-added scripts in the correct order.

This is the fastest way to load scripts, right? So?


Well, if you dynamically decide which scripts to download - yes, otherwise - maybe not. In the example above, the browser must parse and load the script to determine which scripts to load. It hides your scripts from preload scanners. Browsers use these scanners to locate resources that you are likely to visit next and to discover page resources while the parser is locked by another resource.

We can add detectability back by putting this in the head of the document:
 <link rel="subresource" href="//other-domain.com/1.js"> <link rel="subresource" href="2.js"> 

This tells the browser that the page requires 1.js and 2.js and is visible to preloaders. link[rel=subresource] is similar to link[rel=prefetch] , but with different semantics . Unfortunately, this is only supported in Chrome, and you need to declare the scripts to load twice: the first is in the link elements, the second is in your script.

This article depresses me


The situation is depressing and you should feel depressed. There is still no declarative, no repetition method for loading scripts quickly and asynchronously, while at the same time controlling the order of execution.

With the advent of HTTP2 / SPDY, you can reduce the overhead resources to the point where the delivery of scripts in small self-cached files will be the fastest way. Just imagine:
 <script src="dependencies.js"></script> <script src="enhancement-1.js"></script> <script src="enhancement-2.js"></script> <script src="enhancement-3.js"></script> … <script src="enhancement-10.js"></script> 

Each enhancement script deals with a specific component of the page, but requires helper functions in dependencies.js. Ideally, we want to load everything asynchronously, then execute the enhancement script as early as possible, in any order, but after dependencies.js. This is a progressive progressive improvement!

Unfortunately, there is no declarative way to achieve this, only if you modify the scripts themselves to track the state of loading dependencies.js. Even async = false will not solve this problem, because the implementation of enhancement-10.js will be blocked by 1-9. In fact, there is only one browser in which you can achieve this without hacks ...

IE has an idea!


IE loads scripts differently than other browsers.
 var script = document.createElement('script'); script.src = 'whatever.js'; 

IE is starting to download "whatever.js" now, while other browsers will not start downloading until the script has been added to the document. IE also has a "readystatechange" event and a "readystate" property that report the download process. This is actually very useful because it allows us to manage the loading and execution of scripts independently of each other.
 var script = document.createElement('script'); script.onreadystatechange = function() { if (script.readyState == 'loaded') { // Our script has download, but hasn't executed. // It won't execute until we do: document.body.appendChild(script); } }; script.src = 'whatever.js'; 

We can build complex dependency models by choosing when to add scripts to the document. IE supports such a model, starting with the 6th version. Quite interesting, but it has the same drawback with browser detectability as with async=false .

Enough! How should i download scripts?


OK OK. If you want to load scripts in a way that does not block rendering, does not require duplication, and has excellent browser support, then I suggest this:
 <script src="//other-domain.com/1.js"></script> <script src="2.js"></script> 

It is this one. At the end of the body element. Yes, being a web developer is like being the king of Sisyphus (boom! 100 hipster points for mentioning Greek mythology!). The limitations of HTML and browsers do not allow us to do much better.

I hope that JavaScript modules will save us by providing a declarative, non-blocking way to load scripts and have control over the order in which they are run, even if this requires writing scripts as modules.

Yee, should there be something better that we can use now?


Well, for the sake of bonus points, if you are seriously thinking about performance and are not afraid of complexity and duplication, you can combine several tricks considered.

First, we add a subresource declaration for preloaders:
 <link rel="subresource" href="//other-domain.com/1.js"> <link rel="subresource" href="2.js"> 

Then we load our scripts directly into the head of the document using JavaScript, using async=false , giving way to the IE script based on readystate, which, in turn, gives way to defer.
 var scripts = [ '1.js', '2.js' ]; var src; var script; var pendingScripts = []; var firstScript = document.scripts[0]; // Watch scripts load in IE function stateChange() { // Execute as many scripts in order as we can var pendingScript; while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') { pendingScript = pendingScripts.shift(); // avoid future loading events from this script (eg, if src changes) pendingScript.onreadystatechange = null; // can't just appendChild, old IE bug if element isn't closed firstScript.parentNode.insertBefore(pendingScript, firstScript); } } // loop through our script urls while (src = scripts.shift()) { if ('async' in firstScript) { // modern browsers script = document.createElement('script'); script.async = false; script.src = src; document.head.appendChild(script); } else if (firstScript.readyState) { // IE<10 // create a script and add it to our todo pile script = document.createElement('script'); pendingScripts.push(script); // listen for state changes script.onreadystatechange = stateChange; // must set src AFTER adding onreadystatechange listener // else we'll miss the loaded event for cached scripts script.src = src; } else { // fall back to defer document.write('<script src="' + src + '" defer></'+'script>'); } } 

A few tricks, then minification, and here are 362 bytes + URL of your scripts:
 !function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[ "//other-domain.com/1.js", "2.js" ]) 

Are those extra bytes worth, compared to simply connecting scripts? If you already use JavaScript to conditionally load scripts, as the BBC does , then you can also benefit from the early launch of these downloads. Otherwise, rather not, stick to the easy way with the connection at the end of the body.

Uh, now I know why the WHATWG section on script downloads is so huge. I need a drink.

Quick Reference


Simple script elements

 <script src="//other-domain.com/1.js"></script> <script src="2.js"></script> 

The specification says: Download together, follow in order after any waiting CSS, block the drawing until you finish
The browser replies: Yes, sir!

Defer

 <script src="//other-domain.com/1.js" defer></script> <script src="2.js" defer></script> 

The specification says: Download together, follow the order before DOMContentLoaded. Ignore "defer" for scripts without "src".
IE <10 replies: It’s possible that I will run 2.js in the middle of 1.js. This is fun ??
Red zone browsers respond: I have no idea what a defer is, I will load the scripts as if they are not.
The rest of the browsers respond: Ok, but it is possible that I will not ignore the "defer" for scripts without "src"

Async

 <script src="//other-domain.com/1.js" async></script> <script src="2.js" async></script> 

The specification says: Download together, execute in any order in which they are downloaded.
Red zone browsers respond: What is "async"? I will download the scripts as if it is not.
The rest of the browsers respond: Yes, good.

Async false

 [ '1.js', '2.js' ].forEach(function(src) { var script = document.createElement('script'); script.src = src; script.async = false; document.head.appendChild(script); }); 

The specification says: Download together, follow in order when everything is loaded.
Firefox <3.6, Opera responds: I have no idea what "async" is, but it so happens that I run the scripts added via JS in the order in which they were added.
Safari 5.0 answers: I understand "async", but I don’t know how to set it to false via JS. I will execute your scripts when they come, in any order.
IE <10 responds: I have no idea about "async", but there is a workaround using "onreadystatechange".
Other red zone browsers respond: I don't understand "async", I will execute your scripts when they come, in any order.
The others answer: I am your friend, we will make it as according to the textbook.

Source: https://habr.com/ru/post/182310/


All Articles