Rocket Loader is in the news again. One of Cloudflare's earliest web performance products has been re-engineered for contemporary browsers and Web standards.
No longer a beta product, Rocket Loader controls the load and execution of your page JavaScript, ensuring useful and meaningful page content is unblocked and displayed sooner.
For a high-level discussion of Rocket Loader aims, please refer to our sister post, We have lift off - Rocket Loader GA is mobile!
Below, we offer a lower-level outline of how Rocket Loader actually achieves its goals.
Prehistory
Early humans looked upon Netscape 2.0, with its new ability to script HTML using LiveScript, and <BLINK>
ed to ensure themselves they weren’t dreaming. They decided to use this technology, soon to be re-christened JavaScript (a story told often and elsewhere), for everything they didn’t know they needed: form input validation, image substitution, frameset manipulation, popup windows, and more. The sole requirement was a few interpreted commands enclosed in a <script>
tag. The possibilities were endless.
Soon, the introduction of the src
attribute allowed them to import a file full of JS into their pages. Little need to fiddle with the markup, when all the requisite JS for the page could be included in a single, or a few, external files, specified in the page’s <HEAD>
. It didn’t take our ancestors long before they decided that the same JS file(s) should be in all pages, throughout their website, containing JS for the complete site. No worries about bloat; after all, the browser would cache it.
A clear, sunny, road to dynamic, interactive sites lay ahead. What could go wrong?
Blockage
Those early JS adopters deduced that when the HTML parser encountered an external script, it suspended visual rendering of the page while it went off to retrieve and execute it. Simple. The more numerous and larger the scripts, the longer the wait for the page to paint. JavaScript, therefore, was very soon, most often unnecessarily, blocking page rendering.
The solutions poured in, both from the developer community and browser vendors:
Community: Move script location to end of HTML pageA classic duh! moment. Amazingly, this simple suggestion helped, unless the script was required to help build the page, eg. using
document.write
for markup.Vendor: Use
<script defer>
.It’s 1997, and IE4 introduces thedefer
attribute. Scripts that do not contribute to the initial rendering of the page should be marked withdefer
, and they will load in parallel, without blocking, and be executed in their markup order beforewindow.load
is fired (later, beforedocument.DOMContentLoaded
). Script tags could remain in the<head>
, and execute as if they were at the end of page.The main benefit to page rendering was the saving in script retrieval time.Community: Reduce latency by reducing actual script size.What began as script obfuscation for intellectual property and vanity reasons, quickly became script minification, still used widely.
Community: Reduce latency and http handshake instances through concatenation of all scripts, delivered as one.
Vendor: Use
<script async>
.In 2010, 13 years (yes, 13, thirteen) afterdefer
was born, HTML5 provideddefer
with a sibling,async
. Scripts can be loaded asynchronously, be non-blocking, and be executed when they load. Markup order is irrelevant to execution order. A clear benefit overdefer
was thatload/DOMContentLoaded
events were not delayed.Community: Lazy Loading.Use JS to load JS by dynamically creating non-blocking script tags.
Cloudflare: Rocket LoaderIt's 2011, and Cloudflare enters the fray, leveraging our network to reduce http requests for 1st party scripts, “bag”ging 3rd party scripts into a single file, and delaying and controlling JS execution.See Combining Javascript & CSS, a Better Way
Vendor: Use
<link rel="preload">
in the<head>
.Important resources like scripts, in our case, can be specified for preload. The browser will load scripts in parallel and not block render-parsing.
Rocket Loader, The Early Years
Much has been written in this blog space about Rocket Loader, from its initial launch, to the current one.
If reading outdated blog posts is not your thing, perhaps watching an extremely short video of a high-profile early Rocket Loader success (June 9, 2011) is:CloudFlare Rocket Loader makes the Financial Times website (FT.com) faster
Rocket Loader improved page load times by:
Minimising network requests through the bundling of JS files, including third-party, speeding up page rendering
Asynchronously loading the bundles, avoiding HTML parsing blockage
Caching scripts locally (using LocalStorage), reducing refetch requests.
As browsers matured, Rocket Loader fell behind, leading to several severe shortcomings:
It did not honour Content-Security-Policy.Rocket Loader was unaware of CSP headers, and loaded scripts indiscriminately.
It did not honour Subresource IntegrityRocket Loader loaded scripts through XHR, so browsers could not validate the fetched script.
It allowed for XSS PersistenceSince Rocket Loader stored scripts in LocalStorage, a site’s compromised script could exist as a trojan in a customer’s storage, loading whenever the customer visited the site.
It was just out-of-date
Script bundling fell out of favour with the introduction of http2.
The use of
eval()
was finally recognised as evil.Mobile use skyrocketed; mobile browsers became sophisticated; eventually Rocket Loader was unable to support mobile.
New and Improved Rocket Loader
We recently rebuilt Rocket Loader from the ground up.
Although our aim remains the same, to improve customer page performance, we incorporated lessons learned. Most importantly, we learned not to aim too high. In order to satisfy all permutations of page layout, the old Rocket Loader created a virtual DOM, a decision that ultimately led to fragility. We've gone the simple, elegant route, knowing full well that there will be a minority of websites that will not benefit.
The main concept behind Rocket Loader is quite straightforward: execute blocking scripts after all other page assets have loaded.
The scripts need to be loaded and executed in the originally intended order. Only external blocking scripts curtail page resources, but any script may rely on another one. We must simulate the loading process of scripts, mimicing how the browser would handle them during page load, but do it after the page is actually fully loaded.
On the Server
Rocket Loader has both a server-side and a client-side component. The goal of the former is to
rewrite
<script>
tags in the page markup to make them non-executable, andinsert the client-side component of Rocket Loader into the page.
The server-side component is built on top of our CF-HTML pipeline. CF-HTML is an nginx module that provides streaming HTML parsing and rewriting functionality with a SAX-style (Simple API for XML) API on top of it.
To make the scripts non-executable, we simply prepend their type
attribute value with a randomly generated value (nonce), unique for each page request. Having a unique prefix for each page prevents Rocket Loader from being used as an XSS gadget to bypass various XSS filters.
Markup that looked like this:
<!DOCTYPE html>
<html>
<head>
<script src="example.org/1.js"></script>
<script src="example.org/2.js" type="text/javascript"></script>
</head>
<body>
...body markup...
<script src="example.org/3.js" type="text/javascript"></script>
...more body markup...
</body>
</html>
becomes:
<!DOCTYPE html>
<html>
<head>
<script src="example.org/1.js" type="42deadbeef-"></script>
<script src="example.org/2.js" type="42deadbeef-text/javascript"></script>
</head>
<body>
...body markup...
<script src="example.org/3.js" type="42deadbeef-text/javascript"></script>
...more body markup...
<script src="https://ajax.cloudflare.com/rocket-loader.js"
data-cf-nonce="42deadbeef" defer>
</body>
</html>
So far, no rocket science, but by making most, or all, scripts non-executable, Rocket Loader has unblocked page-parsing. Browsers display content sooner, improving perceived page load metrics, and engaging the user.
On The Client
Generally, scripts can be divided into four categories, each having distinct load and execution behaviours when inserted into the DOM:
Inline scripts - executed immediately upon insertion.
External blocking scripts - start loading upon insertion, preventing other scripts from loading and executing.
External
defer
scripts - start loading upon insertion, without preventing other scripts from loading and executing. Execution should happen right beforeDOMContentLoaded
event.External
async
scripts - start loading upon insertion, without preventing other scripts from loading and executing. Executed when loaded.
Modified diagram from HTML Standard
To handle load and execution of all script types, Rocket Loader needs two passes.
Pass One
On the first pass, we collect all scripts with our nonce onto a stack, then re-insert them into the DOM, with nonce removed, and wrapped in a comment node. These serve as our placeholders.
<!DOCTYPE html>
<html>
<head>
<!--<script src="example.org/1.js"></script>-->
<!--<script src="example.org/2.js" type="text/javascript"></script>-->
</head>
<body>
...body markup...
<!--<script src="example.org/3.js" type="text/javascript"></script>-->
...more body markup...
<script src="https://ajax.cloudflare.com/rocket-loader.js"
data-cf-nonce="42deadbeef" defer>
</body>
</html>
Rocket Loader now iterates through the scripts in our stack and re-inserts them, maintaining their intended position in relevant DOM collections (document.scripts
, document.querySelectorAll("script")
, document.getElementsByTagName("script")
, etc.).
This process of script insertion and execution differs for each script category:
Inline scripts - Placeholder is replaced with the original script element, without nonce, making the script executable. Browsers execute such scripts immediately upon insertion, in the same execution tick.
External blocking scripts - As above, but Rocket Loader waits for the script’s load
event before unwinding the script stack further. This delay simulates the script's blocking behaviour manually. Only parser-inserted external scripts (i.e. scripts present in the original HTML markup) are naturally blocking. External scripts inserted or created via a DOM API are considered async. This behaviour can’t be overridden, so we need our simulation.
External async
scripts - The same insertion procedure as inline scripts. Browsers treat all inserted external scripts as async, so the default behaviour suits us.
External defer
scripts - These are not executed during the first pass, since in the simulated environment we haven’t reached the DOMContentLoaded
event yet. If we encounter a defer
script on the stack we re-insert it, as is, without removing the nonce prefix. It remains non-executable, but in the correct DOM position.
Pass Two
The second pass loads the defer
scripts. Again, Rocket Loader collects all scripts with the nonce prefix (these are now just defer
scripts) onto the execution stack, but does not replace them with placeholders. They remain in the DOM, since at this point in our simulated environment the complete document has loaded. We then activate them by replacing the <script>
elements with themselves, nonce removed, and let the browser do the rest.
Quirks I: Taming the Waterfall
Ostensibly, we have now simulated browser script loading and execution behaviours. However, there are some one-off issues we must deal with, quirks if you will.
There is one not-so-obvious difference between our algorithm and the real behaviour of browsers. Modern browsers try to be clever with the way they manage page resources, engaging various heuristics to improve performance during page load. These are, generally, implementation-specific and not set-in-stone by any specification.
One such optimisation that affects us, is speculative parsing. Despite the official specification requiring a browser to block parsing on script execution, browsers continue parsing received HTML markup speculatively, and prefetch found external resources. For example, even with blocking scripts on a page, Chrome loads them simultaneously, in parallel.
With Rocket Loader, browsers don’t prefetch scripts, as our nonce makes them non-executable during page load. Later, when we sequentially re-insert activated scripts, we witness a sequential “waterfall” graph.
In our attempt to improve page load performance, we significantly slowed down some script loading. Ironic. Fortunately, we have a workaround: we can insert preload hints (see Preloading content with rel="preload") before we begin unwinding our script stack, giving the browser notice that we’ll soon be requiring these scripts. It begins fetching them as it would do during speculative parsing.
Our waterfall is replaced with improved parallel loading and better load metrics.
Quirks II: document.write()
is not dead yet
We've simulated script execution and insertion. We still need to deal with dynamic markup insertion. We can’t use document.write()
directly since the document is already parsed and document.close()
has been implicitely executed. Calling write()
will create a new document, erasing the entire current document. We must manually parse content created by the document.write
function and insert it in the intended location.
Not so simple, if one considers that document.write
can insert partial markup. In the following example, if we parse and insert content on the first document.write
call, we’ll completely ignore the completion of the id
attribute that should be inserted with the second call:
document.write('<div id="elm');
document.write(Date.now());
document.write('">some content</div>');
So, we have a hard choice:
We can buffer all content inserted via
document.write
during script execution and flush it afterwards, in which case already executed code expecting elements to be in the DOM will fail, orWe can flush inserted markup immediately, but not handle partial markup writes.
Choosing the lesser of two evils, we decided to go with the first option: our observations showed cases like these are more common.
(Actually, there is a third option that allows for handling of both cases, but it requires proxying of a significant number of DOM APIs, a rabbit hole that we don’t want to dive into, KISS FTW, you know…).
Quirks III: I ain't got no<body>
As mentioned, it’s not enough to just insert parsed markup. There are various modifications of the DOM performed by the parser during full document parsing that contend with malformed markup. We felt we should simulate at least some of them, because, well… scripts may rely on malformed markup.
Our initial implementation even included simulation of relatively exotic mechanisms such as foster parenting, but eventually we decided to keep things simple and the only thing that Rocket Loader simulates is the squeezing out of unallowed content from the <head>
element.
To perform this simulation we wrap our document.write
buffer in a <head>
element and feed this markup to the DOM Parser.
Using the resulting document from the parser, we identify all nodes in its <head>
and move them into the page, immediately following the script that performed the document.write
. If we encounter any nodes in the parsed document's <body>
element, we copy all nodes that follow the current script to the <body>
element, prepended with the nodes in the parsed document.
To illustrate this simulation, consider the following page markup:
<!DOCTYPE>
<head>
<script>
document.write(‘<link rel=”stylesheet” href=”1.css”>’);
document.write(‘<div></div>’);
document.write(‘<link rel=”stylesheet” href=”2.css”>’);
</script>
<link rel=”stylesheet” href=”3.css”>
</head>
<body>
<div>Hey!</div>
</body>
The buffered, dynamically inserted, markup after script execution will be
<link rel=”stylesheet” href=”1.css”>
<div></div>
<link rel=”stylesheet” href=”2.css”>
and the string that we’ll feed to the DOMParser will be
<!DOCTYPE>
<head>
<link rel=”stylesheet” href=”1.css”>
<div></div>
<link rel=”stylesheet” href=”2.css”>
</head>
The parser will produce the following document structure from the provided markup (note that <div>
is not allowed in <head>
and was squeezed out to the <body>
):
<!DOCTYPE>
<html>
<head>
<link rel=”stylesheet” href=”1.css”>
</head>
<body>
<div></div>
<link rel=”stylesheet” href=”2.css”>
</body>
</html>
Now we move all nodes that we found in parsed document's <head>
to the original document:
<!DOCTYPE>
<head>
<script>
document.write(‘<link rel=”stylesheet” href=”1.css”>’);
document.write(‘<div></div>’);
document.write(‘<link rel=”stylesheet” href=”2.css”>’);
</script>
<link rel=”stylesheet” href=”1.css”>
<link rel=”stylesheet” href=”3.css”>
</head>
<body>
<div>Hey!</div>
</body>
We see that parsed document's <body>
contains some nodes, so we prepend them to the original document’s <body>
:
<!DOCTYPE>
<head>
<script>
document.write(‘<link rel=”stylesheet” href=”1.css”>’);
document.write(‘<div></div>’);
document.write(‘<link rel=”stylesheet” href=”2.css”>’);
</script>
<link rel=”stylesheet” href=”1.css”>
<link rel=”stylesheet” href=”3.css”>
</head>
<body>
<div></div>
<link rel=”stylesheet” href=”2.css”>
<div>Hey!</div>
</body>
And as a final step, we move all nodes in the <head>
, that initially followed the current script, to after the nodes that we’ve just inserted in the <body>
:
<!DOCTYPE>
<head>
<script>
document.write(‘<link rel=”stylesheet” href=”1.css”>’);
document.write(‘<div></div>’);
document.write(‘<link rel=”stylesheet” href=”2.css”>’);
</script>
<link rel=”stylesheet” href=”1.css”>
</head>
<body>
<div></div>
<link rel=”stylesheet” href=”2.css”>
<link rel=”stylesheet” href=”3.css”>
<div>Hey!</div>
</body>
Quirks IV: Handling handlers
There is one edge case which drastically changes the behaviour of our script-loading simulation. If we encounter elements with inline event handlers in the HTML markup, we need to execute all scripts that precede such elements since the handlers may rely on them.
We insert the Rocket Loader client side script in special "bailout" mode immediately before such elements. In bailout mode, we activate scripts the same way as in regular mode, except we do it in a blocking manner (remember, we need to prevent element from being parsed while we activate all preceding scripts).
As noted, it’s impossible to dynamically create blocking external scripts using DOM APIs such as document.appendChild
. However, we have a solution to overcome this limitation.
Since the page is still loading, we can document.write
the outerHTML
of activatable script into the document, forcing the browser to mark it as parser-inserted and, thus, blocking. However, the script will be inserted in a DOM position different from its original, intended, position, which may break traversing of surrounding nodes from within the script (e.g. using document.currentScript
as a starting point).
There is a trick. We leverage browser behaviour which parses generated content in the same execution tick as the document.write
that produced it. We have immediate access to the written element. The execution of the external script is always scheduled for one of the next execution ticks. So, we can just move script to its original position right after we write it and have it in the correct DOM position, awaiting its execution.
"I can resist everything except temptation"[1]
The need to account for every quirk, every variation in browser parsing, is strong, but implementation would eventually only weaken our product. We've dealt with the best part of browser parser behaviours, enough to benefit the majority of our customers.
What's Next?
As Rocket Loader matures, and inevitably is affected by changes in Web technologies, it may be expanded and improved. For now, we're monitoring its use, identifying issues, and ensuring that it's worthy of its predecessor, which lasted through so many advances and changes in Web technology.
Oscar Wilde, Lady Windermere's Fan (1892), and apologies to Jethro Tull for the blog post title. ↩︎