Asynchronous inline scripts via data: URIs

  • You are here: Free PHP » Uncategorized » Asynchronous inline scripts via data: URIs

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.

Well, ditch inline scripts, you may say, but 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.

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: URL. 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!

Powered by Gewgley