<link rel=”prefetch/preload”> in webpack

webpack 4.6.0 adds support for prefetching (and preloading).

Tobias Koppers
webpack

--

TL;DR: Use import(/* webpackPrefetch: true */ "...") for prefetching.

What‘s <link rel=”prefetch”>?

This “Resource Hint” tells the browser that this is a resource that is probably needed for some navigation in the future.

Browsers usually fetch this resource when they are in idle state. After fetched the resource sits ready in the HTTP cache to fulfill future requests. Multiple prefetch hints queue up and are fetched while idling. When leaving idle state while prefetching to browser may cancel any ongoing fetch (and put the partial response into cache, to be continued with Content-Range headers) and stop processing the prefetch queue.

To sum it up: Fetch while idle.

What’s <link rel=”preload”>?

This “Resource Hint” tells the browser that this is a resource that is definitely needed for this navigation, but will be discovered later. Chrome even prints a warning when the resource isn’t used 3 seconds after load.

Browsers usually fetch this resource with medium priority (not layout-blocking).

To sum it up: Fetch like normal, just earlier discovered.

Why is this useful?

Preload is used to discover resources earlier and avoid a waterfall-like fetching. It’s can bring down the page load to 2 round-trips (1. HTML, 2. all other resources). Using it doesn’t cost additional bandwidth.

Prefetch is used to use the idle time of the browser to speed up future navigations. Using it may cost additional bandwidth when the user doesn’t do the expected future navigation.

Code Splitting

We assume you are building your huge application with webpack and use On-Demand-Loading via import() to load only the parts of your application the user currently needs.

As example we take a HomePage which contains a LoginButton which opens a LoginModal. Once logged in the application brings the user to the DashboardPage. The pages may contain other buttons but these are less common.

To get the best performance you used import("LoginModal") at the LoginButton to keep the HomePage minimal. Similar the LoginModal contains import("DashboardPage").

Now these example application is splitted into at least 3 chunks: home-chunk, login-chunk, dashboard-chunk. On initial load only the home-chunk need to be loaded, which creates a great UX. But when the user clicks on the LoginButton there is a delay until the LoginModal opens, because this part of the application need to be loaded first. Similar with the DashboardPage.

Using prefetching in webpack

The new prefetch feature allows to improve this workflow. And it’s super easy.

At the LoginButton change
import("LoginModal") to
import(/* webpackPrefetch: true */ "LoginModal").

Similar to this change
import("DashboardPage") to
import(/* webpackPrefetch: true */ "DashboardPage") in the LoginModal.

This will instruct webpack to prefetch (in browser idle time) this On-Demand-Loaded chunk when the parent chunk finish loading. In this example: When home-chunk finish loading, add login-chunk to the prefetch queue. When login-chunk finish loading (real load, not prefetch), add dashboard-chunk to the prefetch queue.

Assuming the home-chunk is a entrypoint this will generate <link rel="prefetch" href="login-chunk.js"> in the HTML page.
The login-chunk is a on-demand-chunk so the webpack runtime will take care of injecting a <link rel="prefetch" href="dashboard-chunk.js"> once the login-chunk load is completed.

This will improve the UX in this way:

The user visits the HomePage. UX and performance is as great as before. Once the HomePage finished loading the browser enters idle state and starts fetching the login-chunk in background. The user doesn’t notice this. Assuming the user need some time to find the LoginButton this request will finish before the user clicks the button.
When the user now clicks the button, login-chunk already sits in the HTTP cache and the request hits the cache and only take a minimal amount of time. The user will see the LoginModal instantly.
At the same time the webpack runtime adds dashboard-chunk to the prefetch queue, so it won’t take additional time to load the chunk after login.

Note that the user might not always have this instant LoginModal experience. There are a bunch of factors which can re-add the chunk loading delay to the LoginButton: slow networks, fast clicking users, disabled prefetch on bandwidth-limited devices, no prefetch browser support, very slow execution of the chunk, …

Using preloading in webpack

Similar to import(/* webpackPrefetch: true */ "...") it’s possible to use
import(/* webpackPreload: true */ "..."). This has a bunch of differences compared to prefetch:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finish.
  • A preloaded chunk has medium priority and instantly downloaded. A prefetched chunk is downloaded in browser idle time.
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
  • Browser support is different.

