The Cost Of JavaScript In 2018

JavaScript processing times for CNN.com as measured by WebPageTest (src). A high-end phone (iPhone 8) processes script in ~4s. Compare to the ~13s an average phone (Moto G4) takes or the ~36s taken by a low-end 2018 phone (Alcatel 1X).

tl;dr:

  • To stay fast, only load JavaScript needed for the current page. Prioritize what a user will need and lazy-load the rest with code-splitting. This gives you the best chance at loading and getting interactive fast. Stacks with route-based code-splitting by default are game-changers.
  • Embrace performance budgets and learn to live within them. For mobile, aim for a JS budget of < 170KB minified/compressed. Uncompressed this is still ~0.7MB of code. Budgets are critical to success, however, they can’t magically fix perf in isolation. Team culture, structure and enforcement matter. Building without a budget invites performance regressions and failure.
  • Learn how to audit and trim your JavaScript bundles. There’s a high chance you’re shipping full-libraries when you only need a fraction, polyfills for browsers that don’t need them, or duplicate code.
  • Every interaction is the start of a new ‘Time-to-Interactive’; consider optimizations in this context. Transmission size is critical for low-end mobile networks and JavaScript parse time for CPU-bound devices.
  • If client-side JavaScript isn’t benefiting the user-experience, ask yourself if it’s really necessary. Maybe server-side-rendered HTML would actually be faster. Consider limiting the use of client-side frameworks to pages that absolutely require them. Server-rendering and client-rendering are a disaster if done poorly.
Video for my recent talk on “The Cost of JavaScript” on which this write-up is based.

The web is bloated by user “experience”

A mad rush of files being thrown at you.
Statistics from the HTTP Archive state of JavaScript report, July 2018 highlight the median webpage ships ~350KB of minified and compressed script. These pages take up to 15s to get interactive.
Countries that perform better in a particular metric are shaded darker. Countries not included are in grey. It’s also worth noting that rural broadband speeds, even in the US, can be 20% slower than urban areas.
Uncompressed JS bundle size numbers from “Bringing Facebook.com and the web up to speed”. Sites like Google Sheets are highlighted as shipping up to 5.8MB of script (when decompressed).

JavaScript has a cost

“Sites with this much script are simply inaccessible to a broad swath of the world’s users; statistically, users do not (and will not) wait for these experiences to load” — Alex Russell
  • A client-side framework or UI library
  • A state management solution (e.g. Redux)
  • Polyfills (often for modern browsers that don’t need them)
  • Full libraries vs. only what they use (e.g. all of lodash, Moment + locales)
  • A suite of UI components (buttons, headers, sidebars etc.)
Loading is a journey. We’re shifting to increasing caring about user-centric happiness metrics. Rather than just looking at onload or domContentLoaded, we now ask “when can a user actually *use* the page?”. If they tap on a piece of user-interface, does it respond right away?
A visualization of Time-to-Interactive highlighting how a poorly loaded experience makes the user think they can accomplish a goal, when in fact, the page hasn’t finished loading all of the code necessary for this to be true. With thanks to Kevin Schaaf for the interactivity animation
Lighthouse measures a range of user-centric performance metrics, like Time-to-Interactive, in a lab setting.
Here’s an example where a user may tap around some UI. Ordinarily, they might check a checkbox or click a link and everything’s going to work perfectly fine. But if we simulate blocking the main thread, nothing’s able to happen. They are not able to check that checkbox or click links because the main thread is blocked.
JavaScript can delay interactivity for visible elements. Visualized are a number of UI elements from Google Search.
Time-to-Interactive of news.google.com as measured by WebPageTest and Lighthouse (source)

Why is JavaScript so expensive?

JavaScript parse/compile = 10–30% of the time spent in V8 (Chrome’s JS engine) during page load
Not all bytes weigh the same. A 200KB script has a very different set of costs to a 200KB JPG outside of the raw network transmission times for both sets of bytes.

Mobile is a spectrum.

Mobile is a spectrum composed of cheap/low-end, median and high-end devices.
“Insights into the 2.3 Billion Android Smartphones” by newzoo. Android has a 75.9% share of the global market with a predicted 300m more smartphones joining the market in 2018. Many of these will be budget Android devices.
Processing (parse/compile) times for 1MB of uncompressed JavaScript (<200KB minified and gzipped) manually profiled on real devices. (src)
JavaScript processing times for CNN.com via WebPageTest (src)
Comparison of loading CNN.com, a JavaScript-heavy site, over 3G on mid-lower end hardware (source). The Alcatel 1X takes 65s to fully load.
Test on real phones & networks.
“Knowing your audience and then appropriately focusing the performance of your application is critical” — Brian Holt (src)
Google Analytics > Audience > Mobile > Devices visualizes what devices & operating systems visit your site.

How to send less JavaScript

Splitting large, monolithic JavaScript bundles can be done on a page, route or component basis. You’re even better setup for success if “splitting” is the default for your toolchain from the get-go.
Adding code-splitting in a React app using React Loadable — a higher-order component that wraps dynamic imports in a React-friendly API for adding code-splitting to your app at a given component.
Audit your JavaScript bundles regularly. Tools like webpack-bundle-analyzer are great for analyzing your built JavaScript bundles and import-cost for Visual Code is excellent for visualizing expensive dependencies during your local iteration workflow (e.g. when you `npm install` and import a package)
From “Put your Webpack bundle on a diet” by Benedikt Rötsch

Measure, Optimize, Monitor, and Repeat.

Lighthouse recently added a number of useful new performance audits that you may not be aware of.
Find unused CSS and JS code with the Coverage tab in Chrome DevTools.
PRPL is a performance pattern for efficient loading. It stands for (P)ush critical resources for the initial route, (R)ender initial route, (P)re-cache remaining routes, (L)azy-load and create remaining routes on demand
  • Milestone timings — timings based on the user-experience loading a page (e.g Time-to-Interactive). You’ll often want to pair several milestone timings to accurately represent the complete story during page load.
  • Quality-based metrics — based on raw values (e.g. weight of JavaScript, number of HTTP requests). These are focused on the browser experience.
  • Rule-based metrics — scores generated by tools such as Lighthouse or WebPageTest. Often, a single number or series to grade your site.
  • “Leadership buy-in is important. The willingness to put feature work on hold to keep the overall user experience good defines thoughtful management of technology products.”
  • “Performance is about culture supported by tools. Browsers optimize HTML+CSS as much as possible. Moving more of your work into JS puts the burden on your team and their tools”
  • “Budgets aren’t there to make you sad. They’re there to make the organization self-correct. Teams need budgets to constrain decision space and help hitting them”
Make performance part of the conversation.
“Make performance relevant to your stakeholders’ goals by showing how it can impact the key metrics they care about. Without a performance culture, performance is not sustainable” — Allison McKnight
Prevent pull requests from being merged if your perf scores fall below a certain value with Lighthouse CI. Lighthouse Thresholds is another configuration-based approach to setting perf budgets.
JavaScript perf budgets for my site teejungle.net using SpeedCurve which supports a range of budget metrics.
Field data (or RUM — Real User Monitoring) is performance data collected from real page loads your users are experiencing in the wild. Sites with heavy JavaScript payloads would benefit from measuring main-thread of this work through Long Tasks and First Input Delay.
Marcel Freinbichler had a viral tweet about USA Today shipping a slim version of their site to EU users. It loaded 42 seconds faster than their normal pages.

Get fast, stay fast.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store