Early Hints (103)

Speed up first paint with HTTP 103 Early Hints via the Link/preload header convention

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

What Early Hints do

HTTP 103 Early Hints (RFC 8297) is an interim response the server can send before the final response while it is still computing the body. The browser uses it to start preloading or preconnecting to the assets the page will need — CSS, JS, fonts, third-party origins — so those fetches overlap with your server's render time instead of waiting for the first byte of HTML.

Reach for Early Hints when first-byte is slow but the page's static assets are known up front — a Suspense dashboard, a Stream page, or any route where the shell render is delayed. You opt in by settingLinkpreload headers on the response; Chirp does the rest.

Chirp has no special return type or config flag for Early Hints. The lever is a header convention: set asset-preload-classLinkheaders on the response, and Chirp's sender automatically emits a preliminary103frame carrying those headers before the final response. The sameLinkheaders also remain on the final response (the 103 hint is advisory; the canonicalLinkheader still belongs on the final message).

Link headers live on a Response. The chainable .with_header()method returns a newResponsewith the header appended:

from chirp import Response

# A non-preload Link silently fires no hint; rel=preload does.
response = Response("<html>...</html>").with_header(
    "Link", "</static/app.css>; rel=preload; as=style"
)

Handlers return render intents like Page, Suspense, or Stream — not a Responseyou build by hand — so the idiomatic place to attach preload headers is response-transforming middleware, which sees the renderedResponseafter the handler runs:

from chirp import App, Request, Response, Suspense
from chirp.middleware.protocol import Next

app = App()

async def preload(request: Request, next: Next) -> Response:
    response = await next(request)
    if isinstance(response, Response):
        return response.with_header(
            "Link", "</static/app.css>; rel=preload; as=style"
        )
    return response

app.add_middleware(preload)

async def load_stats() -> dict[str, int]:
    ...  # slow query; deferred so the shell paints first

@app.route("/dashboard")
async def dashboard(request: Request) -> Suspense:
    return Suspense("dashboard.html", stats=load_stats())

Multiple preloads use repeated .with_header("Link", ...)calls — each value is a separateLinkheader:

return (
    response
    .with_header("Link", "</static/app.css>; rel=preload; as=style")
    .with_header("Link", "</static/app.js>; rel=modulepreload")
    .with_header("Link", "<https://cdn.example.com>; rel=preconnect")
)

Which rel=values trigger a hint

Only asset-hint relations are promoted to the 103 frame:

rel= Use for
preload Stylesheets, fonts, images this page needs
modulepreload ES modules
preconnect Warm up a connection to a third-party origin
dns-prefetch Resolve DNS for a third-party origin
prefetch Fetch a likely next resource
prerender Pre-render a likely next page

A singleLinkheader may carry more than one relation (rel="preconnect dns-prefetch"); the hint fires if any token is asset-class.

Early Hints vs Speculation Rules

These two features look similar but operate at different layers and times, and they compose cleanly:

Early Hints (103) Speculation Rules
Target Speculation Rules">The current response The next navigation
Transport Speculation Rules">Interim HTTP frame before the body <script type="speculationrules">in the page body
Speeds up Speculation Rules">First paint of this page The user's next click

Use Early Hints to accelerate the page being served; use Speculation Rules to accelerate where the user goes next. Neither shares plumbing with the other.

Verifying

In a browser, the DevTools Network panel shows the early preloads starting before the document response completes.