Skip to content

Instantly share code, notes, and snippets.

@littledan
Last active February 5, 2021 10:19
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save littledan/e01801001c277b0be03b1ca54788505e to your computer and use it in GitHub Desktop.

See current development here.

This document proposes a new resource batch file format, together with a concept of preloading resource batches on the Web. This proposal is derived from Web Bundles by Jeffrey Yasskin and dynamic bundle serving by Yoav Weiss, with significant input from Pete Snyder, who raised concerns about Web Bundles.

Resource Batch Preloading

When loading subresources on the Web, developers currently have an unfortunate choice between serving resources individually, or using bundlers, both of which have disadvantages which hurt loading performance. This document proposes a new mechanism, "resource batch preloading", which allows subresources to be loaded as a batch, to get the best aspects of both mechanisms.

Resource batches are represented in a new, simple file format, which is more or less an association of URLs to a MIME type and payload. We are proposing a new format, rather than reusing something like tar and zip, because those formats have a lot of legacy complexity relating to certain kinds of filesystems, while lacking sufficient mechanisms to store metadata.

On the Web, resource batches can be preloaded in the context of an HTML document. Preloading a resource batch requires that the HTML document explicitly list which resources are expected to be served from the batch. The resource batch must be served from the same origin as the resources it represents. Resource batches may not be personalized; they contain several mitigations to preserve privacy and enable content blocking and cache efficiency.

Goals

Overhead and advantages of subresource processing today

Web pages consist of many different subresources. Web developers have found that loading a large number of these resources can be a performance bottleneck. This issue exists for JS, CSS, images, SVG, and other resource types. The overhead comes from several factors, including:

  • The need to load subresources is sometimes only "discovered" incrementally, e.g., due to deep import graphs, or just being referenced further down in the HTML (addressable by existing prefetch/preload constructs, but not always used in practice).
  • Each subresource uses a separate compression dictionary, so there is higher network traffic for many smaller resources than fewer big resources.
  • Interprocess communication between the browser's network and renderer processes is heavy (which was shown to be the dominating factor once preloading is enabled in V8's data).

At the same time, there are strong benefits to the Web Platform's current mechanism of processing subresources individually:

  • Subresources can be downloaded and processed in a streaming way--they can be processed and even rendered as soon as they are pulled down, incrementally.
  • Subresources are cached individually, so the browser consults its cache before requesting to load one, even if other subresources are not in the cache.
  • Subresources are separate from each other, so they can often be processed in parallel.
  • Subresources are identified by a MIME type which is visible to the browser, so it can start processing them directly, without waiting for any JavaScript to include/invoke them.

Issues with current bundlers

Javascript bundling programs like webpack, Rollup, Parcel, etc. build a collection of modules, stylesheets, images, and other resources into a smaller number of resources for the client to download. Today, they have to represent every non-Javascript resource as a JS string, which causes several efficiency problems:

  • Resources need to be parsed both as the JavaScript string and again as their actual format.
  • Images and other binary resources have to be encoded, which increases their size.
  • Subresources cannot easily be shared from one bundle to the other.
  • It's hard to parallelize processing of different subresources when they're all embedded in JS; the browser cannot see what's going on until the JS processes them.
  • The bundle cannot be processed in a streaming, incremental way; it can only be processed once the whole thing is downloaded.
  • When bundles are split up to allow only downloading the parts that are needed ("code splitting"), the split needs to be static in practice, leading to several smaller bundles.

Bundlers today are approaching standard JS/Web semantics more and more closely, but differences remain, leading to differences between development and production environments.

Opportunity for resource batch preloading to address issues

Resource batch preloading could help address these issues that developers face with the current choice between individual subresource processing and inflexible emulated bundling.

Compared to serving individual subresources, resource batch preloading would allow a single HTTP response (or file read in a case like Node.js) to serve several subresource requests. This makes it possible to use native ES modules, natively use separate CSS files per component, separate SVG and images each into separate files rather than using spriting techniques, etc., without a regression in runtime performance.

