HTTP/2 Server Push was deprecated because it often made performance worse by pushing resources the browser didn’t need, leading to wasted bandwidth and increased latency.
Let’s see it in action. Imagine a web server sending an HTML page, and it also proactively sends a CSS file and a JavaScript file it thinks the browser will need.
HTTP/2 200 OK
content-type: text/html
link: </styles.css>; rel=preload; as=style, </script.js>; rel=preload; as=script
... (HTML content) ...
The link: rel=preload header is how a server could indicate it’s pushing these assets. The browser, upon receiving the HTML, would see these headers and start fetching styles.css and script.js immediately, potentially before it even parsed the HTML to know if it actually needed them.
The core problem was that servers lacked the context to make truly informed decisions. A server might push a large JavaScript file, but the browser might decide, after parsing the HTML, that it’s only needed for a specific interactive element that the user hasn’t even triggered yet. This "guesswork" often resulted in the browser having to discard pushed resources, wasting the upstream bandwidth and the time it took to push them. It also meant the browser might start downloading a pushed resource, only to later decide it already had a cached, newer version, leading to redundant downloads.
The most surprising thing about HTTP/2 Server Push was how fundamentally it misunderstood the browser’s role. It treated the browser as a passive recipient of data, rather than an active participant in resource negotiation. This led to a situation where the server, with its limited view, was making suboptimal decisions about what the client actually required at that exact moment.
The primary mechanism for optimizing resource loading in HTTP/2 that replaced Server Push is rel=preload. This directive, placed within the HTML itself, allows the browser to signal to the server (or a CDN) which critical resources it needs as soon as possible.
Consider this snippet within your HTML:
<!DOCTYPE html>
<html>
<head>
<title>Preload Example</title>
<link rel="preload" href="/critical-styles.css" as="style">
<link rel="preload" href="/main-script.js" as="script">
<link rel="stylesheet" href="/critical-styles.css">
</head>
<body>
<h1>Hello!</h1>
<script src="/main-script.js"></script>
</body>
</html>
Here, the <link rel="preload"> tags tell the browser to prioritize fetching critical-styles.css and main-script.js. The as attribute is crucial; it informs the browser about the type of resource being preloaded, allowing it to prioritize the fetch correctly (e.g., a style resource has a higher priority than a font). The browser then uses this information to request these resources immediately, but only those it has explicitly declared as critical via preload. This gives the browser control, leveraging its understanding of the DOM and parsing process to make better decisions.
The server, in this model, acts as a more responsive agent. When it receives a request for /index.html, it sends back the HTML. If the HTML contains rel=preload directives, the browser will immediately issue new requests for those preloaded resources. The server’s job is to fulfill these requests efficiently, perhaps by having them already cached or by serving them quickly from disk. This shift from "pushing proactively" to "responding to preloads" is the core of the change.
The real power of rel=preload is its ability to integrate with browser heuristics and network conditions. Browsers can intelligently decide when to actually start fetching a preloaded resource based on the current page load state, network congestion, and even user interaction. This is a far cry from the server blindly pushing resources that might never be used.
Crucially, rel=preload also allows for specifying crossorigin attributes, enabling the preloading of resources from different origins or CDNs, which was a significant limitation of HTTP/2 Server Push. This flexibility is key for modern, distributed web architectures.
What most people don’t realize is that rel=preload is not just about making requests earlier; it’s also about identifying critical path resources. By explicitly marking what’s needed, developers are forced to analyze their page load dependencies. This analysis often reveals unnecessary JavaScript or CSS that was being loaded indiscriminately, leading to further optimization opportunities beyond just faster loading.
The next step after mastering rel=preload is understanding how to effectively manage resource priorities with fetchpriority for different elements on the page.