Speed is everything. It always is … there is no such thing as “the page loaded too fast”. To make matters worse, today we have Google Pagespeed Insights to make our lives miserable (a whole different topic). They usually recommend loading your JS asynchronous (async) or on the footer.
Why?
Javascript is render blocking, which means the browser can’t work on displaying the page while the script is running. So, consider this:
|
<html> <head> <title>Example page</title> <script src="myscript.js"></script> </head> <body> <div id="theDiv"></div> <script> changeText( 'theDiv', 'Hello World' ); </script> </body> </html> |
|
// myscript.js changeText = function( id, text ) { var el = document.getElementById( id ); el.innerHTML( text ); }; |
If myscript.js takes 1 minute to load, the whole page will be delayed 1 minute, no matter how simple it is. Not cool. To fix this, we just add async to the script tag. This makes the browser start the load of the asset (myscript.js) but continue parsing html. The problem now is that because the asset is still loading, and will arrive in 1 minute, the function changeText() is not available by the time the browser gets to it. Your code will not run, but the page has loaded, which is good news.
|
<html> <head> <title>Example page ASYNC( Failing )</title> <script src="myscript.js" async></script> </head> <body> <div id="theDiv"></div> <script> // If myscript.js takes 1 minute to load, // this function call will fail ... editor is undefined // but at least the page finished rendering editor.text( 'theDiv', 'Hello World' ); </script> </body> </html> |
How?
You can implement a queue or a callback*. Both ways work just as nicely, but they have different use cases. (* you can use Promises but that’s kind of a callback, just real fancy)
Queues work better if you need to do many calls to the same script in different parts of the page but all depend on the previous call one way or another. Here you go (Notice the change on the js file):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
<html> <head> <title>Example page ASYNC with queue</title> <script src="myscript.js" async></script> <script> queue = []; </script> </head> <body> <div id="theDiv"></div> <script> queue.push( function() { editor.text( 'theDiv', 'Hello' ); } ); </script> <div id="theSecondDiv"></div> <script> queue.push( function() { editor.text( 'theSecondDiv', 'World' ); } ); </script> </body> </html> |
|
// myscript.js changeText = function( id, text ) { var el = document.getElementById( id ); el.innerHTML( text ); }; // Execute all the functions on the queue queue.forEach( function( q ) { q(); } ); // Lets stop queueing and execute immediately queue.push = function( q ) { q(); }; |
Tiny change … works like a charm … once myscript.js loads 1 minute later, it’ll execute all the functions pushed to the queue.
Potential pitfall
Lets say instead of the script loading slow, it loads super fast. If you are working on the DOM, make sure the elements you want, are in place by the time the queue executes. In the example this is mitigated by adding to the queue after the dom element ( <div> ).
Implementing a callback is a bit easier, but works better if you only need to run code once. Notice, no changes are needed on myscript.js from the original version.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
<html> <head> <title>Example page with script loader</title> <script> function loadScript( url, callback ) { if ( !callback ) callback = function(){}; var script = document.createElement( "script" ); script.type = "text/javascript"; if (script.readyState) { //IE script.onreadystatechange = function () { if (script.readyState === "loaded" || script.readyState === "complete") { script.onreadystatechange = null; callback(); } }; } else { script.onload = function () { callback(); }; } script.src = url; document.getElementsByTagName("body")[0].appendChild(script); } </script> </head> <body> <div id="theDiv"></div> <script> loadScript( 'myscript.js', function() { editor.text( 'theDiv', 'Hello World' ); } ); </script> </body> </html> |
You can make a hybrid of these 2 approaches where the callback is the code to process the queue … but it feels a bit ghetto to me. I’m sure there is some good use case where this would be desirable. The same pitfall as above applies, but as long as you load the script after the dom element you’ll be ok.
As always there is a jQuery way to do this. And it is in fact quite nice, what I don’t like about it is having to load jQuery synchronous. That being said, you can probably turn all your jquery into a queue and now “we all happy“.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
<html> <head> <title>Example page with script loader</title> <script src="https://code.jquery.com/jquery-3.2.1.min.js"> </head> <body> <div id="theDiv"></div> <script> $.getScript( 'myscript.js' ) .done( function() { editor.text( 'theDiv', 'Hello World' ); } ) .fail( function() { // WTF script no loadey } ); </script> </body> </html> |