Learn why Gartner just named Builder a Cool Vendor

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

web development

Code Prefetching is a LIE

March 8, 2023

Written By Miško Hevery

Congratulations! Your application has grown enough to be broken down into multiple JavaScript bundles! Well, these bundles will need to be prefetched for a good user experience! But how prefetch works is a bit misleading. (Not to mention that Lazy Loading is a LIE as well.)

To get your application broken down into multiple bundles, some place in your code, there must be a dynamic import, such as import('./some-dependency.js'). Such dependencies are usually inserted as part of the meta-framework routing, but you, as a developer, can also insert additional dynamic imports into your code.

Let’s say you placed the code behind a Buy button behind the lazy-loaded chunk. Your code may look something like this.

export default () => {
  return (
    <div>
      <button onClick={async () => {
        // Let's lazy-load the logic behind
        // the "buy" button on click.
        (await import('./buy.js')).default();
      }}>Buy</button>
    </div>
  );
};

But now you have a new problem to solve! When the user clicks on the Buy button, the browser lazy loads the buy.js bundle. Depending on the size of the bundle and the speed of the network, this may introduce a significant, noticeable, and annoying delay. What can we do about it?

Luckily, Browsers come with prefetch support! So you will drop something like this into the head section and think you have solved the problem.

<head>
  <link rel="prefetch" href="buy.js"/>
  <!-- Or choose an alternate strategy
    <link rel="preload" href="buy.js"/>
    <link rel="modulepreload" href="buy.js"/>
  -->
</head>

You test it in Chrome on a desktop because that is your primary development environment, and everything works as expected. You declare victory and start working on your next task in the queue.

But soon, you start getting reports from the field that there are many instances when the user has to wait for the Buy button to perform its action. This extra waiting is hurting the bottom line. So what is going on?

Miško Hevery (Builder.io/Qwik) avatar
Miško Hevery (Builder.io/Qwik)@mhevery
RANT: You would think that <link rel="prefetch"> will prefetch. Turns out some browsers just ignore it. FF won't do it if it is HTTPS, and mobile browsers won't do it to save on data. WAT? Why? What is the point of prefetch, then? You had one job!

The first problem with the prefetch (and friends) is that the browser is under no obligation to honor it. 🤯 It is just a hint. So let’s look at some surprises:

  1. modulepreload can’t be used in most browsers.
  2. Firefox has network.dns.disablePrefetchFromHTTPS option, which is set to true by default. Yes, by default, Firefox will not prefetch anything on HTTPS. Given that most things are HTTPS these days, this effectively disables prefetching on Firefox.
  3. Some mobile browsers ignore prefetch because they assume they are on the mobile network and are trying to save on bandwidth.
Untitled

Most browsers only process prefetch when the network tab becomes idle. This makes sense, but for the application to feel interactive, you want to make sure that the interactivity is present before secondary things, such as high-res images are present. It is often too late to wait until everything else on the page loads before we start fetching JavaScript.

Imagine a site showing you your photos. Photos are large and take a while to download. But you would like to start interacting with the site before all the photos are downloaded. Fetching JavaScript after all the images are resolved is probably not what you want.

This really speaks to the lack of control as to “when” prefetch is resolved in a browser.

Prefetching is supposed to improve interactivity, but in some cases, it can worsen it.

Untitled

Imagine you are on a slow connection. Prefetch kicks in and starts downloading JavaScript. Before JavaScript can fully download, the user interacts with your application. Now, import('./buy.js') gets executed, but buy.js is not in the cache. An active buy.js request in flight has not yet been completed. But because the request is incomplete, the browser does not know what the cache headers are, so it does not know if it is safe to reuse the request. So the browser does the safe thing and issues another buy.js resource request. Now you have two requests for the same resource in flight. What is worse, the original resource resolves, and buy.js is inserted into the browser cache, but the resolution of that resource does not unblock the user interaction. Instead, the UI has to wait for the second buy.js to return before the UI can be unblocked.

Yes, prefetch is set up such that it can result in requesting the same resource multiple times.

Finally, some browsers will issue a console warning if they detect that a given prefetch resource has not been used within x number of seconds. The assumption is that if you have not used it, you should not be prefetching it.

Untitled

What we are trying to do is to speculatively prefetch code so that the user does not have to wait. Yes, the speculative nature is such that it is possible that the user will not use it, and that should be OK.

What if your buy.js bundle gets further broken down to buy.js and common.js because common.js is also used from other.js? In that case, you can’t just prefetch buy.js but also common.js. But that is bit of a problem. The developer knows about buy.js because that is what they placed in import('./buy.js'), but they don’t know about common.js because that bundle is a synthetic bundle that the bundle system created. How is the developer supposed to know to insert additional prefetch URLs?

What makes this worse is that the common.js is actually some-hash.js and the hash changes on each build. So in order to get the hash, the application needs to be built, but to build the application we need to know what the hash is to prefetch.

Failing to do the above results in slow waterfall resolutions. Even if buy.js is in cache, the dependency probably isn’t.

Really prefetch is a hint that you will need something and so the browser should have a head start on downloading, but what we want is to speculatively pre-fill the cache with possible code which the user may need for user interaction.

Ideally, we want to control the cache so that we:

  • Control when the cache is populated.
  • Understand the dependency graph of chunks so that we can also prefetch synthetic bundles.
  • Control the requests so that we can de-dup the requests if the request is not yet in the bundle.

Really we want to go from a passive list of prefetch chunks and hoping for the best to an active participant that controls the prefetching.

Turns out we can do that with a service worker. Service workers can intercept requests and control what is in the cache. Using a service worker, we can have the right control over the process. Service workers can also know the chunk graph and can load related code to prevent waterfalls.

But creating such a service worker is not easy, and so most developers don’t do it. Actually, this sounds like a perfect OSS project. Anyone up for it? (Also we are not first to have this idea: Prefetching Links using Service Workers is from 2019 and describes just such concept.)

Actually Qwik does exactly this as described in Speculative Module Fetching: a Modern Approach to Faster App Interactivity. With Qwik we have learned the hard way that prefetch and friends do not actually do what you need to have a quickly interactive site. Our solution was to build the service worker which works way better than any static prefetch could.

But the above got us thinking just how unrealistic the prefetch advice is. Yes, in theory it will do what you want, but in practice we find it very lacking.

Prefetching your JavaScript is common advice given by the “experts” to make sure that your lazy-loaded chunks do not incur delay on user interaction. But it turns out reality is never so simple and using prefetch does not work as well in practice as you may have hoped.

Instead, we recommend using a service worker to get full control over the prefetching process. The results have worked great for us by allowing us to remove any interactivity delays due to lazy-loading of code.

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
Design to code5 MIN
Builder.io Named a Cool Vendor in the 2024 Gartner® Cool Vendors™ in Software Engineering: User Experience
November 21, 2024
AI8 MIN
How to Build Reliable AI Tools
November 15, 2024
Web Design11 MIN
Design Smarter with Figma Auto Layout
November 13, 2024