Stoyan (@stoyanstefanov) is a former Facebook and Yahoo! engineer, writer ("JavaScript Patterns", "React: Up and Running"), speaker (JSConf, Velocity, Fronteers), toolmaker (Smush.it, YSlow 2.0) and a guitar hero wannabe.
Problem
As the previous post puts it:
A slow CSS prevents the JavaScript following it from executing.
And in addition, when the JS following the CSS is inline, it’s naturally synchronous, which compounds the undesirable effect.
Example
As a demonstration let’s take a look at a sample page that has:
- Slow CSS1 that takes 5 seconds
- External async JS1
- Inline JS in the HEAD
- Slow CSS2 that takes 10 seconds
- External async JS2
- Inline JS in the body
Here’s the relevant code:
<head> <script> const log = {}; const start = +new Date; </script> <title>Baseline: before</title> <link rel="stylesheet" href="css1.css.php" type="text/css" /> <script src="js.js" async></script> <script> log['inline HEAD script'] = +new Date - start; </script> <link rel="stylesheet" href="css2.css.php" type="text/css" /> <script src="js2.js" async></script> </head> <body> <script> log['inline BODY script'] = +new Date - start; </script> <!-- --> </body>
The live page to play with yourself.
And the waterfall:
The waterfall looks as expected: 5s artificially delayed CSS takes 5s, the second one 10s, all resources are loaded in parallel and the whole thing is done in 10s.
The problem is not the loading but the execution of JavaScript. A sample page load gives us times like:
The first external JS is quick, but the first inline and second external are delayed by 5s, exactly the time the first CSS takes. The execution of the second inline script is delayed by 10s, waiting for the second CSS.
Rounding the results to the nearest second would give us something like:
- 0s external script
- 5s inline HEAD script
- 5s external script 2
- 10s inline BODY script
- 10s DOMContentLoaded
- 10s onload 10280
Solution with data URIs
The solution to this behavior where CSS blocks execution offered in the previous post was to use data URIs to externalize the inline scripts so they can benefit from the async
attribute, which otherwise doesn’t apply. As a result, the execution times look like:
- 0s external script
- 0s inline HEAD script
- 0s external script 2
- 0s inline BODY script
- 0s DOMContentLoaded
- 10s onload 10280
So much better!
But turns out there’s a simpler solution…
type=”module”
How about using inline scripts and making them as modules? Like so:
<head> <script> const log = {}; const start = +new Date; </script> <title>Baseline: before</title> <link rel="stylesheet" href="css1.css.php" type="text/css" /> <script src="js.js" async></script> <script type="module"> log['inline HEAD script'] = +new Date - start; </script> <link rel="stylesheet" href="css2.css.php" type="text/css" /> <script src="js2.js" async></script> </head> <body> <script type="module"> log['inline BODY script'] = +new Date - start; </script> <!-- --> </body>
Results:
- 0s external script
- 0s inline HEAD script
- 10s external script 2
- 10s inline BODY script
- 10s DOMContentLoaded
- 10s onload 10280
Interesting. The first inline script is fast! However the second external JS went from 5 to 10s and the rest is all the same.
Let’s call this option A.
Option B: async type="module"
How about adding async to the script tag? Like so:
<head> <script> const log = {}; const start = +new Date; </script> <title>Baseline: before</title> <link rel="stylesheet" href="css1.css.php" type="text/css" /> <script src="js.js" async></script> <script async type="module"> log['inline HEAD script'] = +new Date - start; </script> <link rel="stylesheet" href="css2.css.php" type="text/css" /> <script src="js2.js" async></script> </head> <body> <script async type="module"> log['inline BODY script'] = +new Date - start; </script> <!-- --> </body>
In non-modules, async
didn’t help (see previous post), but now?
Results:
- 0s external script
- 0s inline HEAD script
- 0s external script 2
- 0s inline BODY script
- 0s DOMContentLoaded
- 10s onload 10280
Oh yes! Option B wins! No more blocking JavaScript execution. And no more data URI hacks.
Safari
This works consistently in Firefox, Brave (and I assume Chrome and Edge) and Safari. In fact, in Safari option A = option B. Looks like modules are already async by default.
Another note on Safari: the top level (global) const log
and const start
were not defined in the inline module. That feels like the correct behavior to me, but anyway, it happened only in Safari. Making these window.log
and window.start
fixed the problem. Something to be aware to test if you have variables shared between scripts.
Happy unblocking
…and modularly async-ifying!