JavaScript Start-up Performance
By: Addy Osmani
Tags: JavaScript, Compile, Performance, Parse, Web Development

Chrome Tracing

about:tracing — Chrome’s lower-level Tracing tool allows us to use the `disabled-by-default-v8.runtime_stats` category to get deeper insights into where V8 spends its time. V8 have a step-by-step guide on how to use this that was published just the other day.

WebPageTest

WebPageTest’s “Processing Breakdown” page includes insights into V8 Compile, EvaluateScript and FunctionCall time when we do a trace with the Chrome > Capture Dev Tools Timeline enabled.

We can now also get out the Runtime Call Stats by specifying `disabled-by-default-v8.runtime_stats` as a custom Trace category (Pat Meenan of WPT now does this by default!).

For a guide on how to get the most out of this, see this gist I wrote up.

User Timing

It’s possible to measure Parse times through the User Timing API as Nolan Lawson points out below:

The third <script> here isn’t important, but it’s the first <script> being separate from the second (performance.mark() starting before the <script> has been reached) that is.

This approach can be affected on subsequent reloads by V8’s preparser. This could be worked around by appending a random string to the end of the script, something Nolan does in his optimize-js benchmarks.

I use a similar approach for measuring the impact of JavaScript Parse times using Google Analytics:

A custom Google Analytics dimension for ‘parse’ allows me to measure JavaScript parse times from real users and devices hitting my pages in the wild.

DeviceTiming

Etsy’s DeviceTiming tool can help measure parse & execution times for scripts in a controlled environment. It works by wrapping local scripts with instrumentation code so that each time our pages are hit from different devices (e.g laptops, phones, tablets) we can locally compare parse/exec. Daniel Espeset’s Benchmarking JS Parsing and Execution on Mobile Devices goes into more detail on this tool.

What can we do to lower our JavaScript parse times today?

Ship less JavaScript . The less script that requires parsing, the lower our overall time spent in the parse & compile phases will be.

. The less script that requires parsing, the lower our overall time spent in the parse & compile phases will be. Use code-splitting to only ship the code a user needs for a route and lazy load the rest . This probably is going to help the most to avoid parsing too much JS. Patterns like PRPL encourage this type of route-based chunking, now used by Flipkart, Housing.com and Twitter.

. This probably is going to help the most to avoid parsing too much JS. Patterns like PRPL encourage this type of route-based chunking, now used by Flipkart, Housing.com and Twitter. Script streaming: In the past, V8 have told developers to use `async/defer` to opt into script streaming for parse-time improvements of between 10–20%. This allows the HTML parser to at least detect the resource early, push the work to the script streaming thread and not halt the document parsing. Now that this is done for parser-blocking scripts too, I don’t think there’s anything actionable we need to do here. V8 recommend loading larger bundles earlier on as there’s only one streamer thread (more on this later)

In the past, V8 have told developers to use `async/defer` to opt into script streaming for parse-time improvements of between 10–20%. This allows the HTML parser to at least detect the resource early, push the work to the script streaming thread and not halt the document parsing. Now that this is done for parser-blocking scripts too, I don’t think there’s anything actionable we need to do here. V8 recommend (more on this later) Measure the parse cost of our dependencies, such as libraries and frameworks. Where possible, switch them out for dependencies with faster parse times (e.g switch React for Preact or Inferno, which require fewer bytes to bootup and have smaller parse/compile times). Paul Lewis covered framework bootup costs in a recent article. As Sebastian Markbage has also noted, a good way to measure start-up costs for frameworks is to first render a view, delete and then render again as this can tell you how it scales. The first render tends to warm up a bunch of lazily compiled code, which a larger tree can benefit from when it scales.

If our JavaScript framework of choice supports an ahead-of-time compilation mode (AoT), this can also help heavily reduce the time spent in parse/compile. Angular apps benefit from this for example:

Nolan Lawson’s ‘Solving the Web Performance Crisis’

What are browsers doing to improve Parse & Compile times today?

Developers are not the only ones to still be catching up on real-world start-up times being an area for improvement. V8 discovered that Octane, one of our more historical benchmarks, was a poor proxy for real-world performance on the 25 popular sites we usually test. Octane can be a poor proxy for 1) JavaScript frameworks (typically code that isn’t mono/polymorphic) and 2) real-page app startup (where most code is cold). These two use-cases are pretty important for the web. That said, Octane isn’t unreasonable for all kinds of workloads.

The V8 team has been hard at work improving start-up time and we’ve already seem some wins here:

We also estimate a 25% improve on V8 parse times for many pages looking at our Octane-Codeload numbers:

And we’re seeing wins in this area for Pinterest too. There are a number of other explorations V8 has started over the last few years to improve Parsing and Compile times.

Code caching

Chrome 42 introduced code caching — a way to store a local copy of compiled code so that when users returned to the page, steps like script fetching, parsing and compilation could all be skipped. At the time we noted that this change allowed Chrome to avoid about 40% of compilation time on future visits, but I want to provide a little more insight into this feature:

Code caching triggers for scripts that are executed twice in 72 hours .

. For scripts of Service Worker: Code caching triggers for scripts that are executed twice in 72 hours.

For scripts stored in Cache Storage via Service Worker: Code caching triggers for scripts in the first execution.

So, yes. If our code is subject to caching V8 will skip parsing and compiling on the third load.

We can play around with these in chrome://flags/#v8-cache-strategies-for-cache-storage to look at the difference. We can also run Chrome with — js-flags=profile-deserialization to see if items are being loaded from the code cache (these are presented as deserialization events in the log).

One caveat with code caching is that it only caches what’s being eagerly compiled. This is generally only the top-level code that’s run once to setup global values. Function definitions are usually lazily compiled and aren’t always cached. IIFEs (for users of optimize-js ;)) are also included in the V8 code cache as they are also eagerly compiled.

Script Streaming

Script streaming allows async or defer scripts to be parsed on a separate background thread once downloading begins and improves page loading times by up to 10%. As noted earlier, this now also works for sync scripts.