Asynchronous inline scripts via data: URIs

February 9th, 2019. Tagged: browsers, CSS, JavaScript

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.

The test file is here

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>

Test page

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 links? 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>

Test page

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!

Tell your friends about this post on Facebook and Twitter

Sorry, comments disabled and hidden due to excessive spam.

Meanwhile, hit me up on twitter @stoyanstefanov