HTTP/2 PUSH vs HTTP Preload
HTTP/2 PUSH is a feature that lets a server pre-emptively push resources to the client (without a corresponding request). HTTP Preload is a way to indicate to the browser resources it would require while loading the current page. In this post, we will discuss the key differences between PUSH and Preload, with a detailed explanation of which one to choose based on your use case.
The easiest way to understand the advantages of PUSH and Preload is via a diagram of network requests. Let us assume that the browser is loading a simple HTML page with a CSS file, which in turn references a font file. The following is a simplified version of how the request-response cycles will look like.
Using HTTP/2 PUSH, the server can push the font file immediately when it gets the request for the HTML file since it knows that the client will be requesting for it in the future.
A Preload tag or header can be used to make a request for a resource that would be referenced in the future. This is how things change with a preload tag.
How are they similar
Both these mechanisms are used to optimize the unused bandwidth of the client to download resources preemptively and are an excellent way to decouple the download with the actual “execution” of the resource. For example, a script can be preloaded or PUSHed to the client and when the browser actually makes a request for that particular script, it finds it in the cache and does not have to wait on the network.
How are they different
Resources can be preloaded via HTML.
<link rel="preload" href="/styles/other.css" as="style">
They can also be preloaded via response headers
The way you initiate a HTTP/2 PUSH depends on the server you are using. For example, if you are using Node.js you can initiate a PUSH stream via the
Link: https://example.com/other/styles.css; rel=preload; as=style
The preload spec mentions that servers may initiate a PUSH when it sees a preload header (yup, you read that right) and most CDNs have standardized around this behavior to initiate a PUSH from their edge nodes. This can be quite confusing at times and as you can see from this post, PUSH and preload are quite different. Only in very rare cases you would want to use them interchangeably. There has been some discussion to see if this behavior can be separated.
For now, if you are sure you just want to preload a resource and not PUSH it, you can use the
no-push attribute in your Link header like so:
Link: </app/style.css>; rel=preload; as=style; nopush
Kinds of resources
You can only PUSH resources from your own domain or domains you are authoritative for. However, you can preload resources from any domain.
Earliest point when you can PUSH / preload
Since the whole point of PUSH / Preload is to early load resources which are discovered later on by the browser, it is interesting to compare how early a PUSH or preload can be initiated. With respect to this, PUSH does better than preload since you can initiate a PUSH as soon as the server gets the very first request from the client.
A Preload tag can initiate a preload only after the browser receives the HTML from the server, parses it, finds the preload tag. Almost all browsers also have a lookahead parser which should help here.
Preloading via the HTTP header gives you a minor improvement over the previous approach since you do not need to wait for the browser to parse the HTML to start the preload request.
Early Hints takes this a step further. Using this status code, servers would be able to initiate a preload (or a PUSH), even before the response headers for the HTML is sent. The Early Hints Status code (103) was recently approved by IETF as an official status code!
Resources that are PUSHed and preloaded end up at different types of caches. Preloaded resources are part of the memory cache and PUSHed resources are stored separately in the PUSH cache (no, you don’t get any points for guessing the name of that cache).
This becomes important since both these caches have different semantics as we will see in a bit.
Lifetime of the cache
The PUSH cache is tied to the HTTP/2 connection and is purged when the connection is terminated. If no request matches an item in the PUSH cache, it is not used. Since a HTTP/2 connection can be re-used across multiple tabs, PUSHed resources can be claimed by requests from other tabs as well.
However, the memory cache is tied to the page making the request. This means that preloaded requests cannot be shared across pages.
Interaction with the HTTP Cache
The server can also end up PUSHing resources which are already in the cache. In theory, browsers should be able to abort PUSH streams by sending a
RST_STREAM frame to the server for resources which are already in their cache. But due to various browser bugs, this doesn’t work as expected yet.
Even when these bugs do get fixed, the problem is not eliminated completely since the server can end up PUSHing a lot of frames before it recieves the
RST_STREAM from the client. Cache digests is one proposal to solve this problem. Till browsers implement this, you can mimic their implementation via a service worker.
On the other hand, preload plays very well with the HTTP cache. A network request is never sent if the item is already there in the HTTP cache.
For a more detailed explanation on these types caches, check out Yoav Weiss’s fantastic article on the topic.
Browsers have spent significant time figuring out optimal resource loading strategies. With preload you can attach the type of resource you are preloading which helps the browser figure out the priority with which the resource is made.
With HTTP/2 push, part of this burden falls on to the server as well. Browsers attach different priorities to different streams in an HTTP/2 connection. It is up to the server to decide how to allocate and prioritize its resources based on the priorities sent by the client. The algorithms for prioritization, both on the server and client side would significantly become better as HTTP/2 implementations get more mature.
If you preload assets with the link tag, you can set the
onerror) attributes to the tag to get notified when the resource has been downloaded.
Since HTTP/2 preloads are plain old HTTP requests, content negotiation can be used to determine the right resource to be sent to the user based on the different
Cookie and other headers. For example, Dexecure uses Client Hints to load the correct version of a image. This plays well with preload since the Client Hint headers are sent along with the request for the image. Since with HTTP/2 PUSH you are serving a resource without a corresponding request, there is little room for content negotiation.
The support for preload is not that good yet and most HTTP/2 PUSH implementations are still not as polished. The good thing is that you can start using these features for browsers which support it already, unsupported browsers will just ignore the markup or the header (yay progressive enhancement). There are also ways to feature detect preload in JS, if you are adding markup dynamically to your page.
These are the use cases that are enabled only by PUSH. Preload is not such a good fit here.
Utilising server think time. Usually the network is idle when the server is generating the HTML page, which can take a non-trivial amount of time for dynamic webpages. During this time, the browser is not able to make any requests since it is not aware of any sub-resources that the page might reference. You can use this time to PUSH resources to the client. This has the added benefit of warming up the connection and increasing the TCP congestion window size, so that future requests can be completed faster. Head over to this handy website to see if your website can leverage this technique.
PUSH resources you would traditionally inline, such as small /images, CSS and JS. This is one hack that PUSH obviates completely. You can leverage the browser cache better when you externalize it as a separate asset and PUSH it. When you inline, you are unnecessarily sending the asset along with the HTML every time.
For all other use cases, we recommend using Preload. Preload is good for resources which are discovered late by the browser’s parser but are critical to rendering of the page. Here are some examples of resources that are prime candidates for preloading
fonts referenced by CSS files
hero image which is loaded via background-url in your external CSS file
Inlining critical CSS (or JS) and pre-loading the rest of the CSS (or JS)
What about Websockets, SSE, WEBRTC, Prefetch, Prerender and Subresource?
Websockets, Server Sent Events (SSE) and WebRTC are used to delivering real time data to (and sometimes from) users. PUSH and Preload play better when used on an asset level.
Prefetch is a hint to the browser to download resources for future navigations. For example, webpack bundles needed for pages the user hasn’t visited yet are good candidates to prefetch. On the other hand, as mentioned before, preloaded resources are lost across page navigations.
Prerender, now deprecated, was used to fetch and render a page that you were sure that the user was definitely going to visit in the future.
Subresource, now deprecated, had some fundamental ways in which it was broken.
So what should users do now? I made a flow chart for you to quickly decide which method to go ahead with. Note - I did not include content negotiation and js notifications for error and onload events because both of these can be done if your website is using service workers.