1. The Feature That Was Removed
If you came here to configure http2_push, I have news that will save you an afternoon: it doesn't work any more. Chrome disabled HTTP/2 Server Push by default in version 106, in late 2022. Firefox removed it. Nginx dropped the directives entirely in 1.25.1. The feature exists in the HTTP/2 specification and in almost no browser you can reach.
This is unusual. Web platform features rarely die — they get deprecated, warned about, and then linger for a decade. Server Push was killed deliberately, by the browser vendors who had championed it, because years of measurement showed it made things slower about as often as it made them faster.
That's a genuinely interesting failure, and understanding why it failed is the fastest route to using its replacement correctly. Because the replacement — a status code called 103 Early Hints — solves the same problem, and the reason it works is precisely the reason Push didn't.
This article is in three parts. What the http/2 server push feature promised and why it collapsed. What Early Hints does instead, and how to actually deploy it. And then the wider family of resource hints — preload, preconnect, dns-prefetch, prefetch, fetchpriority — which is what most teams should be spending their attention on regardless, because they're where the reliable wins are.
2. What Server Push Promised
The idea was elegant, which is often the problem.
Normally a browser has to read your HTML before it learns that it needs your stylesheet. Request HTML, wait, parse, discover <link rel="stylesheet">, request CSS, wait again. Two round trips before a single pixel can be painted, and on a mobile connection with 120ms of latency that's a quarter of a second spent doing nothing but asking.
Server Push let the server skip the asking. When the browser requested the HTML, the server could say "here's your HTML, and by the way I'm also sending you the CSS and the font you're about to need." Both arrive on the same connection, in parallel with the document. The second round trip disappears.
# This no longer does anything. Included for recognition, not for use.
location = /index.html {
http2_push /css/critical.css;
http2_push /fonts/space-grotesk.woff2;
}
On a cold cache, in a lab, with a well-chosen set of pushed resources, this genuinely worked. Demos were impressive. And then it met the real internet.
3. Why It Failed
Four problems, and the first one is fatal on its own.
The server doesn't know what's in the browser's cache. This is the whole story. Your server decides to push critical.css. The browser already has critical.css, cached from a visit yesterday. The server sends it anyway, because it has no way to know. Bytes go down the wire, competing for bandwidth with the HTML that actually matters, and are then thrown away on arrival.
For a returning visitor — which on a healthy ecommerce site is most of your traffic — Push was frequently a pure regression. You were paying to send files the browser already had, and delaying the ones it didn't. There was an attempt to fix this with a cache digest mechanism, where the browser would tell the server what it held. It was complicated, it never shipped, and the effort was abandoned.
Pushed resources competed with the document. HTTP/2 multiplexes streams over one connection, sharing bandwidth. Push the CSS and the font and two images, and all of them are now contending with the HTML for the same pipe. The HTML arrives later than it would have. You have delayed the thing that unblocks everything else in order to speed up things that come after it, which is exactly backwards.
Nobody could tune it. Getting Push right meant pushing precisely the resources a given visitor needed and no others, which varies by page, by cache state, by viewport, by whether they're logged in. In practice teams configured a static list, over-pushed, and made things worse. The tooling to do better never materialised.
The measured benefit was near zero. Chrome ran the numbers across real traffic at scale and found that the aggregate effect on page load was roughly neutral, with a meaningful tail of sites where it was clearly negative. When the team announced the removal, the honest summary was that almost nobody was using it correctly and the ones who were saw very little for the effort.
I want to be fair to the idea, though, because the diagnosis was right. The two-round-trip problem is real, and it's worth solving. Push just solved it with the wrong actor: it let the server decide what the browser needed, when only the browser knows what it already has.
4. 103 Early Hints, and Why It's Different
Early Hints keeps the timing benefit and gives the decision back to the browser.
Instead of sending files, the server sends an early, informational response — status code 103 — carrying nothing but Link headers. It's a hint: "while I'm putting your page together, you'll probably want these." The browser reads it and decides for itself. If it already has the file cached, it does nothing. If not, it starts fetching immediately, without waiting for the HTML.
HTTP/1.1 103 Early Hints
Link: </css/critical.css>; rel=preload; as=style
Link: </fonts/space-grotesk.woff2>; rel=preload; as=font; crossorigin
Link: <https://cdn.shop.example.com>; rel=preconnect
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
...the actual page...
Two responses to one request. The 103 goes out the instant the server knows what the page will need — before the database queries, before the template renders, before any of the work that produces your Time to First Byte. The 200 follows whenever it's ready.
That gap is where the value lives, and it explains who benefits. If your TTFB is 40ms, there's nothing to fill and Early Hints does almost nothing. If your TTFB is 600ms because a category page is running an expensive faceted query, you have half a second of dead time during which the browser could have been downloading your CSS, your font, and your hero image. That's the win, and it's substantial precisely on the slow pages you most want to fix.
Which leads to an uncomfortable but honest point: Early Hints is most valuable when your server is slow. If you can make the server fast instead, do that first. Early Hints is a way to overlap latency you cannot eliminate, not a substitute for eliminating it.
5. Deploying Early Hints
Support is the awkward part. The browser side is fine — Chrome has supported it since version 103 for navigation requests. The server side is patchier than you'd expect, because sending two responses to one request requires plumbing that a lot of stacks simply don't have.
Cloudflare
The easiest path by a distance. Cloudflare can generate Early Hints automatically: it caches the Link headers from your origin's responses and replays them as a 103 on subsequent requests, before your origin has even been contacted. You emit ordinary Link headers, they handle the rest.
# Your origin just declares the links; the CDN turns them into 103s
add_header Link "</css/critical.css>; rel=preload; as=style" always;
add_header Link "</fonts/space-grotesk.woff2>; rel=preload; as=font; crossorigin" always;
Enable Early Hints in the dashboard under Speed, and verify it's actually firing rather than assuming — see the testing section below, because this is a feature that fails silently.
Fastly and other edge platforms
Fastly supports it through VCL, with explicit control over when the hint goes out. Most modern edge platforms have some equivalent; the pattern is always the same — emit the informational response as early in the request lifecycle as you can, then continue.
Node
Node's HTTP server can write an informational response directly:
import http from 'node:http';
http.createServer((req, res) => {
// Fire the hint before any of the slow work begins
if (req.url === '/' || req.url.startsWith('/category/')) {
res.writeEarlyHints({
link: [
'</css/critical.css>; rel=preload; as=style',
'</fonts/space-grotesk.woff2>; rel=preload; as=font; crossorigin',
'<https://cdn.shop.example.com>; rel=preconnect'
]
});
}
renderPage(req).then(html => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
});
}).listen(3000);
The placement is the whole point. Put writeEarlyHints above the expensive work, not below it. I have reviewed code that computed the hint list from the rendered page and then sent it, which is a lot of machinery to send a hint at the exact moment it stops being useful.
Nginx and Apache
Neither supports sending 103 natively at time of writing. Nginx has no directive for it; the workable options are to put an edge platform in front, or to have your application emit it if your application server can. This is the most common blocker I run into, and it's usually what decides whether a team does Early Hints at all.
Magento and Shopify
Magento's response pipeline doesn't expose informational responses, so on a standard stack this comes from the CDN layer. Given most Magento stores sit behind Cloudflare or Fastly already, that's not much of a limitation — enable it there and emit Link headers from the application for the CDN to pick up.
Shopify handles it at the platform level and you don't configure it. Your leverage on Shopify is the resource hints in your theme's <head>, covered next.
6. The Hints You Should Actually Be Using
Early Hints is a delivery mechanism for hints. The hints themselves work perfectly well in your HTML, are supported everywhere, and are where the reliable wins are for most sites. If you take one thing from this article, take this section.
preconnect
Opens a connection — DNS, TCP, TLS — to a host you're about to use, before you use it. On a mobile connection that handshake is 200–400ms, and preconnect moves it off the critical path.
<link rel="preconnect" href="https://cdn.shop.example.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
This is the highest-value, lowest-risk hint on the list, and it's the one most sites under-use. The rules: only for origins you will definitely use, only for the two or three that matter most, and always with crossorigin for fonts. That last one catches everybody — a font request is made in anonymous CORS mode, so a preconnect without crossorigin opens a connection the font request cannot reuse. You get two connections and zero benefit.
Don't preconnect to more than about four hosts. Each one costs a connection and some radio power on mobile, and speculative connections that go unused are pure waste. There is more on the economics of that budget — and on why the real fix is usually fewer third-party origins — in the connection-cost guide.
dns-prefetch
The cheap cousin: resolves DNS only, no connection. Useful for origins you'll probably need later but not immediately, and as a fallback for very old browsers.
<link rel="preconnect" href="https://analytics.example.net" />
<link rel="dns-prefetch" href="https://analytics.example.net" />
Pairing them like that is a common pattern and is harmless. In practice, if a host is worth hinting at all, preconnect is usually the one you want.
preload
Says "fetch this now, at high priority, I will definitely need it." The workhorse, and the one most often misused.
<link rel="preload" href="/fonts/space-grotesk.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/img/hero-desktop.avif" as="image" fetchpriority="high"
media="(min-width: 768px)" />
The as attribute is mandatory in practice. Without it the browser can't determine the priority or the request mode, and it will fetch the resource twice — once for your preload, once for the real request. That's the "resource was preloaded but not used" console warning, and it means you have made the page slower, not faster.
Fonts need crossorigin even when same-origin, for the same anonymous-mode reason as preconnect. Get this wrong and you download the font twice.
Preload two or three things at most. Every preload competes with everything else for bandwidth, so preloading ten resources means the browser fetches all ten at high priority and none of them arrive early. The discipline here is genuinely hard: the temptation is to preload everything important, and the effect of preloading everything is identical to preloading nothing, with extra steps.
prefetch
For the next navigation, not this one. Low priority, fetched during idle time, stored for a subsequent page.
<!-- On a product page: the customer's likely next step -->
<link rel="prefetch" href="/checkout/cart" />
Genuinely useful in a funnel where the next step is predictable — cart to checkout, for instance. Wasteful on a category page where the visitor might click any of forty products. And be careful on mobile data: you are spending someone else's bandwidth on a guess.
Speculation Rules
The modern replacement for the old rel="prerender", and considerably more capable. It lets you declare, in JSON, which links should be prefetched or fully prerendered, with conditions:
<script type="speculationrules">
{
"prerender": [{
"where": { "selector_matches": ".product-card a" },
"eagerness": "moderate"
}],
"prefetch": [{
"where": { "href_matches": "/checkout*" },
"eagerness": "conservative"
}]
}
</script>
eagerness is the control that matters: conservative acts on pointer-down, moderate on hover, eager immediately. A prerendered page loads instantly on click, which is a genuinely dramatic effect — and it also runs your analytics, fires your tags, and executes your JavaScript for a page the visitor may never visit. Check the Page Visibility API before counting a pageview, or your analytics will inflate.
Browser support is Chromium-only for prerender at present, so treat it as an enhancement for a slice of your traffic rather than a strategy.
fetchpriority
Not a hint in the <link> sense, but the same family of idea: adjust the priority of a request the browser was going to make anyway.
<!-- The LCP image: tell the browser it matters -->
<img src="/img/hero.avif" fetchpriority="high" width="1200" height="800" alt="..." />
<!-- A carousel's third slide: it does not -->
<img src="/img/slide-3.avif" fetchpriority="low" loading="lazy" width="800" height="600" alt="..." />
On an ecommerce product page this is frequently the single highest-value change available. Browsers assign images a low initial priority until layout tells them what's in the viewport, which means your hero image — the LCP element — starts downloading later than the below-the-fold thumbnails that happened to appear earlier in the markup. One attribute fixes it, and I have seen it move LCP by 300–500ms on image-heavy pages.
7. How the Browser Decides What Matters
Every hint in this article is an adjustment to a priority decision the browser was already making. It helps enormously to know what those default decisions are, because half the time the right move is to leave them alone.
Roughly, and simplifying across engines: the HTML document is highest. Stylesheets in the head come next, because nothing can paint until they arrive. Synchronous scripts in the head sit alongside them. Fonts are high but only once CSS has revealed that they're needed. Images start low — and this is the detail that catches people — until layout determines they're in the viewport, at which point they get promoted. Asynchronous and deferred scripts, prefetches, and anything below the fold sit at the bottom.
Two consequences follow from that list, and between them they explain most performance work on a storefront.
First, images start low. Your hero image, the thing Largest Contentful Paint is measured against, begins life in the same bucket as a footer icon. The promotion to high priority happens after layout, which happens after CSS, which is exactly the delay you were trying to avoid. fetchpriority="high" short-circuits that, and it is why one attribute can move LCP by half a second.
Second, fonts are discovered late. A font is referenced inside a @font-face rule inside a stylesheet. The browser must download the CSS, parse it, apply it, determine that some text on the page uses that family, and only then request the font. That's three sequential steps after the HTML arrives. Preloading fonts is one of the few cases where you reliably know something the preload scanner cannot.
You can watch all of this rather than taking my word for it. In DevTools, right-click the Network panel's column headers and enable Priority. Then reload a product page and read the order. What you want to see is: document, CSS, font, hero image, everything else. What you usually see on an untuned storefront is: document, CSS, six thumbnails, a chat widget, an analytics bundle, and the hero image somewhere in the middle.
One more subtlety worth knowing. Priority is not a queue position, it's a bandwidth share. A low-priority request doesn't wait for high-priority ones to finish; it gets a thinner slice of the pipe. That's why over-preloading is corrosive rather than merely useless — you aren't reordering a queue, you're dividing a fixed amount of bandwidth into more equal parts, which is precisely the opposite of prioritisation.
8. Choosing Between Them
| Situation | Use | Why |
|---|---|---|
| Third-party origin you'll definitely hit | preconnect | Removes handshake from critical path |
| Origin you might hit later | dns-prefetch | Cheap, no connection held open |
| Font used above the fold | preload + crossorigin | Fonts are discovered late, inside CSS |
| LCP image | fetchpriority="high" | Beats the default low image priority |
| CSS discovered by an import | preload as=style | Parser can't see it until too late |
| Predictable next page | prefetch or Speculation Rules | Uses idle time, not critical path |
| Slow TTFB you can't fix | Early Hints | Fills server think-time with useful work |
| Anything else | nothing | The browser's heuristics are good |
That last row is the one I'd underline. The browser's preload scanner is genuinely sophisticated — it parses ahead of the main thread specifically to discover resources early. Most hints you add are telling it something it already worked out, at the cost of contending with its own priority decisions. Hints help when the browser cannot discover a resource by scanning your HTML: fonts referenced inside CSS, images set by JavaScript, stylesheets pulled in by @import, anything behind a redirect.
9. What Goes Wrong
Preloading things you don't use. The console tells you plainly — "was preloaded using link preload but not used within a few seconds" — and it's worth treating as an error rather than a warning. Usually a leftover from a redesign, or a preload with a mismatched as that caused a second, separate fetch.
Preloading everything. The instinct is that if one preload helps, five help more. Priority is zero-sum: elevating everything is identical to elevating nothing, except you've also removed the browser's ability to sequence sensibly. Pick the two resources that block your first paint. Stop.
Missing crossorigin on fonts. Downloads the font twice and slows the page. So common that if a site has font preloads at all, I check this before anything else.
Hints for resources that aren't on the critical path. Preconnecting to your review widget's CDN, or preloading a script that's already deferred, spends critical-path bandwidth on something that by definition isn't urgent.
Hints that outlive their page. A preload in a shared header template fires on every page, including the ones that don't use the resource. Scope hints to the templates that need them — this is the most common source of the "preloaded but not used" warning on large sites.
Assuming Early Hints is working. It fails silently and completely: no error, no warning, just no 103. Verify it rather than believing the dashboard toggle.
10. The Preload Scanner, and How Sites Break It
Browsers run a secondary parser called the preload scanner. While the main parser is blocked — waiting on a script, executing JavaScript, doing layout — the scanner races ahead through the raw HTML looking for URLs to start fetching. It's one of the most valuable optimisations in the browser and it is entirely invisible until you break it.
It only sees markup. Anything a resource depends on being computed is invisible to it, and that's where sites lose seconds without noticing.
Images set by JavaScript. A carousel that stores its slides in data-src and assigns src after hydration means the scanner sees nothing to fetch. Your hero image cannot begin downloading until your JavaScript bundle has downloaded, parsed, and executed. On a mid-range Android that's easily two seconds of nothing happening. If the first slide is your LCP element, put a real src on it in the HTML and let the script take over from there.
CSS @import. An import inside a stylesheet is only discovered after that stylesheet has been fetched and parsed, so imported CSS is serialised behind its parent. Two stylesheets chained this way cost two round trips where one would have done. Flatten them at build time, or preload the imported file.
Client-side rendered pages. If the HTML is an empty <div id="root">, the scanner has nothing whatsoever to work with. Every resource on the page is discovered only after the framework boots. This is the largest single reason SPA storefronts struggle with LCP, and no arrangement of hints fixes it — the fix is server-side rendering, or at minimum putting the critical image and font references into the initial HTML.
Fonts loaded by script. Any font loading library that injects @font-face at runtime is invisible to the scanner by construction. Prefer plain CSS with font-display: swap and a preload.
The diagnostic is straightforward. View source — actual source, not the DevTools Elements panel, which shows you the DOM after JavaScript has run. If your hero image's URL doesn't appear in the raw HTML, the scanner never saw it, and that's your problem rather than anything to do with hints.
# Does the critical image exist in the HTML the server sent?
curl -sS https://shop.example.com/product/example | grep -o 'src="[^"]*hero[^"]*"'
# Compare with what the rendered DOM contains — a difference is the scanner's blind spot
11. Testing What's Actually Happening
Curl will show you the informational response if you ask for it, though you need a recent version and HTTP/2:
# Look for a 103 before the 200
curl -sSv --http2 https://shop.example.com/ 2>&1 | grep -E '< HTTP|< link'
# Full picture including the response ordering
curl -sS --http2 -D - -o /dev/null https://shop.example.com/
In Chrome DevTools, the Network panel shows an "Early Hints headers" section on the document request when a 103 arrived, and resources fetched because of one are attributed accordingly. If that section is absent, no 103 was sent, whatever your CDN dashboard claims.
For the hints themselves, sort the Network panel by the Priority column. That single view answers most questions: is the LCP image high priority, is the font being fetched twice, is something below the fold competing with the hero. Turn the column on if it isn't already — it's hidden by default and it's the most useful column there.
Then measure the thing you actually care about:
// LCP, with the element that produced it — paste in the console
new PerformanceObserver((list) => {
const e = list.getEntries().at(-1);
console.log('LCP', Math.round(e.startTime), 'ms —', e.element?.tagName, e.url || '');
}).observe({ type: 'largest-contentful-paint', buffered: true });
// Where the time actually went before the document arrived
const nav = performance.getEntriesByType('navigation')[0];
console.table({
dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
tcp: Math.round(nav.connectEnd - nav.connectStart),
tls: nav.secureConnectionStart ? Math.round(nav.connectEnd - nav.secureConnectionStart) : 0,
ttfb: Math.round(nav.responseStart - nav.requestStart),
download: Math.round(nav.responseEnd - nav.responseStart)
});
Run that before and after any change. Lab numbers vary enough between runs that a single comparison proves nothing — take five of each and compare medians, and be sceptical of anything under about 50ms of difference.
12. Where This Lands on a Storefront
Product detail pages. The main product image is almost always the LCP element, and almost always starts at low priority. fetchpriority="high" on it, plus a preload with media attributes if the image differs by breakpoint. This is the single change I'd make first on any PDP.
Category and listing pages. Harder, because the LCP element depends on where the fold lands and what the grid looks like. Give high priority to the first row of product images and lazy-load the rest — and check what your platform is actually emitting, because Magento and most Shopify themes lazy-load everything by default, including the images above the fold. Lazy-loading your LCP image is a reliable way to make it 400ms slower.
Checkout. Preconnect to your payment provider's domain. The Stripe or Adyen handshake happens partway through the page's life and it's a full TLS negotiation to a third party. Opening that connection early is free and removes a stall from a page where stalls cost money directly.
Search results. If search is powered by a third-party service, preconnect to it from the header on every page. The customer who is going to search will search early, and the connection will be waiting.
13. A Worked Example
An upholstery retailer, Magento 2 behind Cloudflare, mobile LCP sitting at 4.1 seconds on product pages — comfortably in the red. They'd asked about Server Push, which is how this conversation usually starts.
What the waterfall showed. TTFB of 780ms on an uncached product page. The hero image was requested 1.9 seconds in, at low priority, behind six thumbnail images that appeared earlier in the DOM. Two web fonts were discovered inside the stylesheet and started downloading at 2.3 seconds. Four third-party origins were contacted with no preconnect, each costing a full handshake.
What we changed, and what each was worth.
fetchpriority="high" on the main product image, and removing loading="lazy" from it — the theme had applied lazy loading globally. LCP improved by roughly 600ms. One line of template change, and by a distance the best return of the whole exercise.
Preloading the two fonts with crossorigin, and cutting from four font files to two by dropping weights nobody used. Around 250ms off the point at which text stopped shifting, and a modest CLS improvement.
Preconnect to the CDN and the review widget's origin. About 180ms, mostly on mobile where the handshakes were slowest.
Early Hints via Cloudflare, carrying the font preloads and the CDN preconnect. Around 200ms further — and here the 780ms TTFB was the reason it paid, because there was a genuine window of server think-time to fill. On their much faster category pages, the same change measured as noise.
Where it ended. Mobile LCP at 2.4 seconds, which is inside the good threshold with a little room. Total engineering time was under two days, and most of it went on the font audit rather than anything discussed in this article.
What I'd flag. The TTFB of 780ms was the real problem, and we treated the symptom. Early Hints let us overlap it rather than fix it. A month later they put full-page caching in front of product pages and TTFB dropped to 90ms — at which point the Early Hints benefit largely evaporated, because there was no longer any dead time to fill. That's not an argument against having done it. It is an argument for being honest about what it's doing: Early Hints is a way to spend latency you're stuck with, and if you later stop being stuck with it, the hint stops mattering.
14. The Thing That Beats Every Hint Here
Before spending a sprint on hints, spend an hour on cache headers. A resource served from the browser's cache takes zero milliseconds and zero bytes, which no amount of clever prioritisation can match.
The pattern for anything with a content hash in its filename:
location ~* \.(css|js|woff2|avif|webp|jpg|png|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location ~* \.html$ {
add_header Cache-Control "public, max-age=0, must-revalidate" always;
}
immutable is the underrated part. Without it, a browser will still revalidate a cached file when the user hits reload, sending a conditional request and waiting for a 304 — a full round trip to learn nothing. immutable tells it not to bother. It only makes sense with hashed filenames, but if your build produces those, it's free.
The corollary is that your HTML must not be cached aggressively, or customers get stale pages referencing bundles that no longer exist. Short or zero max-age on documents, long and immutable on hashed assets. Most sites I audit have this exactly inverted somewhere — typically an over-broad CDN rule that caught HTML along with everything else.
Then, for the server side: if your TTFB is high because the application is doing real work on every request, the answer is usually full-page caching rather than anything in this article. Varnish, Fastly, or Cloudflare's cache-everything with sensible bypass rules for cart and checkout. Getting a product page from 780ms to 90ms does more for real users than every hint here combined, and it makes half of them unnecessary.
I put this section near the end deliberately, because it's the least interesting advice and the most effective. Hints reorder work the browser must do. Caching removes the work entirely. Do the removal first and then optimise whatever's left.
15. Does HTTP/3 Change Any of This?
Not really, and it's worth saying because the question always comes.
HTTP/3 runs over QUIC instead of TCP, which removes head-of-line blocking at the transport layer and makes connection setup faster — often zero round trips when resuming. Those are real improvements and you should be on it, which mostly means enabling it at your CDN and moving on.
But it doesn't change the shape of the problem this article is about. The browser still can't request your CSS until it knows the CSS exists. Early Hints still fills server think-time. fetchpriority still tells the browser which image matters. HTTP/3 makes each request cheaper; it doesn't make requests happen sooner.
And Server Push technically exists in the HTTP/3 specification too, and is likewise unimplemented in browsers. The idea is not coming back.
16. Questions That Come Up
"Should I still configure Server Push for old browsers?" No. The browsers that support it are a rounding error, the configuration is a maintenance liability, and Nginx has removed the directives so it won't survive your next upgrade anyway. Delete it if you find it.
"Is Early Hints worth it if we're already fast?" Probably not. If your TTFB is under about 200ms there's little dead time to fill, and the effort is better spent elsewhere. Measure your TTFB first; that number tells you whether to bother.
"How many preconnects is too many?" Beyond three or four you're usually losing more than you gain. Each speculative connection costs setup work and, on mobile, radio power. Rank your third-party origins by how early and how certainly they're used, and hint the top few.
"Does preload help scripts?" Occasionally — for a script discovered late, or one loaded by another script. For an ordinary <script defer> in your head, the preload scanner already found it and you're adding nothing. Check the Priority column before and after; if nothing moves, remove the preload.
"We added hints and nothing improved." Most likely the resources weren't on the critical path, or your bottleneck is elsewhere — a slow TTFB, render-blocking JavaScript, an over-large main thread task. Hints reorder work; they don't reduce it. If the page is slow because it's doing too much, hints will not save you.
"Can Early Hints break anything?" The realistic risk is hinting resources that don't exist or have moved, so browsers fetch 404s at high priority. That happens when hint headers are hardcoded and the asset filenames are hashed by a build. Generate them from your manifest, or you'll ship stale hints the first time you change a bundle.
17. What I'd Actually Do First
If you arrived looking for Server Push and you're now wondering what to do with the afternoon you'd set aside, here is the order I'd work in.
Open a product page in DevTools, turn on the Priority column, and look at when the LCP image is requested and at what priority. On most storefronts this reveals a genuine problem within thirty seconds, and one attribute fixes it.
Then audit your fonts. Count the files, count the weights you actually use, add crossorigin to any preload that's missing it. Font handling is wrong on more sites than any other item in this article.
Then add two or three preconnects for the third-party origins your page genuinely depends on early.
Then, and only then, look at Early Hints — and look at your TTFB first, because that number decides whether it's worth the plumbing. If TTFB is high, ask whether you can cache the page instead. Fixing the server is better than overlapping the wait.
One last piece of advice about how to hold all of this. Resist the urge to add every hint at once and then measure. You will not be able to attribute the result, and when something regresses you'll have four suspects. Ship one change, measure it over a few days of real traffic rather than a handful of lab runs, and either keep it or take it out. Performance work accumulates through a series of small, attributed wins — and a hint you cannot justify is a hint that will still be in your templates in three years, quietly competing for bandwidth with whatever actually matters by then.
Server Push failed because it tried to be clever on behalf of a browser that knew more than it did. The hints that survived are the ones that tell the browser something true and then get out of the way. That's a reasonable principle to apply to the rest of your performance work as well.