The basic operation of resource preloading with bundled responses follows this outline:
- the Web document provides (declaratively or imperatively) a list of required resources;
- the client sends an HTTP request for those not in its cache;
- the server replies with a bundled response containing the requested resources.
The API to implement bundle preloading consists of:
- a declarative way in HTML to list resources to be retrieved in a bundle;
- an imperative JavaScript API for bundle preloading;
- request headers and corresponding response behavior for delivering bundled resources.
This page explains the general mechanism of bundle preloading and shows examples of each of these features in use, walking through their basic interaction with websites and caches. More complex situations and details, such as CDN behavior and different server edge cases, are discussed in subsequent parts of this proposal.
Let's start with a simplified description of the general mechanism used in this proposal:
When the user wants to visit a Web page, their client requests and receives a document from a remote server.
Since the Web page requires more styles, images, code, etc. to be properly displayed, the document provides a list of required resources that may be preloaded in a bundle (A
, B
, C
).
The client examines its own cache and discovers that it already has one of those resources (A
), but not the others (B
, C
).
The client sends a request to the server to retrieve a bundle with the resources (B
, C
) required by the document and which could not be found in the cache.
The server replies with a bundle containing the requested resources.
Finally, the client has everything that it needs to display the Web page!
Web developers can preload resources from a bundle by using a <script>
tag with type=bundlepreload
. The tag contains a static resource list in JSON format declaring the responses expected to be included in the bundle. Only resources declared in a resource list may be accessed from the bundle; this ensures URL integrity is preserved.
Consider the following resource list for the page https://www.example.com/index.html
:
<script type=bundlepreload>
{
"source": "./assets/resources.wbn",
"resources": [
"render.js",
"sidebar.js",
"profile.png"
]
}
</script>
For easier portability, the list may use relative locations as in the example.
Because of same origin and path restrictions, the resources referenced by the bundle must correspond to URLs with the same base URL as the bundle itself. If they use relative locations, those are resolved taking the location of the bundle as the base URL (see RFC7230 section 2.7 and RFC3986 section 5.1).
In our example, relative locations referenced by the bundle at https://www.example.com/assets/resources.wbn
would be resolved using https://www.example.com/assets/
as their base URL.
This directs the browser that it may serve any of the resources in the "resources"
list by making a single request for https://www.example.com/assets/resources.wbn
. That request must contain the structured header Bundle-Preload
; the value for the Bundle-Preload
header may only contain a non-empty subset of the "resources"
list. To retrieve all the declared resources, the request may look like:
GET /assets/resources.wbn HTTP/1.1
...
Host: www.example.com
Bundle-Preload: "render.js", "sidebar.js", "profile.png"
...
The response to that request must be a bundled response containing HTTP responses for each of the following URLs:
https://www.example.com/assets/render.js
https://www.example.com/assets/sidebar.js
https://www.example.com/assets/profile.png
For more details on the bundled response format, see the IETF WEBPACK draft.
Any references to these resources later in the document may be loaded from the bundled response. For example, if the following elements are present later on the page, the comments indicate what the browser may do:
<img src="https://example.com/assets/profile.png" />
<!-- the image may be loaded from the bundle without an additional request -->
<img src="assets/profile.png" />
<!-- if this relative location corresponds to one of the URLs in the bundle,
the image may be loaded from the bundle without an additional request -->
<img src="more_assets/profile.png" />
<!-- this URL was not part of the bundle (note the different path), so this
results in a new request to https://example.com/more_assets/profile.png -->
Suppose the page at https://www.example.com/index.html
instead contained the following JavaScript:
<script>
window.bundlePreload({
source: "./assets/resources.wbn",
resources: ["render.js", "sidebar.js", "profile.png"]
});
// Results in bundle preloading request, as in previous section
let img1 = document.createElement("img");
img1.src = "profile.png";
// A new request to https://www.example.com/profile.png
let img2 = document.createElement("img");
img2.src = "assets/profile.png";
// Response may be loaded from bundle without an additional request
let img2 = document.createElement("img");
img2.src = "https://www.example.com/assets/profile.png";
// Response may be loaded from bundle without an additional request
</script>
The behavior here matches the behavior for the static resource list.
The example request used in the previous section contained a new structured Bundle-Preload
header. This section showcases the behavior of that header and the expected responses.
In this section, we are assuming both the server and the browser have implemented support for bundle preloading. We call this the full implementation use case. The other documents in this proposal cover how bundle preloading works when one of the parties doesn't implement bundle preloading. The goal in those situations is to achieve graceful degradation for parties that don't implement bundle preloading, while allowing progressive enhancement for all parties that do implement bundle preloading.
In the full implementation use case, the browser may make a request like this:
GET /assets/resources.wbn HTTP/1.1
...
Host: www.example.com
Bundle-Preload: "render.js", "sidebar.js", "profile.png"
...
The Bundle-Preload
header may not always contain all of the values listed in either the <script type=bundlepreload>
section or the window.bundlePreload(...)
call. For example, if the browser has a cached response for https://www.example.com/assets/profile.png
, the header might instead have the following value:
Bundle-Preload: "render.js", "sidebar.js"
The server response to any bundle preloading request must be a bundled response file. Additionally, it must include the Vary: Bundle-Preload
directive.
For example, for the request for three resources used in these examples, some of the response headers would have these values:
Content-Type: application/webbundle
Vary: Bundle-Preload
...
*** data for the resources "render.js", "sidebar.js", and "profile.png" ***
The Bundle-Preload
header isn't the only HTTP change introduced by this proposal, but it is the most important one. For a more detailed description of how bundle preloading works with other HTTP headers such as Vary
and Cache-Control
, see the section on bundle preloading for servers.
Let's consider how bundle preloading usage might look for a large Web application. We'll demonstrate how:
- Bundle preloading enables effective code splitting
- Bundlers can use bundle preloading to prioritize responsiveness
- Updates to individual resources do not cause cachebusting
Let's consider a Web application with the following layout:
├── assets
│ ├── css
│ │ ├── base.css
│ │ ├── index.css
│ │ ├── profile.css
│ │ └── sidebar.css
│ ├── img
│ │ └── logo.png
│ ├── js
│ │ ├── analytics.js
│ │ ├── fancyWidget.js
│ │ └── sidebar.js
│ └── site.wbn
├── index.html
└── profile.html
site.wbn
is a bundle containing all of the other files under assets/
.
Note that from the point of view of this protocol, it is not strictly necessary that the content of the bundle is actually duplicated in the server's filesystem. It would be possible for the server to store just the bundle file and use it to serve single (not bundled) requests to individual resources. See the section on Bundle preloading for servers for more details.
In this example, the server side task of code splitting is simple. site.wbn
contains all the css
, img
, and js
files.
An example resource list for the index.html
page might look like this:
<script type=bundlepreload>
{
"source": "./assets/site.wbn",
"resources": [
"css/base.css",
"css/index.css",
"img/logo.png",
"js/analytics.js"
]
}
</script>
And a corresponding example resource list for the profile.html
page could look like:
<script type=bundlepreload>
{
"source": "./assets/site.wbn",
"resources": [
"css/base.css",
"css/profile.css",
"img/logo.png",
"js/analytics.js",
"js/fancyWidget.js"
]
}
</script>
These resource lists contain overlapping resources. What would happen if a user were to visit index.html
and then profile.html
without any of the site cached?
On the initial visit to index.html
, the browser could issue the following request:
GET /assets/site.wbn HTTP/1.1
...
Bundle-Preload: "css/base.css", "css/index.css", "img/logo.png", "js/analytics.js"
...
Let's assume that the responses for each of those resources in the bundle has the response header Cache-Control: immutable
, indicating to the browser that it may keep those resources cached.
When the user navigates to profile.html
later on, the browser can then issue a request only for those elements not yet in its cache:
GET /assets/site.wbn HTTP/1.1
...
Bundle-Preload: "css/profile.css", "js/fancyWidget.js"
...
Because the browser may issue requests for a subset of the listed resources, it may instead choose to serve resources it already has from its cache. This helps achieve effective code splitting.
An important metric for many websites with lots of static resources to load is the TTI, or the time until the page is ready for user interaction. Before that time, the user is waiting for the browser to load and process the resources needed to render the page.
In this example, we've omitted css/sidebar.css
and js/sidebar.js
from the static resource lists embedded in the page with <script type=bundlepreload>
tags. A bundler or Web developer might defer loading of these resources until the page has rendered and is ready for user interaction, in order to improve TTI. A simple way to then preload these resources so that the sidebar will respond quickly without delays caused by needing to fetch resources would be:
document.body.onload = function() {
window.bundlePreload({
source: "./assets/site.wbn",
resources: ["css/sidebar.css", "js/sidebar.js"]
});
};
This makes it easy to dynamically load parts of bundles together, whenever client code determines it is ready to preload them.
Many people using the Web have metered internet data, lower bandwidth connections, and infrequent internet access. For such users it can be especially important to avoid causing them to unnecessarily download extra data to access a site. Even as websites change, some avoid changing their bundling configurations ("cachebusting") so that users can use already cached resources for a website; this spares users from needing to download a whole bundle when only some of the resources it contains have changed. It does so at the expense of needing to track historical bundling configurations on the server side, to model and estimate user cache contents, and to balance complicated performance heuristics.
Bundle preloading helps sites to avoid cachebusting. Let's consider what happens to a user who has already visited our entire example site. Because the responses contained in the bundle all have the Cache-Control: Immutable
response header, their browser has likely retained these resources in its cache. But what if a Web developer wants to change the base stylesheet and logo for a site, as well as improve the fanciness of fancyWidget.js
?
All the developer needs to do is rev those resources' URLs and update the resource list. For example, in index.html
:
<script type=bundlepreload>
{
"source": "./assets/site.wbn",
"resources": [
"css/base2.css",
"css/index.css",
"img/hot-new-logo-2021-rev2.png",
"js/analytics.js"
]
}
</script>
And in profile.html
:
<script type=bundlepreload>
{
"source": "./assets/site.wbn",
"resources": [
"css/base2.css",
"css/profile.css",
"img/hot-new-logo-2021-rev2.png",
"js/analytics.js",
"js/fancierWidget.js"
]
}
</script>
Again, because the browser has cached resources, the bundle preloading requests might only contain resources the browser does not have. If the user visits profile.html
first after the page has changed, their browser could issue just the following request:
GET /assets/site.wbn HTTP/1.1
...
Bundle-Preload: "css/base2.css", "img/hot-new-logo-2021-rev2.png", "js/fancierWidget.js"
...
Of course, this Web developer's strategy for revving isn't ideal; it would help them to use a bundler that can perform revving for them, perhaps producing merkled URLs inside Web bundles rather than manually renaming files. One suspects that js/fanciestWidget.js
is just around the corner, but solving that problem is another topic well beyond the scope of this proposal.
This is an overview of bundle preloading. In summary:
- Bundle preloading introduces a mechanism for browsers to preload and natively interpret multiple resources with a single request.
- Browsers only serve bundled responses from the same scope as the bundle, helping to preserve resource identity.
- If browsers have cached responses, they can request only a subset of what a bundle may contain.
- Servers do not need to keep track of any per-client bundle or cache state.
- This mechanism simplifies code splitting for bundlers, allowing client side JavaScript to decide what and when it needs resources.