Due to this properties use cases are rare. It can be used if a module always import() something instantly. It could make sense if a Component depends on a big library that should be in a separate chunk. Example: A ChartComponent uses a big ChartingLibrary. It displays a LoadingIndicator when used and instantly
import(/* webpackPreload: true */ "ChartingLibrary"). When a page which uses the ChartComponent is requested, the charting-library-chunk is also requested via <link rel="preload">. Assuming the page-chunk is smaller and finishs faster, the page will be displayed with a LoadingIndicator, until the already requested charting-library-chunk finishs. This will give a little load time boost since it only needs one round-trip instead of two. Especially in high-latency enviroments.

Using webpackPreload incorrectly can actually hurt performance, so be careful when using it.

When thinking about preload vs. prefetch, you probably want prefetch.
Note: This statement is about webpack import(). In HTML pages you probably want preload.

Multiple prefetched chunks

You can add the webpackPrefetch flag to as many import() as you like to, but note that all prefetched chunks fight for the bandwidth. They are actually queued up and the really used chunk might not be prefetch when the user requests it.

This isn’t a big issue when all chunks are equally likely to be requested. But when some chunks are more likely than others you probably want to control the order of prefetching.

Lucky you, we got you covered. (Note this is an advanced use case.)

Instead of using webpackPrefetch: true you can pass a number as value. webpack will prefetch chunks in the order you specified. Example: webpackPrefetch: 42 will be prefetched before webpackPrefetch: 1 which weill be prefetched before webpackPrefetch: true which will be prefetched before webpackPrefetch: -99999 (like z-order). Actually true counts a 0.

Similar for webpackPreload but I don’t think you will need that!

FAQ

What if multiple import()s request the same chunk and some of the are prefetched/preloaded?
Preload wins over prefetch, which wins over nothing. In the same category the highest order wins.

What if prefetch/preload is not supported in the browser?
It’s ignored by the browser. webpack doesn’t try to do any fallback. It’s a hint anyway, so even browser which support it may ignore it if they feel so.

Prefetch/preload doesn’t work in the entrypoint for me. What’s the problem?
The webpack runtime only takes care of prefetch/preload for on-demand-loaded chunks. When prefetch/preload is used in import()s in a entry chunk the html generation is resposible for adding the <link> tags to the HTML. The stats json data provides information in entrypoints[].childAssets.

Why not using prefetch/preload on every import()?
You waste a lot a bandwidth. It’s also more beneficial to use it selectively for import()s that are very likely to be visited. Don’t waste bandwidth. Some people have only limited volume or bandwidth. Use it selectively to get the most benefits!

I don’t need/want this feature. What’s the cost for not using it?
The runtime code is only added when this feature is used in one of your on-demand-loaded chunk. So when not using it you don’t have to pay any overhead.

I already have a magic comment at import(). Can I add multiple magic comments?
Yes, either separated with , or in separate comments:

import(
/* webpackChunkName: "test", webpackPrefetch: true */
"LoginModal"
)
// or
import(
/* webpackChunkName: "test" */
/* webpackPrefetch: true */
"LoginModal"
)
// spacing optional

I build my library with rollup and also use import(). Can users of my library which use webpack benefit from prefetch?
Yes, you can add webpack magic comments to import() and rollup will keep the comment. When the result it built with webpack it’s will use the comment. When not built with webpack the comment is removed by minimizers.

I’m unsure if adding a prefetch for a specific import() improves performance. Should I add it?
Best measure it. A/B testing. There is no general advise here. It depends on the probability of users visiting this path of the application.

I already have a service worker in place which caches to whole application on load. Does using prefetch makes sense for me?
It depends on your application. In general this kind of Service Worker loads all assets of the application in no partical order, while prefetch allows to specify an order and depend on the actually position of the user in the application. Prefetch is also more bandwidth-efficient and only downloads in browser idle time.
Measure how long it takes to download the whole application in low bandwidth enviroments, compared to when the first user navigation happens. In huge applications it could make sense to use prefetch instead.

I want to split my initial page load into critical resources and non-critical resource and load them in this order. Does preload help here?
Yes, you can put critical resources into the entrypoint and non-critical resources behind a import() with webpackPreload: true. Note don’t forget to add <link rel="preload"> for entry chunk assets too (before the tags for children).

webpack is not backed by a big company, unlike many other big Open Source products. The development is funded by donations. Please consider donating if you depend on webpack… (Ask your boss!)

Special thanks to these sponsors: (Top 5)

  • Trivago (Hotel metasearch) donated a total of $100,000
  • ag-Grid (DataGrid) donated a total of $35,000
  • Segment (Data Infrastructure) donated a total of $24,000
  • Adobe (Software) donated a total of $12,000
  • Capital One (Bank) donated a total of $12,000
  • Full list

--

--