Skip to main content
fast frontend

Server Push, Early Hints, and Priority Signals

6 min read Chapter 20 of 33

Server Push, Early Hints, and Priority Signals

Server Push: The Good Idea That Failed

HTTP/2 Server Push allows the server to send resources to the browser before the browser requests them. The server sends the HTML response and proactively pushes the CSS and JavaScript that it knows the browser will need. Zero round trips wasted on discovery.

In theory, this eliminates the sequential dependency: HTML → parse → discover CSS → request CSS → receive CSS. With Server Push: HTML + CSS arrive simultaneously.

In practice, Server Push failed:

  1. Cache invalidation: The server pushes resources that the browser may already have cached. There is no mechanism for the browser to tell the server “I already have this file” before the push begins. The server wastes bandwidth pushing cached resources.

  2. Priority conflicts: Pushed resources compete with the HTML response for bandwidth on the same connection. The browser has no control over the priority of pushed resources. Pushing a large JavaScript bundle can delay the HTML document’s delivery.

  3. CDN incompatibility: Many CDNs do not support Server Push, or support it poorly. The push happens from the CDN edge, which may not know which resources to push.

Chrome removed Server Push support. It is effectively dead.

103 Early Hints: The Replacement That Works

103 Early Hints is an informational HTTP response sent before the final response. The server sends a 103 response with Link: <preload> headers, then continues processing the request and sends the final 200 response.

The browser receives the 103 response and begins fetching the preloaded resources while waiting for the server to generate the final HTML response. This overlaps server processing time with resource fetching.

# Server response sequence:

HTTP/1.1 103 Early Hints
Link: </assets/main.css>; rel=preload; as=style
Link: </assets/vendor.js>; rel=preload; as=script
Link: </fonts/custom-sans.woff2>; rel=preload; as=font; crossorigin

# ... server processes the request (200ms for database queries, SSR, etc.) ...

HTTP/1.1 200 OK
Content-Type: text/html
<html>...

The browser starts fetching main.css, vendor.js, and the font file 200ms earlier than it would without Early Hints. This 200ms is the server processing time that would otherwise be wasted from the browser’s perspective.

Nginx configuration:

location / {
    # Send 103 Early Hints before proxying to the application server
    add_header Link "</assets/main.css>; rel=preload; as=style" early;
    add_header Link "</assets/vendor.js>; rel=preload; as=script" early;
    add_header Link "</fonts/custom-sans.woff2>; rel=preload; as=font; crossorigin" early;

    proxy_pass http://app:3000;
}

Unlike Server Push, Early Hints does not send the resource data. It sends a hint that the browser uses to initiate its own requests. The browser’s cache handles deduplication naturally: if the resource is cached, the browser skips the fetch. No wasted bandwidth.

On the e-commerce platform with a 200ms server-side rendering time:

MetricWithout Early HintsWith Early HintsDelta
CSS fetch start220ms (after HTML received)20ms (during server processing)-200ms
Font fetch start260ms (after CSS parsed)20ms (during server processing)-240ms
FCP1,200ms980ms-220ms
LCP3.4s3.1s-300ms

The FCP improvement of 220ms comes from the CSS arriving 200ms earlier. The LCP improvement of 300ms includes the font arriving earlier, which prevents a font-swap delay on pages where text is the LCP element.

Priority Hints: fetchpriority

The browser assigns default priorities to resources based on their type and position in the document. Images are low priority during initial load. Scripts are high priority if they are render-blocking. These defaults are usually correct, but for specific resources, they are wrong.

The LCP image on the product detail page is an <img> tag in the body. The browser assigns it low priority during initial load because it discovers the image after higher-priority resources (CSS, JS). By the time the browser raises the image’s priority (after layout determines it is above the fold), several seconds of download time have been lost to lower-priority resources.

<!-- SLOW: Default priority - browser assigns low priority to images -->
<img
  src="/images/product-main.avif"
  alt="Product photo"
  width="800"
  height="533"
  loading="eager"
/>

<!-- FAST: High priority hint - browser downloads this image first -->
<img
  src="/images/product-main.avif"
  alt="Product photo"
  width="800"
  height="533"
  loading="eager"
  fetchpriority="high"
/>

The fetchpriority="high" attribute tells the browser to treat this image with the same priority as render-blocking CSS. The browser begins downloading it alongside CSS rather than after CSS completes.

For non-critical resources, fetchpriority="low" deprioritizes them:

<!-- Below-fold product images: deprioritize to free bandwidth for LCP image -->
<img
  src="/images/product-thumb-2.avif"
  alt="Thumbnail"
  width="200"
  height="200"
  loading="lazy"
  fetchpriority="low"
/>

For fetch() requests:

// SLOW: Analytics beacon competes with critical API calls
fetch("/api/analytics", { method: "POST", body: analyticsData });

// FAST: Analytics deprioritized
fetch("/api/analytics", {
  method: "POST",
  body: analyticsData,
  priority: "low",
} as RequestInit);

// FAST: Product data prioritized
fetch("/api/product/sku-001", {
  priority: "high",
} as RequestInit);

On the e-commerce product detail page:

MetricDefault PriorityWith fetchpriorityDelta
LCP image download start680ms220ms-460ms
LCP3.1s2.5s-600ms
Analytics beacon latency180ms450ms+270ms (acceptable)

The LCP image starts downloading 460ms earlier. The analytics beacon is delayed by 270ms, which has zero user-facing impact because analytics data is not displayed to the user.

The combined effect of Early Hints + fetchpriority on the product detail page:

MetricBaseline+ Early Hints+ fetchpriorityCombined Delta
LCP3.4s3.1s2.5s-900ms

900ms of LCP improvement from two configuration changes. No code changes. No refactoring. The CI Lighthouse gate from Chapter 2 validates these improvements and detects regressions if the Early Hints configuration is accidentally removed during a server deployment.

The Trade-off

103 Early Hints requires the server to know which resources are critical before generating the response. If the critical resources change between pages (different CSS bundles for different routes), the Early Hints configuration must be per-route. A static Early Hints configuration that lists all resources for all routes will hint resources that the page does not use, wasting browser connection capacity on speculative fetches.

For the e-commerce platform, the critical resources (main CSS, vendor JS, primary font) are shared across all pages. Per-route resources (route-specific JS chunks) are not included in Early Hints because they vary.

fetchpriority support: Chrome, Edge, and Opera support it. Firefox and Safari have partial support. On browsers that do not support the attribute, it is silently ignored and the default priority is used. There is no degradation, just no improvement.