Front-End Performance 2021: Build Optimizations

About The Author

Vitaly Friedman loves beautiful content and doesn’t like to give in easily. When he is not writing, he’s most probably running front-end & UX … More about Vitaly ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 176.000 folks.

Quick summary ↬ Let’s make 2021… fast! An annual front-end performance checklist with everything you need to know to create fast experiences on the web today, from metrics to tooling and front-end techniques. Updated since 2016.

Table Of Contents

  1. Getting Ready: Planning And Metrics
  2. Setting Realistic Goals
  3. Defining The Environment
  4. Assets Optimizations
  5. Build Optimizations
  6. Delivery Optimizations
  7. Networking, HTTP/2, HTTP/3
  8. Testing And Monitoring
  9. Quick Wins
  10. Everything on one page
  11. Download The Checklist (PDF, Apple Pages, MS Word)
  12. Subscribe to our email newsletter to not miss the next guides.

Build Optimizations

  1. Have we defined our priorities?
    It’s a good idea to know what you are dealing with first. Run an inventory of all of your assets (JavaScript, images, fonts, third-party scripts and "expensive" modules on the page, such as carousels, complex infographics and multimedia content), and break them down in groups.

    Set up a spreadsheet. Define the basic core experience for legacy browsers (i.e. fully accessible core content), the enhanced experience for capable browsers (i.e. an enriched, full experience) and the extras (assets that aren’t absolutely required and can be lazy-loaded, such as web fonts, unnecessary styles, carousel scripts, video players, social media widgets, large images). Years ago, we published an article on "Improving slots empire bonus codes  Magazine’s Performance," which describes this approach in detail.

    When optimizing for performance we need to reflect our priorities. Load the core experience immediately, then enhancements, and then the extras.

  2. Do you use native JavaScript modules in production?
    Remember the good ol' cutting-the-mustard technique to send the core experience to legacy browsers and an enhanced experience to modern browsers? An updated variant of the technique could use ES2017+ <script type="module">, also known as module/nomodule pattern (also introduced by Jeremy Wagner as differential serving).

    The idea is to compile and serve two separate JavaScript bundles: the “regular” build, the one with Babel-transforms and polyfills and serve them only to legacy browsers that actually need them, and another bundle (same functionality) that has no transforms or polyfills.

    As a result, we help reduce blocking of the main thread by reducing the amount of scripts the browser needs to process. Jeremy Wagner has published a comprehensive article on differential serving and how to set it up in your build pipeline, from setting up Babel, to what tweaks you’ll need to make in Webpack, as well as the benefits of doing all this work.

    Native JavaScript module scripts are deferred by default, so while HTML parsing is happening, the browser will download the main module.

    An example showing how native JavaScript modules are deferred by default
    Native JavaScript modules are deferred by default. Pretty much in general). Since Device Memory also has a JavaScript API which is how you can use Code Coverage from Devtools to achieve it.

    When dealing with single-page applications, we need some time to initialize the app before we can render the page. Your setting will require your custom solution, but you could watch out for modules and techniques to speed up the initial rendering time. For example, . In general, most performance issues come from the initial time to bootstrap the app.

    So, what’s the best way to code-split aggressively, but not too aggressively? According to Phil Walton, "in addition to code-splitting via dynamic imports, [we could] also use code-splitting at the package level, where each imported node modules get put into a chunk based on its package’s name." Phil provides a tutorial on how to build it as well.

  3. Can we improve Webpack's output?
    As Webpack is often considered to be mysterious, there are plenty of Webpack plugins that may come in handy to further reduce Webpack's output. Below are some of the more obscure ones that might need a bit more attention.

    One of the interesting ones comes from Ivan Akulov's thread. Imagine that you have a function that you call once, store its result in a variable, and then don’t use that variable. Tree-shaking will remove the variable, but not the function, because it might be used otherwise. However, if the function isn't used anywhere, you might want to remove it. To do so, prepend the function call with /*#__PURE__*/ which is supported by Uglify and Terser — done!

    A screenshot of JS code in an editor showing how the PURE function can be used
    To remove such a function when its result is not used, prepend the function call with /*#__PURE__*/. Via . This would move webpack’s runtime into a separate chunk — and would also improve caching.
  4. google-fonts-webpack-plugin downloads font files, so you can serve them from your server.
  5. workbox-webpack-plugin allows you to generate a service worker with a precaching setup for all of your webpack assets. Also, check Service Worker Packages, a comprehensive guide of modules that could be applied right away. Or use preload-webpack-plugin to generate preload/prefetch for all JavaScript chunks.
  6. speed-measure-webpack-plugin measures your webpack build speed, providing insights into which steps of the build process are most time-consuming.
  7. duplicate-package-checker-webpack-plugin warns when your bundle contains multiple versions of the same package.
  8. Use scope isolation and shorten CSS class names dynamically at the compilation time.

