Blog

Experiment: Script-Streaming

Experiment: Improving Page Load Times with Script-Streaming

July 17, 2018 · by Utkarsh Goel

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 main thread). This optimization is called script-streaming and was introduced in Chrome v41. With script-streaming, Google claims to observe pages to load 10% faster because large JavaScript files are parsed as they download, which speeds 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 JavaScripts 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 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.

Experiment 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#resourcesPage Size#JavaScript ResourcesTotal JavaScript Bytes
A412.7 MB121.3 MB
B952.2 MB191.1 MB

Performance on MacBook Pro

Figures 1 and 2 below 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 lead 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 below, loading page A with reordered script tag reduced the median page load time by 4.3%.

Loading page B (no graph shown) did not yield faster load times—perhaps the script-streaming thread was occupied when the mobile version of page A was loaded on Moto E device.

Performance on Motorola Moto G

As shown in the figure below, loading page A with reordered script tags reduced the median page load time by 3.5%.

For page B (no graph shown), the median page load time reduced by 1.9%.

Summary

The experimental work described in this article 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.

Utkarsh Goel is an architect in Akamai’s Web Performance business unit who likes to build technologies to improve the current state of web performance. He is also a member of Akamai’s Foundry, the cutting-edge arm for applied R&D that believes in the “fail fast, succeed faster” philosophy and focuses on exploring new technology opportunities to improve all forms of Internet performance.