Compared to current bundling techniques, resource batch preloading would:

  • Avoid overhead caused by encoding subresources into bundles, by instead including their contents simply concatenated verbatim.
  • Allow batches to be streamed in, so even if parts of the batch are not yet downloaded, the earlier subresources towards the beginning of the batch are already available and can be displayed to the user.
  • Let the browser "see into" the preloaded batch, so it can start processing resources in parallel as they are downloaded, and the parallelism can be in smaller chunks because it is clear what is separate from what.
  • When a batch is requested, the request includes a digest of the relevant parts of the browser's cache, so that only the needed parts are downloaded.
  • Batch preloading lets incremental loading be dynamic, with each preload site asking for the specific list of resources that it needs.

The developer experience for batch-preloaded vs non-batched content would match: batch preloading would continue to use the semantics of web standards.

Privacy preservation

Origin model

This proposal aims to strongly preserve the Web's origin model. On the Web, resource batches are preloaded from a URL in a particular https:// origin. Resource batches contain subresources identified by paths relative to where the bundle was fetched, in the same enclosing "directory". They can't contain resources within any other https:// origin.

(A possible extension would be to lower the privilege level even further with mutually-isolated segments within an https:// origin, but this is not part of the proposal.)

Personalization

Contents of resource batches may not be personalized: Servers are required to respond with the same contents to the individual resource load as they put in the batch. This is a critical requirement to preserve privacy for multiple reasons:

  • Tracking: Apple raised the concern that batch preloading could be used as a tracking vector, by sending different clients versions of the batch with certain bits set differently.
  • URL integrity: Brave raised the concern that bundling systems could be used in a way where URLs are "rotated" between different requests, making URLs less meaningful/stable.

The prohibition of personalizing resource batches is enforced in the following way:

  • All fetches to resource batches are treated as uncredentialed.
  • Browsers may always decide to ignore a resource batch preload command (treating it as loaded immediately). Then, fetches to the "preloaded" resources will be fetches to the network. The decision to ignore a resource batch preload can be based on an off-line analysis of sites to collect a list of poorly behaved resource batches (similar to existing content blocking techniques).
  • Browsers may perform on-line validation of a subset of resources that are preloaded in batches, to enable them to make dynamic decisions about which batch preloads to ignore.

(Some mismatches may be expected occur when the site is updated. In this case, the cache headers of the batch will indicate the update. If they rotate too frequently, then it indicates the exact personalization case that should be blocked, causing the fallback to the underlying resources.)

Note that resource batches are not "isolated" in the URL space. Resources that are preloaded in resource batches have access to the same URLs as the rest of the document and Web. Subresources in the resource batch can point to things outside of the resource batch. So, if personalized content is needed, it can be referenced from inside the resource batch as a link to something outside of it.

Content blocking

Content blockers have a number of requirements when it comes to ensuring that batching/bundling systems do not lead to them being circumvented in practice:

  • It must not be possible for a "trusted" intermediary to "repackage" sites, as this could lead to lead to situations in practice where ads and tracking are signed as the publisher. This is prevented by enforcing the origin model, as explained in "Origin model".
  • Batch preloading must not enable the cheap rotation of URLs, as this would make URL-based content blocking much more difficult. URL rotation is disallowed in this proposal, as described in "Personalization".
  • When content is blocked, browsers should not have the overhead of downloading the blocked content anyway. Content blockers can skip fetching subresources they are not interested in, by requesting exactly the subset of resources that they need, just as if the resource were found in cache or absent from the resources list.

(Please let me know if there are any more requirements that I'm missing!)

Beyond this document

Some further ideas beyond the core batch preloading concept:

  • Resource batch format as a convention in building/serving: We will need a build pipeline that leads to the appropriate calls to the batch preloading API if it is to be adopted. Further, the resource batch file format could help aid the deployment various HTTP features. This idea is described in the below document.
  • Streaming module graph execution: Current ES module semantics are that the whole module graph is linked and executed once it is all available. It could improve loading performance to start executing (not just compiling) modules as they are streamed in, rather than waiting for them to all load, as investigated by Yoav Weiss and Leszek Swirski.
  • Efficient upgrades combined with consistent incremental loading: Subsets of a batch, requested dynamically over time, may have mismatching "versions" if a change is made on the server while a long-running web app is loaded. To enable incremental loading while maintaining consistency of version, without transforming all URLs, we are investigating protocols to enable a separation between incrementally loading one consistent version and performing an online "delta upgrade" without a full reload.
  • Automatic identification of the resources list: One source of difficulty in deploying resource batch preloading is the need to identify the list of resources present in the batch. Guy Bedford's depcache proposal provides a compact representation of the dependency graph of resources, which could be loaded early in the document's lifetime. This could be used to infer which resources should be pulled in, so only the roots need to be explicitly identified.

Resource batch format

Resource batches are a new resource type, whose MIME type would be application/resourcebatch and a convention of a ".rba" file extension. It is a binary file format with two main sections, index and resources. The index provides a mapping from paths to offsets and lengths among responses. These responses contain both initial metadata and a main payload.

Resource batches on and off the Web

On the Web, certain fields take on more specific meaning, grammar, and validity requirements:

  • Paths are interpreted as WHATWG URLs. Resource batches may be served on the Web only from a secure context (https://) and the URLs of each resource must be of the same origin as the URL where the resource batch was served from.
  • The metadata is specifies a MIME type, indicating the type of resource.
  • The payload is the body of the response. It is unrestricted.

On the Web, a resource batch can be thought of as a serialization of a ServiceWorker Cache. This makes it somehow analogous to a "declarative ServiceWorker". The following sections explain the API to preload a resource batch in a web page.

In environments which have some similarities to the Web, such as Node.js, some variant of these interpretations may also apply. In contexts which have more significant differences from the Web, the path and metadata may take different interpretations.

Servers which support resource batch preloading are required to make all of the resources available with the same contents when fetched individually from the same URL, and to strip out unneeded resources to the specific subset that the client requests. Resource batch preloading may not be used for personalized contents--fetches to and responses from resource batches are uncredentialed.

Web APIs for preloading resource batches from HTML

There are two ways to preload a resource batch:

  • Statically with a special <link rel=batchpreload> tag
  • Dynamically with the window.preloadBatch() function

Websites can check whether the preloadBatch function is defined in order to feature-detect whether the browser supports resource batch preloading. If the function is missing, the site may decide to use "legacy" bundler output as they do today instead.

Resource batch preloading can be thought of as a two-stage protocol: first, the enclosing HTML page is fetched, which includes the list of resources within the resource batch to prefetch. Then, the link tag or preloadBatch function is invoked, which will make the appropriate request to the server, based on what is in cache, what contents are blocked, etc. Once the preload is requested, fetches to the subset of listed subresources which the browser emitted the preload to are served from the batch response, rather than from the network (unless the browser decides to make separate requests to the network instead, because it is suspicious of the batch's non-personalization).

<link rel=batchpreload href="./foo.rba" resources="./bar.js ./baz.css" onload="alert('done')">

This API requests that the resource batch located at ./foo.rba be preloaded, specifically for the resources ./bar.js and ./baz.css. The onload event is fired when the index for the resource batch has been fetched.

This API is best to use for resources which are needed for initial page load. It may be that only a certain subset of the batch's contents is needed, and it's fine to list just those specific resources in the attribute--those will be the only ones included in the preload. If a resource is cached, then even if it's in the resources list, it will also not be redundantly preloaded.

The origin of href and each element of resources is required to be the same. However, this origin may differ from that of the surrounding document. For example, I can have my page at https://foo.example/index.html and include a batch preload for <link rel=batchpreload href="https://cdn.example/lib.rba" resources="https://cdn.example/a.js https://cdn.example/b.js">. This is valid because the batch preload is representing resources on the same origin as it was loaded from.

Only elements of resources will be preloaded into cache. Even if the server responds with a resource batch containing additional resources, those will be discarded, and future fetches to those resources will be not be served from the batch.

window.preloadBatch(new URL("./foo.rba", import.meta.url), ["./bar.js", "./baz.css"])

This API does the equivalent of inserting <link rel=batchpreload> tag given the href and resources list provided as parameters. It returns a Promise which resolves when the resource batch's index has been fetched.

This API is best for dynamically loading additional resources after the initial page load. For example, it can be used preceding the dynamic import of some JavaScript modules which are intended to be loaded later as part of "code splitting". This API is also available in workers, unlike the <link> tag.

Semantics of resource batches

Resource batches are preloaded into and served from a "batch preload cache", which exists per document (or, technically, per environment settings object). The batch preload cache may be part of the general "preload" or "memory" cache, but this document restricts itself to defining only the semantics around batch preloading, not other preloading constructs (where differences among implementations exist).

The batch preload cache consists of a mapping from URLs to one of the following states:

  • "loading"
  • a Response representing the individual resource

The batch preload cache is ephemeral, just for the lifetime of the page. It is designed to be implementable from within the renderer process, and requiring only a fixed number of round-trips to the network process when preloading a resource batch.

Sematics of new preloading APIs

To preload a batch at URL url and resources list resources, a list of URLs,

  1. Check that url is a secure context (i.e., https://) and that resources includes only URLs with the same origin as url. The path restriction of ServiceWorker registration also applies to each element of resources. If either condition is not met, return an error.
  2. Remove the entries from resources which are already keys of the batch preload cache.
  3. Synchronously, for each remaining resource, add it to the batch preload cache as "Loading".
  4. Asynchronously, send a message to the network process containing url and resources, and do the following there:
    1. Check the network cache for each element of resources. If any cache entries are found, remove those URLs from resources, and send a compound message back to the renderer containing the appropriate responses.
      1. In the renderer, insert each of these responses in to the batch preload cache (asserting that they were each previously "loading").
    2. Perform a fetch to url sending, as a request header, some sort of compact representation of the remaining list of resources (e.g., this algorithm). Use a distinct "resourcebatch" destination type, triggering a sort of union of relevant headers like Accept, Accept-Language, etc to be sent. The request is uncredentialed.
      1. If the index of the batch is missing any of the entries of resources, generate 404 responses to fill them in.
      2. As responses come over the network in a streaming manner, discard responses which are not in resources, then write any others into the network cache, and send them to the renderer process.
        1. When the renderer receives a set of responses, insert them each into the batch preload cache (asserting that they were "loading").

Semantics of fetch

The HTTP-network-or-cache fetch algorithm is modified towards the end (right before consulting the actual network cache) to check the batch preload cache, in the following way:

  1. If the URL being fetched matches an entry in the batch preload cache, then:
    1. If the entry is "loading", asynchronously continue this algorithm once it is replaced by a response.
    2. Now, let response be the response that the batch preload cache associates with the URL.
    3. Optionally, perform the fetch on the network. If the response differs from response, then return the network's response, and optionally remove other things from the batch cache which came from the same resource batch.
    4. Return response.
  2. Otherwise, proceed to load the response either from the HTTP cache or the network.

Relationships with other proposals

Type-specific bundling formats

There are other proposals for fixed bundling formats for JavaScript and WebAssembly. However, these proposals lack several of the important qualities mentioned above:

  • These formats are not extensible to other subresource types.
  • They are somewhat more complicated/less efficient to parse if you're not already processing the inner resources at the same time (especially in the JS case).
  • There is no protocol described for just fetching the uncached subset, ensuring URL integrity, non-personalization, etc.
  • There is no way to associate other kinds of metadata with particular subresources.

The upside of these resource-specific bundling formats is that they can be processed with the same kinds of tools that process JS and Wasm modules, by having a similar format to them, and that they do not pull in the complexity of allowing the Content-Type header metadata, which requires tools to have an understanding of what a MIME type is.

Web Bundles/WebPackage/WebPackaging/Bundled Exchange

This frequently-renamed effort is being developed in this GitHub repository, led by Jeffrey Yasskin. There are several explainers about various possible details, and other design docs outside of the repo (e.g., for dynamic bundle serving).

The intention of this document is to explain one particular flavor of Web Bundles which meets various requirements, expressed above. The goal is not meant to be a competing/contrasting effort. This document was developed with significant input and feedback from the Web Bundles proponents, as well as detractors (e.g., from Brave), in order to articulate a coherent, concrete, minimal set of features in this space which is aimed to improve loading performance while respecting privacy.

If this document receives positive feedback, the plan would be to work within standards organizations like W3C and IETF to move ahead further, in continuing coordination with the Web Bundles effort as well as with people who expressed concerns about Web Bundles. The exact details of how this would work in standards are to be determined; it's important that the standardization process is consistent with the participants' values.

Resource batches and the developer -> framework -> bundler -> server pipeline

This document proposes conventions for the use of the resource batch file format .rba to package up sites through the pipeline from development to building with frameworks, to bundlers and other optimization tools, to serving in web servers. This pipeline may result in resource batches being ultimately served over HTTP, or other serving strategies (including both current bunder output and simple individual subresources).

The resource batch file format

Resource batches represent a mapping from paths to metadata + payloads. In the context of web tooling/building/serving, the paths are interpreted as URLs, metadata as HTTP headers, and payloads as response bodies.

Compared to .tar or .zip files, resource batches omit a lot of legacy cruft, while representng all the information we need for HTTP responses.

Although resource batches can't represent everything that a site will serve (for example, they omit server-side dynamically generated/personalized content), a large component of websites is static, and can be represented by a resource batch.

Response headers and content negotiation

The metadata field of the resources in a resource batch is interpreted to contain HTTP headers. When preloading resource batches over HTTP, only the Content-Type header is permitted, but within tools, it can be useful to include more headers:

  • There are many more HTTP headers which are important for serving, especially for the main HTML document. Servers can use resource batches to contain all of the headers necessary for a response, even though this means that the response is actually served to the client outside of a resource batch.
  • For content negotiation (including language, file format and more), the resource batch on the server side can contain multiple responses for the same URL. It is the server's job to decide which one to serve, based on the headers from the client.

The use of a common resource batch file format throughout the site build stack allows both of these kinds of information to be propagated through the stack, avoiding complicated configuration problems today.

Resource batch loading API

To preload a resource batch from HTML, use a <link type=preloadbatch> tag. Or, to load resources dynamically, use the preloadBatch function.

Both of these APIs require a URL to load the resource batch from, plus resources list of URLs. Ideally, this list would include all nested dependencies. However, it can be difficult to calculate this list locally, or even say what the final bundle will be called. This proposal includes conventions for tools to output code which

Framework/developer code

When developers write source code, they do so in a high level style. For example, JS modules are directly imported with import statements and dynamic import(). This is later expected to be transformed into particular chunks.

Under the conventions established in this document, frameworks can output the following constructs and expect bundlers to handle the details:

  • <link rel=resourcebatch>
  • import("foo.js")
  • preloadBatch(null, [new URL("resource.xyz", import.meta.url)])

Frameworks are encouraged to output their static contents in resource batches, for later processing by bundlers and serving.

There would be a simple tool for web developers to assembly a resource batch out of a directory of files, as well. This could be used with the same conventions to be passed to a bundler.

Bundler transformations

In a resource batch world, a bundler converts one resource batch (from the developer/framework) into another (to be used directly by the server). This document establishes the following conventions for bundlers:

  • When the bundler sees <link rel=resourcebatch>, its job is to fill in the href= and resources= list. It does this based on its analysis of which resources are used by the enclosing HTML file.
  • When a bundler sees import("foo.js"), it replaces it with preloadBatch("import.rba", [new URL("foo.js", import.meta.url), "dependency.js", ...]).then(() => import("foo.js")) with all of the dependencies of foo.js statically detected and inserted.
  • When a bundler sees preloadBatch(null, [new URL("resource.xyz", import.meta.url)]), it detects all of the dependencies of resource.xyz statically and adds them to the resource list, as well as filling in the location of the resource batch.

Bundlers are encouraged (paradoxically) add resources to the resource batch to contain the output of existing bundling techniques. These will never be served within a resource batch, since they will never be in the resources= list. They are placed in the resource patch that the bundler outputs in order to communicate with the server.

Server expectations

A server, given a resource batch index.rba, is expected to serve it in the following way:

  • Each URL within the resource batch can be fetched from the server, with the relative URLs in the index being resolved relative to the enclosing directory where index.rba is present.
    • If a URL is present multiple times, then the server can choose what to respond with based on its own content negotiation algorithm.
  • If index.rba itself is fetched, then it is served as follows:
    • The request must come with a header indicating the subset of resources to be served. If that header is missing, respond with an error.
    • Omit resources which are not included in the subset requested in the header.
    • Based on the other headers, perform content negotiation to choose appropriate responses for each resource with duplicate URLs (in terms of Content-Type, language, etc).
    • The Content-Type of a resource batch is application/resourcebatch.
    • Serve the result compressed if possible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment