Experiment: Improving Page Load Times with Script Streaming
What is script streaming?
Loading JavaScript is one of the most critical bottlenecks to web performance, especially on mobile devices with a slower CPU. The cost of loading JavaScript not only includes the network time to download the bytes from a content server, but also includes the time to decompress, parse, compile, and execute the JavaScript. As web pages continue to grow with large amounts of JavaScript code for generating user experiences, pages have become slower to load.
The web browser community has been developing numerous optimizations to speed up JavaScript loading by parsing JavaScript files in parallel to their download (previously, browsers waited for JavaScript files to completely download before parsing on the renderer's main thread). This optimization is called script streaming and was introduced in Chrome v41. With script streaming, Google claims to observe pages load 10% faster because large JavaScript files are parsed as they download - speeding up the page load by hundreds of milliseconds.
Script streaming is a great optimization to speed up JavaScript parsing. However, there are only a few conditions under which Chrome implementations (as of July 2018) enable script streaming for JavaScript files.
First, the JavaScript file being downloaded has to be at least 30 KiB in size. The size limit ensures that only large scripts are parsed via the script streaming because parsing large JavaScript files in parallel to download will benefit the most, compared to smaller JavaScript files that would take less than a few milliseconds to parse even if they were parsed after being downloaded completely.
Second, current implementations of script streaming in Chrome only allow for one JavaScript file to be parsed via script streaming at a time. This is because (as of July 2018) Chrome uses only one thread for script streaming. Note that if the thread is busy parsing some JavaScript file, other JavaScript files must be parsed on the renderer’s main thread after they are downloaded completely.
How can web developers take advantage of script streaming?
Script streaming already provides benefits in terms of faster JavaScript parsing. As a website developer, you don’t need to do anything to enable script streaming on your pages. However, there are a few limitations with script streaming, and mind you, these limitations only exist because of how script streaming is currently implemented in Chrome.
Particularly, with script streaming there is no guarantee that when large JavaScript files are being downloaded that the script streaming thread is available. As such, for web pages that download multiple large JavaScript files in parallel, only one of those large files could be parsed via script streaming. And so, in some cases, smaller scripts may parse via script streaming and larger scripts either wait for the streaming thread to become available or to be downloaded on the renderer’s main thread after they are completely downloaded.
As a developer, you could use DevTools performance timeline to investigate whether or not script streaming is used to parse large JavaScript files because that would give more performance boost to your pages. Below is a screenshot of the performance timeline captured for www.akamai.com, where the scripts parsed via the ScriptStreamer thread are contained in a red box.
Two approaches to improve parsing
I did some research to investigate ways to force script streaming to bypass parsing of smaller JavaScript files (still > 30 KiB) in favor of keeping the thread available for parsing relatively larger JavaScript files. One theoretical way to achieve script streaming on large JavaScript files would be to send an HTTP response header on large JavaScript files that instructs the browser to apply script streaming to files that have the header present. However, this approach will require major browser changes and the use of multiple threads for script streaming instead of using just one thread. Note that the use of multiple script streaming threads is still in “things-to-do” for the Chrome team.
A practical technique, which was much easier to play with and needed no browser changes, was to reorder the position of <script> tags with static src URLs in the HTML such that large JavaScript files are downloaded before smaller (still > 30 KiB) script files are downloaded. This approach forces the parsing of large JavaScript files to happen via script streaming while it's available. Note that reordering <script> tags in the HTML could be potentially dangerous because some javaScript execution must happen serially to maintain the page’s functionality and UI.
Therefore, reordering of <script> tags in the HTML must be done with caution. It’s safer to reorder <script> tags with async or defer attributes since script execution order does not matter when these attributes are present. Also note that because of such restrictions around script reordering, this experimental approach would only work on select websites. In addition to reordering scripts, another approach I incorporated was to address cases where multiple large JavaScript files download in parallel and race for the script streaming thread. I concatenated all such JavaScript files with the goal of allowing all of them to parse in as they download.
Experimental results
I did some in-lab experimentation to measure the impact of the above two approaches on page load time on various devices, such as a Macbook Pro laptop, a low-end mobile device (Motorola Moto E), and a high-end mobile device (Motorola Moto G). The performance data is collected via a private WebPageTest instance. On a set of two manually modified websites (“Page A” and “Page B,” details shown in the table below), improvements of up to 6.2% in the median page load time were observed on various mobile devices and a Macbook Pro. These improvements were a result of the JavaScript parse time saved by being parsed in parallel to their download.
Page | #resources | Page size | #JavaScript resources | Total JavaScript bytes |
---|---|---|---|---|
A | 41 | 2.7 MB | 12 | 1.3 MB |
B | 95 | 2.2 MB | 19 | 1.1 MB |
Performance on Macbook Pro
Figures 1 and 2 show the CDF distribution of page load times observed for the two test pages on a Macbook Pro. For page A, loading the page with reordered script tags in the HTML leads to a reduction of 6.2% in the median page load time. For page B, loading the page with reordered script tags reduced the median page load time by 4.5%.
Performance on Motorola Moto E
As shown in the figure, loading page A with a reordered script tag reduced the median page load time by 4.3%.
Loading page B (no graph) did not yield faster load times — perhaps the script streaming thread was occupied when the mobile version of page A was loaded on the Moto E device.
Performance on Motorola Moto G
As shown in the figure, loading page A with reordered script tags reduced the median page load time by 3.5%.
For page B (no graph), the median page load time was reduced by 1.9%.
Summary
The experimental work described in this blog post illustrates the benefits of parsing relatively larger JavaScript files via script streaming, compared to the ones parsed by default. The experiment involves reordering <script> tags in the HTML and/or concatenation of multiple large JavaScript files that download in parallel in a way that allows relatively larger JavaScript files to be parsed via script streaming as they download. In-lab experiments show improvements of up to 6% in the median page load time on both Macbook Pro and two low-end and high-end mobile devices.