Inline scripts are synchronous. "Well, duh!" you may say. That's a feature, not a bug. Because accessing a variable after an inline script should succeed. And that's fine. But not great.
When is this bad? Well, inline scripts cause stylesheets to be blocking. Wait, what? Steve explained it 10 years ago, and it's still relevant today. Allow me to demonstrate.
The baseline
Say we have:
- CSS1 artificially delayed to take 5 seconds to load
- External async JS1 that loads fine and prints to the console
- CSS2 that takes 10 seconds to load
- External async JS2
<link rel="stylesheet" href="css1.css.php" type="text/css" /> <script src="js1.js" async></script> <link rel="stylesheet" href="css2.css.php" type="text/css"/> <script src="js2.js" async></script>
What we have is a waterfall like this:
... and in the console (where we log DOMContentLoaded
and onload
too) you see that even though CSS takes forever to load, it only blocks onload
. The external JS execution is just fine.
Add inline scripts
Now what happens when you add an inline script after each external JS? Code:
<link rel="stylesheet" href="css1.css.php" type="text/css" /> <script src="js1.js" async></script> <script>console.log('inline script 1 ' + (+new Date - start));</script> <link rel="stylesheet" href="css2.css.php" type="text/css"/> <script src="js2.js" async></script> <script>console.log('inline script 2 ' + (+new Date - start));</script>
Now the first external async JS runs fine, but then the inline script and the second external JS are delayed by the slowness of the first CSS file. That's not good. The second inline script is blocked by the second even slower CSS. (And if there were more external JS files they'd be blocked too). DOMContentLoaded
is blocked too.
external script 1 87 inline script 1 5184 external script 2 5186 inline script 2 10208 DOMContentLoaded 10216 onload 10227
There's a good reason why browsers do this, e.g. the inline script may request layout info and for that to work, the CSS must be downloaded and applied. But it's less than perfect.
Motivation
Why is this an issue? Can you just ditch inline scripts, if they make your execution slower. Well that's not always an option. Maybe you need some work that only the server can do (or it's better done by the server) and then made available on the client side. Maybe you want to add a third-party snippet to the page, social buttons, analytics and such. Do you add these at the top before any link
s? That means potentially slowing down your app's scripts. Do you move them to the very bottom? Maybe better, if that's even an option, but they still block. No matter what comes between a link
and an inline script
, the blocking behavior still exists.
So how do you prevent the inline scripts from blocking?
Externalize
If only there was a way to make an inline script appear external to the browser... But yes - make the src
point to a data:
URI. Doesn't need to be base64-encoded either.
So you take this:
<script>console.log('inline script 1 ' + (+new Date - start));</script>
... and turn it into this:
<script async src="data:text/javascript,console.log%28%27inline%20script%201%20%27%20%2B%20%28%2Bnew%20Date%20-%20start%29%29%3B"></script>
And voila! No more blocking! Sync becomes async! Everybody dance!
inline script 1 2 inline script 2 4 DOMContentLoaded 10 external script 1 271 external script 2 277 onload 10270
Looks weird, but hey, it works! And there days learning from view:source is almost impossible anyway.
Notes
I tested this hack in Chrome (Mac/PC), Firefox (Mac/PC), Safari (Mac), Edge (PC). Works everywhere except Edge. Oh well, at least it behaves as if nothing was changed, so it doesn't hurt Edge.
Couple of alternative approaches that didn't work for me were:
- adding
defer
to the inline script. Steve suggests in his post that it used to work in Firefox 3.1 and some old IE. Not anymore though. - adding
async
to the inline script - it's not allowed, but didn't hurt to try
Thank you all for reading and go externalize!