HTTP/2 Server Push is often touted as a way to shave milliseconds off page load times by sending resources before the client even asks for them, but in practice, it’s rarely the silver bullet it’s made out to be.

Let’s see this in action. Imagine a simple HTML page that needs a CSS file and a JavaScript file.

<!DOCTYPE html>
<html>
<head>
    <title>HTTP/2 Push Example</title>
    <link rel="stylesheet" href="/style.css">
    <script src="/script.js"></script>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>

Traditionally, when a browser receives this HTML, it parses it, finds the <link rel="stylesheet"> tag, and then makes a separate HTTP request for /style.css. The same happens for /script.js.

With HTTP/2 Server Push, the server can anticipate these needs. When the browser requests /index.html, the server might respond with:

HTTP/2 200 OK
Content-Type: text/html
Link: </style.css>; rel=preload; as=style, </script.js>; rel=preload; as=script
x-http2-push-promise: /style.css
x-http2-push-promise: /script.js

Notice the Link header which is also used for preload. The x-http2-push-promise headers (or equivalent mechanisms depending on the server implementation) signal that the server will be sending these resources. The server then immediately starts sending /style.css and /script.js over the same connection, before the browser has even finished parsing the HTML and explicitly requested them.

The core problem Server Push tries to solve is the "waterfall" of requests. Without Push, the browser downloads HTML, parses it, sees it needs CSS, requests CSS, downloads CSS, parses CSS, sees it needs JS, requests JS, downloads JS. Each step introduces latency. Server Push aims to collapse these steps by sending resources proactively.

However, the reality is complex. A server doesn’t know for sure if the client already has a cached version of /style.css. If it Pushes /style.css and the client already has it, that’s wasted bandwidth. Browsers have become quite good at optimizing this. The Link header with rel=preload tells the browser "hey, you’re going to need this soon, start fetching it now," but it still leaves the decision to fetch to the browser. It’s a hint, not a command.

The preload directive on the Link header is part of the Fetch API specification and is designed to be a more reliable and controllable way to achieve similar goals to Server Push without the downsides. When a browser sees Link: </style.css>; rel=preload; as=style, it knows it should fetch style.css with high priority, but it respects caching and only fetches if it doesn’t have it. The as=style attribute is crucial; it tells the browser the type of resource so it can prioritize it correctly and apply the right security policies.

The main mental model shift is understanding that Server Push is a server-driven hinting mechanism, whereas Link: preload is a client-driven declaration of need. The server guesses what the client might want. The Link: preload header allows the HTML author (or whoever generates the HTML) to declare what the browser will need. This shift from "server guesses" to "HTML declares" is why Link: preload often performs better.

A key detail is how browsers handle multiple Link: preload headers. If you specify Link: </style.css>; rel=preload; as=style and Link: </script.js>; rel=preload; as=script in your HTML’s <head>, the browser will initiate requests for both concurrently (up to the browser’s connection limit, which is much higher for HTTP/2). It also respects the as attribute to fetch them with appropriate priority and security context. This is far more efficient than the server trying to guess and potentially pushing resources that are already cached.

The one thing most people don’t realize is that the Link: preload header is essentially a way to "inline" the resource-fetching instructions that a server might have tried to push, but with the crucial benefit that the browser is in control and respects its own cache. This makes it a declarative, client-centric optimization, which aligns much better with how modern browsers work. The server’s primary job becomes serving the HTML and the declared resources, not guessing what else might be needed.

The next thing you’ll want to investigate is how to dynamically generate these Link: preload headers based on the actual resources used by a page, rather than hardcoding them.

Want structured learning?

Take the full Http2 course →