A screenshot of a terminal showing how the webpack loader named responsive-loader can be used to help you generate responsive images out of the box
Speed up your images is to serve smaller pictures on smaller screens. With .
  • by Shubhie Panicker and Jason Miller provide a detailed insight into how to use web workers, and when to avoid them.
  • highlights useful patterns for working with Web Workers, effective ways to communicate between workers, handle complex data processing off the main thread, and test and debug them.
  • Workerize allows you to move a module into a Web Worker, automatically reflecting exported functions as asynchronous proxies.
  • If you’re using Webpack, you could use workerize-loader. Alternatively, you could use worker-plugin as well.
  • Code in DOM shown on the left as an example of what to use and avoid when using web workers
    Use web workers when code blocks for a long time, but avoid them when you rely on the DOM, handle input response and need minimal delay. (via , and it has recently become viable as , and he’s debunking some myths about WebAssembly, explores its challenges and we can use it practically in applications today.
  • Google Codelabs provides an at his Google I/O talk. Also, Benedek Gagyi , specifically how the team uses it as output format for their C++ codebase to iOS, Android and the website.
  • Still not sure about when to use Web Workers, Web Assembly, streams, or perhaps WebGL JavaScript API to access the GPU? Accelerating JavaScript is a short but helpful guide that explains when to use what, and why — also with a handy flowchart and plenty of useful resources.

    An illustration of C++, C or Rust shown on the left with an arrow showing to a browser that includes WASM binaries adding to the JavaScript, CSS and HTML
    Milica Mihajlija provides a general overview of , so use use script type="module" to let browsers with ES module support load the file, while older browsers could load legacy builds with script nomodule.

    These days we can write module-based JavaScript that runs natively in the browser, without transpilers or bundlers. <link rel="modulepreload"> header provides a way to initiate early (and high-priority) loading of module scripts. Basically, it’s a nifty way to help in maximizing bandwidth usage, by telling the browser about what it needs to fetch so that it’s not stuck with anything to do during those long roundtrips. Also, Jake Archibald has published a detailed article with gotchas and things to keep in mind with ES Modules that’s worth reading.

    Inline scripts are deferred until blocking external scripts and inline scripts are executed
    Jake Archibald has published a detailed article with gotchas and things to keep in mind with ES Modules, e.g. inline scripts are deferred until blocking external scripts and inline scripts are executed. (Large preview)
    1. Identify and rewrite legacy code with incremental decoupling.
      Long-living projects have a tendency to gather dust and dated code. Revisit your dependencies and assess how much time would be required to refactor or rewrite legacy code that has been causing trouble lately. Of course, it’s always a big undertaking, but once you know the impact of the legacy code, you could start with incremental decoupling.

      First, set up metrics that tracks if the ratio of legacy code calls is staying constant or going down, not up. Publicly discourage the team from using the library and make sure that your CI alerts developers if it’s used in pull requests. polyfills could help transition from legacy code to rewritten codebase that uses standard browser features.

    2. Identify and remove unused CSS/JS.
      CSS and JavaScript code coverage in Chrome allows you to learn which code has been executed/applied and which hasn't. You can start recording the coverage, perform actions on a page, and then explore the code coverage results. Once you’ve detected unused code, find those modules and lazy load with import() (see the entire thread). Then repeat the coverage profile and validate that it’s now shipping less code on initial load.

      You can use Puppeteer to programmatically collect code coverage. Chrome allows you to export code coverage results, too. As Andy Davies noted, you might want to collect code coverage for both modern and legacy browsers though.

      There are many other use-cases and tools for Puppetter that might need a bit more exposure:

      A Screenshot of the Pupeteer Recorder on the left, and a screenshot Puppeteer Sandbox shown on the right
      We can use and Puppeteer Sandbox to record browser interaction and generate Puppeteer and Playwright scripts. (Large preview)

      Furthermore, purgecss, UnCSS and Helium can help you remove unused styles from CSS. And if you aren’t certain if a suspicious piece of code is used somewhere, you can follow Harry Roberts' advice: create a 1×1px transparent GIF for a particular class and drop it into a dead/ directory, e.g. /assets/img/dead/comments.gif.

      After that, you set that specific image as a background on the corresponding selector in your CSS, sit back and wait for a few months if the file is going to appear in your logs. If there are no entries, nobody had that legacy component rendered on their screen: you can probably go ahead and delete it all.

      For the I-feel-adventurous-department, you could even automate gathering on unused CSS through a set of pages by monitoring DevTools using DevTools.

    Webpack comparison table
    In (Material Components for Vue), styles drop from 194KB to 10KB.

    There are many further tools to help you make an informed decision about the impact of your dependencies and viable alternatives:

    Alternatively to shipping the entire framework, you could trim your framework and compile it into a raw JavaScript bundle that does not require additional code. Svelte does it, and so does Rawact Babel plugin which transpiles React.js components to native DOM operations at build-time. Why? Well, as maintainers explain, "react-dom includes code for every possible component/HTMLElement that can be rendered, including code for incremental rendering, scheduling, event handling, etc. But there are applications that do not need all these features (at initial page load). For such applications, it might make sense to use native DOM operations to build the interactive user interface."

    size-limit provides basic bundle size check with details on JavaScript execution time as well
    size-limit provides basic bundle size check with details on JavaScript execution time as well. (Large preview)
    1. Do we use partial hydration?
      With the amount of JavaScript used in applications, we need to figure out ways to send as little as possible to the client. One way of doing so — and we briefly covered it already — is with partial hydration. The idea is quite simple: instead of doing SSR and then sending the entire app to the client, only small pieces of the app's JavaScript would be sent to the client and then hydrated. We can think of it as multiple tiny React apps with multiple render roots on an otherwise static website.

      In the article "The case of partial hydration (with Next and Preact)", Lukas Bombach explains how the team behind, one of the news outlets in Germany, has achieved better performance with partial hydration. You can also check next-super-performance GitHub repo with explanations and code snippets.

      You could also consider alternative options:

      Jason Miller has published working demos on how progressive hydration could be implemented with React, so you can use them right away: demo 1, demo 2, demo 3 (also available on GitHub). Plus, you can look into the react-prerendered-component library.

      +485KB of JavaScript upon loadshare() in Google Docs
      , a fantastic talk by Alex Holachek, along with , covering all the options from start to finish.

    2. Take advantage of optimizations for your target JavaScript engine.
      Study what JavaScript engines dominate in your user base, then explore ways of optimizing for them. For example, when optimizing for V8 which is used in Blink-browsers, Node.js runtime and Electron, make use of , so if you are developing for India or Africa, defer will be ignored, resulting in blocking rendering until the script has been evaluated (thanks Jeremy!).

      You could also hook into V8’s code caching as well, by splitting out libraries from code using them, or the other way around, merge libraries and their uses into a single script, group small files together and avoid inline scripts. Or perhaps even use v8-compile-cache.

      When it comes to JavaScript in general, there are also some practices that are worth keeping in mind:

    An illustration to help you understand time loading and responsiveness
    A blue banner showing running JS with white lines in regular gaps representing the time when we proactively check whether there’s user input without incurring the overhead of yielding execution to the browser and back
    isInputPending() is a new browser API that attempts to bridge the gap between loading and responsiveness.(Large preview)
    An illustration of a map showing the chain of each request to different domains, all the way to eighth-party scripts
    The request map for showing the chain of each request to different domains, all the way to eighth-party scripts. Source. (Large preview)
    1. Always prefer to self-host third-party assets.
      Yet again, self-host your static assets by default. It’s common to assume that if many sites use the same public CDN and the same version of a JavaScript library or a web font, then the visitors would land on our site with the scripts and fonts already cached in their browser, speeding up their experience considerably. However, it’s very unlikely to happen.

      For security reasons, to avoid fingerprinting, browsers have been implementing partitioned caching that was introduced in Safari back in 2013, and in Chrome last year. So if two sites point to the exact same third-party resource URL, the code is downloaded once per domain, and the cache is "sandboxed" to that domain due to privacy implications (thanks, David Calhoun!). Hence, using a public CDN will not automatically lead to better performance.

      Furthermore, it’s worth noting that resources don’t live in the browser’s cache as long as we might expect, and first-party assets are more likely to stay in the cache than third-party assets. Therefore, self-hosting is usually more reliable and secure, and better for performance, too.

    2. Constrain the impact of third-party scripts.
      With all performance optimizations in place, often we can’t control third-party scripts coming from business requirements. Third-party-scripts metrics aren’t influenced by end-user experience, so too often one single script ends up calling a long tail of obnoxious third-party scripts, hence ruining a dedicated performance effort. To contain and mitigate performance penalties that these scripts bring along, it’s not enough to just defer their loading and execution and warm up connections via resource hints, i.e. dns-prefetch or preconnect.

      Currently 57% of all JavaScript code excution time is spent on third-party code. The median mobile site accesses 12 third-party domains, with a median of 37 different requests (or about 3 requests made to each third party).

      Furthermore, these third-parties often invite fourth-party scripts to join in, ending up with a huge performance bottleneck, sometimes going as far as to the eigth-party scripts on a page. So regularly auditing your dependencies and tag managers can bring along costly surprises.

      Another problem, as Yoav Weiss explained in his talk on third-party scripts, is that in many cases these scripts download resources that are dynamic. The resources change between page loads, so we don’t necessarily know which hosts the resources will be downloaded from and what resources they would be.

      Deferring, as shown above, might be just a start though as third-party scripts also steal bandwidth and CPU time from your app. We could be a bit more aggressive and load them only when our app has initialized.

      /* Before */
      const App = () => {
        return <div>
            window.dataLayer = window.dataLayer || [];
            function gtag(){...}
            gtg('js', new Date());
      /* After */
      const App = () => {
        const[isRendered, setRendered] = useState(false);
        useEffect(() => setRendered(true));
        return <div>
        {isRendered ?
            window.dataLayer = window.dataLayer || [];
            function gtag(){...}
            gtg('js', new Date());
        : null}

      In a fantastic post on (also check ).

      Afterwards, we can explore lightweight alternatives to existing scripts and slowly replace duplicates and main culprits with lighter options. Perhaps some of the scripts could be replaced with their fallback tracking pixel instead of the full tag.

      Left example showing 3KB of JavaScript using the lite-youtube custom element, middle and right example showing +540KB of JavaScript with the lite-youtube custom element
      Loading YouTube with facades, e.g. lite-youtube-embed that’s significantly smaller than an actual YouTube player. (Image source) (Large preview)

      If it’s not viable, we can at least lazy load third-party resources with facades, i.e. a static element which looks similar to the actual embedded third-party, but is not functional and therefore much less taxing on the page load. The trick, then, is to load the actual embed only on interaction.

      For example, we can use:

      One of the reasons why tag managers are usually large in size is because of the many simultaneous experiments that are running at the same time, along with many user segments, page URLs, sites etc., so according to Andy, reducing them can reduce both the download size and the time it takes to execute the script in the browser.

      And then there are anti-flicker snippets. Third-parties such as Google Optimize, Visual Web Optimizer (VWO) and others are unanimous in using them. These snippets are usually injected along with running A/B tests: to avoid flickering between the different test scenarios, they hide the body of the document with opacity: 0, then adds a function that gets called after a few seconds to bring the opacity back. This often results in massive delays in rendering due to massive client-side execution costs.

      Seven previews shown from 0.0 seconds to 6.0 seconds showing how and when contents are hidden by the anti-flicker snippet when a visitor initiates navigation
      With A/B testing in use, customers would often see flickering like this one. Anti-Flicker snippets prevent that, but they also cost in performance. Via ) is always a more performant option.

      If you have to deal with almighty Google Tag Manager, Barry Pollard provides some guidelines to contain the impact of Google Tag Manager. Also, Christian Schaefer explores strategies for loading ads.

      Watch out: some third-party widgets hide themselves from auditing tools, so they might be more difficult to spot and measure. To stress-test third parties, examine bottom-up summaries in Performance profile page in DevTools, test what happens if a request is blocked or it has timed out — for the latter, you can use WebPageTest’s Blackhole server that you can point specific domains to in your hosts file.

      What options do we have then? Consider using service workers by racing the resource download with a timeout and if the resource hasn’t responded within a certain timeout, return an empty response to tell the browser to carry on with parsing of the page. You can also log or block third-party requests that aren’t successful or don’t fulfill certain criteria. If you can, load the 3rd-party-script from your own server rather than from the vendor’s server and lazy load them.

      Another option is to establish a Content Security Policy (CSP) to restrict the impact of third-party scripts, e.g. disallowing the download of audio or video. The best option is to embed scripts via <iframe> so that the scripts are running in the context of the iframe and hence don’t have access to the DOM of the page, and can’t run arbitrary code on your domain. Iframes can be further constrained using the sandbox attribute, so you can disable any functionality that iframe may do, e.g. prevent scripts from running, prevent alerts, form submission, plugins, access to the top navigation, and so on.

      You could also keep third-parties in check via in-browser performance linting with feature policies, a relatively new feature that lets you opt-in or out of certain browser features on your site. (As a sidenote, it could also be used to avoid oversized and unoptimized images, unsized media, sync scripts and others). Currently supported in Blink-based browsers.

      /* Via Tim Kadlec. */
      /* Block the use of the Geolocation API with a Feature-Policy header. */
      Feature-Policy: geolocation 'none'

      As many third-party scripts are running in iframes, you probably need to be thorough in restricting their allowances. , so constrain third-party scripts to the bare minimum of what they should be allowed to do.

      A screenshot of the ThirdPartyWeb.Today website visualizing how long the entity’s scripts take to execute on average
      ThirdPartyWeb.Today groups all third-party scripts by category (analytics, social, advertising, hosting, tag manager etc.) and visualizes how long the entity’s scripts take to execute (on average). (Large preview)

      Consider using an Intersection Observer; that would enable ads to be iframed while still dispatching events or getting the information that they need from the DOM (e.g. ad visibility). Watch out for new policies such as Feature policy, resource size limits and CPU/Bandwidth priority to limit harmful web features and scripts that would slow down the browser, e.g. synchronous scripts, synchronous XHR requests, document.write and outdated implementations.

      Finally, when choosing a third-party service, consider checking Patrick Hulce's ThirdPartyWeb.Today, a service that groups all third-party scripts by category (analytics, social, advertising, hosting, tag manager etc.) and visualizes how long the entity’s scripts take to execute (on average). Obviously, largest entities have the worst performance impact to the pages they’re on. Just by skimming the page, you’ll get an idea of the performance footprint you should be expecting.

      Ah, and don't forget about the usual suspects: instead of third-party widgets for sharing, we can use static social sharing buttons (such as by SSBG) and static links to interactive maps instead of interactive maps.

    A graph example comparing the percentage of requests of first- and third parties: 399KB amounting to 27% of requests for first party, and 1.15MB amounting to 73% of requests for third-party published a detailed case study on how they managed to shave 1.7 seconds off the site by self-hosting Optimizely. It might be worth it. (Image source) (Large preview)
    1. Set HTTP cache headers properly.
      Caching seems to be such an obvious thing to do, yet it might be quite tough to get right. We need to double-check that expires, max-age, cache-control, and other HTTP cache headers have been set properly. Without proper HTTP cache headers, browsers will set them automatically at 10% of elapsed time since last-modified, ending up with potential under- and over-caching.

      In general, resources should be cacheable either for a very short time (if they are likely to change) or indefinitely (if they are static) — you can just change their version in the URL when needed. You can call it a Cache-Forever strategy, in which we could relay Cache-Control and Expires headers to the browser to only allow assets to expire in a year. Hence, the browser wouldn’t even make a request for the asset if it has it in the cache.

      The exception are API responses (e.g. /api/user). To prevent caching, we can use private, no store, and not max-age=0, no-store:

      Cache-Control: private, no-store

      Use Cache-control: immutable to avoid revalidation of long explicit cache lifetimes when users hit the reload button. For the reload case, immutable saves HTTP requests and improves the load time of the dynamic HTML as they no longer compete with the multitude of 304 responses.

      A typical example where we want to use immutable are CSS/JavaScript assets with a hash in their name. For them, we probably want to cache as long as possible, and ensure they never get re-validated:

      Cache-Control: max-age: 31556952, immutable

      According to Colin Bendell’s research, immutable .

      According to Web Almanac, "its usage has grown to 3.5%, and it’s widely used in Facebook and Google third-party responses."

      Effectiveness of Cache Control across continents with data retrieved from Android Chrome and iOS Safari
      Cache-Control: Immutable reduces 304s by around 50%, according to by Ilya Grigorik,
    2. Keeping things fresh with stale-while-revalidate by Jeff Posnick.
    3. CS Visualized: CORS by Lydia Hallie is a great explainer on CORS, how it works and how to make sense of it.
    4. Talking on CORS, here’s a little refresher on Same-Origin Policy by Eric Portis.
    A graph showing cache retrieval time by count of cached assets with different OS and browsers named on the right (from top to bottom): Desktop Chrome OS, Tablet Android OS, Mobile Android OS, Desktop Mac OS X, Desktop Windows, Desktop Linux
    We assume that browser caches are near-instantaneous, but data shows that retrieving an object from cache can take hundreds of milliseconds! From Simon Hearne’s research on When Network Is Faster Than Cache. (Large preview)

    Table Of Contents

    1. Getting Ready: Planning And Metrics
    2. Setting Realistic Goals
    3. Defining The Environment
    4. Assets Optimizations
    5. Build Optimizations
    6. Delivery Optimizations
    7. Networking, HTTP/2, HTTP/3
    8. Testing And Monitoring
    9. Quick Wins
    10. Everything on one page
    11. Download The Checklist (PDF, Apple Pages, MS Word)
    12. Subscribe to our email newsletter to not miss the next guides.
    slots empire bonus codes
 Editorial (il)