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.
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.
Early humans looked upon Netscape 2.0, with its new ability to script HTML using LiveScript, and
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
. 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?
The solutions poured in, both from the developer community and browser vendors:
Community: Move script location to end of HTML page
A classic duh! moment. Amazingly, this simple suggestion helped, unless the script was required to help build the page, eg. using
It’s 1997, and IE4 introduces the
deferattribute. Scripts that do not contribute to the initial rendering of the page should be marked with
defer, and they will load in parallel, without blocking, and be executed in their markup order before
window.loadis fired (later, before
document.DOMContentLoaded). Script tags could remain in the
, 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.
In 2010, 13 years (yes, 13, thirteen) after
deferwas born, HTML5 provided
deferwith 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 over
load/DOMContentLoadedevents were not delayed.
Community: Lazy Loading.
Use JS to load JS by dynamically creating non-blocking script tags.
Cloudflare: Rocket Loader
It'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.
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
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 Integrity
Rocket Loader loaded scripts through XHR, so browsers could not validate the fetched script.
It allowed for XSS Persistence
Since 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
tags in the page markup to make them non-executable, and
- insert 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:
...body markup... ...more body markup...
...body markup... ...more body markup... --> ...body markup... ...more body markup...Hey!
The buffered, dynamically inserted, markup after script execution will be
and the string that we’ll feed to the DOMParser will be
The parser will produce the following document structure from the provided markup (note that
and was squeezed out to the
Now we move all nodes that we found in parsed document's
to the original document:
We see that parsed document's
contains some nodes, so we prepend them to the original document’s
And as a final step, we move all nodes in the
, that initially followed the current script, to after the nodes that we’ve just inserted in the
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
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"
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.
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.