HTML-first React · compared with stock React main · measured at 31b26561f1 · 10 November 2026

HTML-first React: results

On first load, an RSC page sends its static markup twice: as HTML, so the browser can paint, and again inside the Flight payload the browser inlines to hydrate. For content that never changes on the client, the second copy is wasted bandwidth, and it costs the server time to produce.

This experiment adds a flag that leaves that content out of the second copy. Across 41 test pages it cuts the gzipped download by 43%. On a Waku page with a 1000-row table rendered by Server Components it raises throughput 3.2×. A framework turns it on with one server option and, for full safety, one client callback. The code is PR #24, a reference implementation (final version df356153a6).

−43%gzip download, 41 pages
−79%inlined Flight payload
−50%uncompressed HTML
3.2×Waku table page, requests/s

How it works

With enableFlightStaticHostElision on, Flight does not write static host markup into the payload the browser receives. It writes a small stub instead, and the browser adopts the DOM that the HTML already contains. SSR still needs the real content to write that HTML, so Flight hands it over through an internal registry.

Markup qualifies when it is plain output of a Server Component in a place React renders in place:

Client Components are never elided. Neither are props other than children, because the client can render those later with no server DOM behind them, nor forms, inputs, images, SVG or head elements.

The feature is opt-in per request. The framework passes elideStaticHosts: true on the Flight render that feeds HTML SSR, and leaves it off everywhere else, so navigation, refetch and action responses stay ordinary full payloads. React cannot tell where a stream is going, because the bytes are identical either way. Frameworks can: Waku serves RSC requests under an RSC/ URL prefix, and Next.js sends an RSC: 1 header.

Bytes across 41 test pages

Each page is rendered at full size with the flag off and on. Gzip and brotli are of the whole document, and Flight is the payload inlined in <script> tags. The table sums all 41 pages.

MetricFlag offFlag onChange
Gzip download1,023,906586,852−42.7%
Raw HTML14,555,7147,252,862−50.2%
Inlined Flight9,280,7541,977,902−78.7%
Brotli479,414286,520−40.2%

The total is dominated by a few pages: the five largest savings make up about 80% of the gzip bytes saved. The median page saves 7.8%, nine pages are within ±1%, and seven get slightly larger, the worst being repeated-long-strings at +12.4%. A page with nothing to elide, like all-client, pays about 0.6% for the registry row. Per-page numbers are on the examples page.

Pagegzipinlined Flight
wide-tree−79%−99.9%
client-composition-slot−59%−94%
docs-sidebar-toc−59%−98%
server-table-client-rows−50%−95%
nested-layouts-context−41%−89%
all-client+0.6%+15%

Throughput on Waku

The test page is a 1000-row table rendered by Server Components, plus inline CSS and JSON-LD. Waku was chosen because it is easy to patch and has good RSC support; the feature does not depend on it. The patch has two parts: the HTML render passes elideStaticHosts: true, and the root refetches the initial payload when React reports that elided content is needed. Figures are medians over 5 rounds on one laptop, with the order of the servers rotated each round.

StockFlag on, stock WakuFlag on, patched Waku
Requests/s1.001.003.18× (3.13–3.22)
TTFB1.000.970.45×
Inlined Flight823,655 B823,655 B18,978 B
Gzip HTML130,459 B130,460 B60,889 B

With the flag on and no opt-in, Waku's output is byte-identical to stock: forgetting the option loses the savings but breaks nothing. A second page whose rows are Client Components improves only 1.10×, since those rows cannot be elided. Next.js was not tried.

Correctness

The 41 pages are loaded in Chromium with and without the flag. For each one the check compares the hydrated DOM, the DOM after interaction, and the DOM after a client navigation to a full payload, and it fails on any console or hydration error. All 41 pass at 31b26561f1. The comparison normalizes style spacing, zero-length CSS units and attribute order. Firefox and WebKit passed on earlier commits but have not been run on this one. The Waku page hydrates, its counters respond, it logs no errors, and a client navigation renders all 1000 rows.

The same kind of check also runs in CI on Linux, with the flag off and on, in real Chromium. The latest CI report covers 10 page variants at df356153a6: every one adopts 1000 of 1000 rows from the server HTML, logs no console problems and no recoverable hydration errors, and navigates correctly.

On a client navigation the full payload contains real elements where the stubs were. React builds fresh DOM for those regions rather than reusing the old DOM, whose content may not match the new page. Client components keep their state.

Limits

Try it yourself

You need Node 22 and npm. Nothing is cloned or built: the starter app installs a React build with the flag on and a Waku with the patch, both from a GitHub release.

npx degit MaxwellCohen/react-eco-ci/starters/waku-static-host-elision elision-demo
cd elision-demo
npm install
npm run build
npm start            # http://localhost:8080
npm run measure      # in a second terminal

npm run measure prints the size of the two table pages next to what stock React and stock Waku send for the same pages. Then open /table-host and view the source: the inlined Flight payload is small and contains $J stubs. Open /late-modal and click “open”: that content was not in the first payload, and appears because Waku refetches it. I have run npm run build and npm start, not npm run dev.

To add the packages to your own Waku app, install the four tarballs from the release and override scheduler and react-is as well; the exact command and overrides are in the react-eco-ci README. The app needs to work on Waku 1.0.0-rc.4, and these are experimental builds, not for production.

Links

Method