./ahmedhashim

Early Hints

The browser can begin requesting resources before the final HTML arrives. You can do this with a Link header on your response. However, if your application is taking a while to render the page, you can use 103 Early Hints.

This will allow you to preload and preconnect to resources as soon as the request is made, while the application is still building the page. Here’s an example of how I did this with my Caddy reverse proxy.

Here’s the flow with the normal Link header being returned on the 200 OK response:

  sequenceDiagram
    participant B as Browser
    participant C as Caddy
    participant A as Backend

    B->>C: Request page
    C->>A: Forward request
    Note over A: DB queries, template rendering,<br/>other middleware work
    A-->>C: 200 OK + Link headers + HTML
    C-->>B: 200 OK + Link headers + HTML
    Note over B: Browser sees Link headers,<br/>then starts CSS/JS/preconnect
    Note over B: Browser parses HTML and renders

The browser will wait for these headers to be returned before making the requests. So if your handler takes 50ms to return your page, the browser isn’t doing anything for that 50ms.

Early hints

By returning a 103 Early Hints response, you can send the Link headers sooner:

  sequenceDiagram
    participant B as Browser
    participant C as Caddy
    participant A as Backend

    B->>C: Request page
    C->>A: Forward request
    A-->>C: 103 Early Hints + Link headers
    C-->>B: 103 Early Hints + Link headers
    Note over B: Browser immediately starts CSS/JS preload<br/>and preconnect work
    Note over A: Backend continues DB queries,<br/>template rendering, etc.
    A-->>C: 200 OK + HTML
    C-->>B: 200 OK + HTML

This allows the browser to make those requests while your application is still rendering your page.

Early hints have decent support across browsers, but that support is a bit uneven. Chrome and Edge have supported both types since 103, and Firefox has supported preload since 123 earlier this year. Safari 17, however, only supports preconnect.

Browsers also ignore early hints on HTTP/1.1 connections, but that’s not a problem behind Caddy since it serves HTTP/2 and 3 by default.

Implementation

You can also tell Caddy to issue a 103 itself by using the header and respond 103 directives, and it’ll pass along any 103 from your application. You could statically define early hints in your Caddyfile, but you’ll quickly run into problems when your assets change per release. I wanted to append a version to each of my assets so that they would cache bust. Since only my application knows the current version, I decided to put them in a middleware.

Something like this will work using the Go standard library:

func earlyHints(next http.Handler) http.Handler {
	v := getAssetsVersion()
	linkHeader := "<https://assets.example.com>; rel=preconnect, " +
		"<https://media.example.com>; rel=preconnect, " +
		"</app.css?v=" + v + ">; rel=preload; as=style, " +
		"</app.js?v=" + v + ">; rel=preload; as=script"

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if isAssetRequest(r.URL.Path) {
			next.ServeHTTP(w, r)
			return
		}

		w.Header().Set("Link", linkHeader)
		w.WriteHeader(http.StatusEarlyHints)
		next.ServeHTTP(w, r)
	})
}

Make sure your middleware runs before your handler starts its expensive work. That’s what allows your preconnect and preload hints to begin loading while your handler is generating the response.

Once deployed, you should see the 103 in your devtools network panel:

devtools

Why it helps

You only gain the time that your backend takes to render the page. If your backend takes 50ms to generate a page, you’ll be 50ms ahead in being able to preload CSS and JS, as well as connect to your asset and image domains.

You’ll most likely see the biggest improvement from preconnect, since the browser can open connections to your other domains early. Connecting to a domain can take tens of milliseconds to resolve the DNS and establish a TLS handshake. If your users are on mobile or have higher latency, you could be looking at much longer. So if the browser is able to connect while your backend is generating a page, you’ll already have those connections open when the HTML calls for images or fonts.

You can also use preload, which will benefit slower networks. You probably won’t see the difference if you have a fast internet connection, but if your user is on their phone with a weak signal, you’ll see your page render sooner.