Skip to content

feat(server): add an earlyHints option for 103 Early Hints - #2350

Open
lxsmnsyc wants to merge 1 commit into
mainfrom
feat/early-hints
Open

lxsmnsyc wants to merge 1 commit into
mainfrom
feat/early-hints

Conversation

@lxsmnsyc

Copy link
Copy Markdown
Member

Closes #1935.

Adds an opt-in earlyHints option. With it on, SolidStart sends the page's assets as 103 Early Hints, so the browser can start downloading them while the server is still rendering.

// vite.config.ts
solidStart({ earlyHints: true });

Change

  • New server/early-hints.ts. sendEarlyHints(event, assets) converts the page's <link> assets to Link values and passes them to h3's writeEarlyHints. On Node, that writes a real 103 response. On other runtimes, h3 adds the same values as Link headers on the final response, which is the fallback the issue asked for.
    • Stylesheets become rel=preload; as=style and scripts rel=modulepreload, matching the page's own <link> tags.
    • URLs are passed through encodeURI, since header values must be ASCII and a chunk name can contain any character a route file name can.
    • It does nothing outside production, with the option off, or once the response headers are sent.
  • Entry assets (client entry, app entry, merged CSS) are sent from createPageEvent, before rendering starts.
  • Route assets are sent from the server-side lazy() wrapper in shared/lazy.ts, in the component it returns. That code runs on every render. My first prototype hooked the module-level part, which lazy() caches, so route hints only went out on the first request after the server started.
  • Off by default, like Nuxt's experimental.writeEarlyHints, because some proxies and clients mishandle 1xx responses.

Testing

Production builds of the fixtures, with a raw Node HTTP client that logs each 1xx response (information event) and the final one, in ms from the request:

Setup Responses for /about (with a route CSS file added)
basic, Node, earlyHints: true, stream mode 103 entry CSS + 3 entry JS (7ms), 103 about.css + about.js (11ms), 200 (15ms)
basic, Node, earlyHints: true, mode: "async" both 103s, then 200
basic, Node, option off 200 only
basic, Bun (NITRO_PRESET=bun), earlyHints: true no 103; 200 with a Link header holding all 6 values (entry and route)
experiments, Node, /漢字 103 with the entry assets, no errors
  • In stream mode the route hint still arrives before the final headers. Once a route module is loaded, its component renders synchronously, before the handler returns the stream.
  • The hints match each page's own <link> tags. For /漢字, the page itself has no route-level <link> for its chunk (漢字-….js exists in the build), so no route hint is sent either. That looks like an existing issue with non-ASCII route ids in the asset lookup, separate from this PR.
  • The client bundle contains no hint code.
  • New early-hints.spec.ts (6 tests): the Link values, including encoding a non-ASCII URL, and skipping <style> and non-preload links. Also, that hints are sent in production with the option on, and that nothing is sent with the option off, outside production, after the headers are sent, or with nothing to hint.
  • @solidjs/start test:ci (246 tests), typecheck, validate-imports and build pass. apps/tests unit tests (6), Playwright (42), bundled-dev Playwright (42) and e2e:base (3) pass.

Expected benefit

On pages that stream quickly, the 103s arrive only milliseconds before the shell, whose <link rel="modulepreload"> tags the browser would find almost as soon. The gain is on pages that take a while before the response, such as mode: "async", deferStream, or slow middleware: there the browser can fetch the assets while the server is still working.

A matching PR for the 1.x branch follows.

🤖 Generated with Claude Code

Send the page's assets as 103 Early Hints before the response, using h3's
writeEarlyHints. On Node that writes a real 103 response; on other
runtimes h3 adds the same values as Link headers on the response.

- Entry assets (client entry, app entry, merged CSS) are sent from
  createPageEvent, before rendering starts.
- Route assets are sent from the server lazy() wrapper when a route
  renders. That code runs on every render, unlike the module-level part
  that lazy() caches.
- Nothing is sent once the response headers are out.
- Stylesheets become rel=preload; as=style and scripts rel=modulepreload,
  matching the page's own <link> tags. URLs are encodeURI'd, since header
  values must be ASCII.

Off by default, like Nuxt's experimental.writeEarlyHints, because some
proxies and clients mishandle 1xx responses. Production builds only.

Closes #1935.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf2ab2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/start Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for solid-start-landing-page ready!

Name Link
🔨 Latest commit 1cf2ab2
🔍 Latest deploy log https://app.netlify.com/projects/solid-start-landing-page/deploys/6acf08f55a62f357cf1864ae
😎 Deploy Preview https://deploy-preview-2350--solid-start-landing-page.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Oct 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@solidjs/start@2350

commit: 1cf2ab2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: add 103 Early Hints / automatic Link headers support

2 participants