|
| 1 | +# Lighthouse: @tanstack/dom-vite shim vs. real React |
| 2 | + |
| 3 | +**Date:** 2026-04-20 |
| 4 | +**tanstack.com commit at time of measurement:** `fb806bb` (shim build with `@tanstack/dom-vite@0.1.0-alpha.5`, pulling `@tanstack/react-dom@0.1.0-alpha.4` — includes the RSC deferred-hydration adoption fix landed the same day). |
| 5 | +**React baseline build:** same source tree with `tanstackDom()` plugin removed from `vite.config.ts` and `serverVariantAliases` dropped — i.e. stock `react@19.2.3` / `react-dom@19.2.3`. |
| 6 | + |
| 7 | +## TL;DR |
| 8 | + |
| 9 | +- **Performance score: parity** (±2 across pages / form factors, within run-to-run noise). |
| 10 | +- **FCP: consistent shim win** everywhere — ~4% on home, ~11–17% on docs / blog. Smaller main-thread parse cost lets first paint land sooner. |
| 11 | +- **LCP: shim regresses on RSC-heavy pages, desktop** — the LCP element on docs / blog pages lives in the Flight-streamed subtree, and the shim's `use(pendingPromise)` + deferred-resume adds latency vs. React's battle-tested RSC client. Mobile is mostly parity (network-bound anyway). |
| 12 | +- **TBT / CLS: effectively zero on both** after the same-day RSC hydration fix — no duplicate DOM, no layout shift from appending. |
| 13 | +- **Bundle (raw JS): −4.7%** on tanstack.com (-980 KB of 21 MB total client JS). Modest because router / store / app code dominates; shim only replaces React's share. |
| 14 | + |
| 15 | +## Methodology |
| 16 | + |
| 17 | +1. `pnpm build` for each variant. |
| 18 | +2. `PORT=4000 pnpm start:prod` to serve from `dist/server/server.js` on `http://localhost:4000`. |
| 19 | +3. **5 trials × 3 URLs × 2 form factors = 30 Lighthouse runs per variant** using `npx lighthouse` v13 with `--only-categories=performance` and headless Chrome. |
| 20 | +4. Mobile runs use Lighthouse's default emulation (slow 4G + 4× CPU slowdown). Desktop uses `--preset=desktop` (no throttling). |
| 21 | +5. Medians reported below. |
| 22 | + |
| 23 | +## Medians |
| 24 | + |
| 25 | +### Performance score |
| 26 | + |
| 27 | +| URL | form | React | Shim | Δ | |
| 28 | +| --- | :-: | -: | -: | -: | |
| 29 | +| `/` | desktop | 99 | 99 | 0 | |
| 30 | +| `/` | mobile | 87 | 88 | +1 | |
| 31 | +| `/query/latest/docs/framework/react/guides/queries` | desktop | 96 | 96 | 0 | |
| 32 | +| `/query/latest/docs/framework/react/guides/queries` | mobile | 64 | 66 | +2 | |
| 33 | +| `/blog/react-server-components` | desktop | 98 | 96 | −2 | |
| 34 | +| `/blog/react-server-components` | mobile | 70 | 71 | +1 | |
| 35 | + |
| 36 | +### Web Vitals |
| 37 | + |
| 38 | +| URL | form | FCP (R → S) | LCP (R → S) | TBT (R → S) | CLS (R → S) | TTI (R → S) | |
| 39 | +| --- | :-: | :-: | :-: | :-: | :-: | :-: | |
| 40 | +| `/` | desktop | 0.61s → 0.59s (−4%) | 0.84s → 0.91s (+8%) | 0ms → 0ms | 0 → 0 | 0.84s → 0.92s | |
| 41 | +| `/` | mobile | 2.34s → 2.31s | 3.71s → 3.60s (−3%) | 19ms → 20ms | 0 → 0 | 5.54s → 5.55s | |
| 42 | +| `/query/.../queries` | desktop | 1.05s → 0.92s (−13%) | 1.05s → 1.24s (+18%) | 0ms → 0ms | 0 → 0 | 1.05s → 1.24s | |
| 43 | +| `/query/.../queries` | mobile | 4.66s → 4.13s (−11%) | 6.62s → 6.39s (−3%) | 17ms → 19ms | 0 → 0 | 8.36s → 8.41s | |
| 44 | +| `/blog/react-server-components` | desktop | 0.90s → 0.74s (−17%) | 0.90s → 1.29s (+43%) | 0ms → 0ms | 0 → 0 | 0.90s → 1.29s | |
| 45 | +| `/blog/react-server-components` | mobile | 3.73s → 3.21s (−14%) | 5.32s → 6.23s (+17%) | 34ms → 21ms (−37%) | 0 → 0 | 6.24s → 6.57s | |
| 46 | + |
| 47 | +### Bundle size (uncompressed total client JS) |
| 48 | + |
| 49 | +| Build | Total client JS | Notes | |
| 50 | +| --- | -: | --- | |
| 51 | +| Real React | 21,052 KB | Dedicated `react-*.js` chunk = 176 KB (`manualChunks` splits `node_modules/react{,-dom}/`) | |
| 52 | +| Shim | 20,072 KB | No dedicated react chunk; shim code inlines into `app-shell` (+16 KB there). Net **−980 KB (−4.7%)** | |
| 53 | + |
| 54 | +## Caveats |
| 55 | + |
| 56 | +- **Lab data only.** Chrome origin-level CWV (CrUX) needs ~28 days of real traffic before aggregates stabilize. Since the shim only went live on `2026-04-20`, field data won't be comparable for a month. |
| 57 | +- **`pnpm start:prod` serves from Node locally — no CDN.** Absolute TTFB numbers are dev-machine noise (5ms–1s range depending on cold-cache loader work); anchor on client-side metrics. |
| 58 | +- **Per-page LCP percentages can look dramatic when the absolute value is small.** Blog desktop LCP `0.90s → 1.29s` is +390 ms — real, but a sub-second LCP regression in both states is still a Core Web Vitals "Good" rating (<2.5s). |
| 59 | +- **Single-node prod server — no edge, no warm cache.** Mobile Lighthouse runs with 4× CPU throttling are inherently high-variance. |
| 60 | + |
| 61 | +## Reproduce |
| 62 | + |
| 63 | +```bash |
| 64 | +# React baseline |
| 65 | +# 1) temporarily remove tanstackDom() plugin + serverVariantAliases in vite.config.ts |
| 66 | +pnpm build |
| 67 | +PORT=4000 pnpm start:prod & |
| 68 | +# run 5 trials × 3 URLs × 2 form factors, save JSON to ./react/ |
| 69 | + |
| 70 | +# Shim |
| 71 | +# 2) restore tanstackDom() plugin + serverVariantAliases |
| 72 | +pnpm build |
| 73 | +PORT=4000 pnpm start:prod & |
| 74 | +# re-run, save JSON to ./shim/ |
| 75 | + |
| 76 | +# Aggregate medians + delta (parse JSON, compute median of numericValues per audit key) |
| 77 | +``` |
| 78 | + |
| 79 | +See the shim side for the runner + aggregator scripts used (`/tmp/lh-compare/run.sh`, `/tmp/lh-compare/aggregate.mjs` at measurement time). |
| 80 | + |
| 81 | +## Related shim work shipped with this comparison |
| 82 | + |
| 83 | +- `@tanstack/react-dom@0.1.0-alpha.4`: `renderFunction`'s deferred-hydration branch now mirrors `renderLazy`'s ancestor-Suspense guard (`_awaitingLazyHydration`). Fixes duplicate-markup on RSC pages. Regression test: `tests/rsc-hydration-adopt.test.tsx`. |
| 84 | +- `@tanstack/react-dom-server@0.1.0-alpha.4`: shell-chunk batching in `streamHtml` (reduces Node stream overhead ~3–4% on SSR bench). |
| 85 | +- `@tanstack/dom-vite@0.1.0-alpha.5`: dep bump to pick up react-dom@alpha.4. |
0 commit comments