perf: Re-renders take ~2× the CPU of React 19.3 in two production TanStack Start sites (0.2.0)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- react, typescript
- 領域
- frontend, performance
調査の方向性
Start by examining the reported hot paths in dom/commit.js, dom/reconcile.js, and dom/dispatcher.js, especially rememberFiber, renderHost, and commitPlacements; compare their work during repeated billing tab updates with React's behavior. Use the attached click traces and CPU profiles, or reproduce with the public pricing pages, to identify the extra render and commit bookkeeping. Done means large tree updates no longer show roughly double the CPU cost while preserving the reported identical DOM mutations.
索引モデルが issue の本文から書いたものです。
説明
We evaluated Redact 0.2.0 on our two marketing sites, webcatalog.io and lexibird.com. Both run TanStack Start on Cloudflare Workers.
Adopting it was a drop-in change: builds, SSR, hydration and client-side navigation all worked. The size saving matched the announcement, and hydration blocked the main thread less than React's. But updates after load cost about 2× more CPU than React 19.3.1, and clicks that re-render a large part of the page became about twice as slow. Numbers, method, and CPU profiles and DevTools traces below.
Setup
@tanstack/redact0.2.0:redact()placed first in the plugins list with default options (fullpreset). No other changes.@tanstack/react-start1.169.3,@tanstack/react-router1.170.45,react/react-dom19.3.1vite8.3.5 (Rolldown),@vitejs/plugin-react6.1.2 with the React Compiler (babel-plugin-react-compiler1.0.0)@cloudflare/vite-plugin1.64.0 (plain plugin, not@tanstack/redact/cloudflare)- UI: Tailwind 4 with
@base-ui/react1.9.0 (Tabs, Accordion) - Production builds served by
vite preview(workerd), behind a local brotli proxy so transfer sizes match production. Headless Chromium on Linux x64 (a shared cloud container), with 4× CPU throttling applied via CDP (Emulation.setCPUThrottlingRate).
Bundle size: as advertised
The JS+CSS each page loads, from the TanStack Start route manifest plus its static imports:
| Page | React (gzip) | Redact (gzip) | Saved |
|---|---|---|---|
| webcatalog.io home | 444.6 KB | 397.3 KB | −47.3 KB |
| webcatalog.io pricing | 417.2 KB | 370.3 KB | −46.9 KB |
| lexibird.com home | 352.1 KB | 305.2 KB | −46.9 KB |
| lexibird.com pricing | 361.3 KB | 314.2 KB | −47.1 KB |
That's about 44 KB less transferred with brotli on every page, in line with the 47.0 KB in the Start row of the announcement.
Interaction latency: ~2× slower
Each pricing page has a billing-cycle Tabs that re-renders all of the page's pricing cards, plus a FAQ Accordion. We toggled the tab 9 times and opened 4 FAQ items per load, in a 412×915 viewport. Each interaction's duration is the max Event Timing duration for its interactionId, the same input INP uses. There were 6 loads per runtime, alternating React and Redact builds.
| Interaction | React 19.3.1 median (range) | Redact 0.2.0 median (range) |
|---|---|---|
| lexibird.com/en/pricing: billing tab | 104 ms (72–200) | 200 ms (144–368) |
| webcatalog.io/en/pricing: billing tab | 116 ms (72–216) | 252 ms (184–400) |
| webcatalog.io/en/pricing: FAQ accordion | 220 ms (worst 952) | 428 ms (worst 1536) |
| lexibird.com/en/pricing: FAQ accordion | 68 ms (worst 360) | 76 ms (worst 352) |
The small Lexibird FAQ update is unchanged, so the cost seems to scale with how much of the tree re-renders.
This doesn't look like the missing time slicing that the compatibility notes describe. React renders discrete click updates synchronously (SyncLane) too, so neither runtime yields during these updates.
Profiles: where the time goes
We recorded a V8 CPU profile (100 µs sampling) and, in a separate pass, a DevTools trace of 10 billing-tab clicks and of the page load. That's 3 rounds per site and runtime, with 4× CPU throttling. "Runtime CPU" is sampled self time in react-dom/react/scheduler or in @tanstack/redact, attributed through our build's source maps.
10 billing-tab clicks:
| Page | Runtime CPU, React | Runtime CPU, Redact | Median click task, React → Redact |
|---|---|---|---|
| lexibird.com/en/pricing | 422 / 554 / 872 ms | 1459 / 1548 / 1644 ms | 143 → 268 ms |
| webcatalog.io/en/pricing | 948 / 951 / 983 ms | 1754 / 1986 / 2061 ms | 158 → 308 ms |
The top Redact frames during clicks (webcatalog.io, round 1, self time) were:
rememberFiber(dom/commit.js:143), 158 msrenderHost(dom/reconcile.js:793), 113 mscommitPlacements(dom/reconcile.js:1044), 102 msuseEffectImpl(dom/dispatcher.js:134), 74 ms- the anonymous function at
dom/commit.js:146, 71 ms renderFunction(dom/reconcile.js:429), 62 msrenderWithHooks(dom/dispatcher.js:76), 60 msreconcile(dom/reconcile.js:907), 55 msreadContext(dom/reconcile.js:774), 51 ms
commitPlacements is not doing extra DOM work. We counted mutations per billing-tab click with a MutationObserver, and the DOM changes are identical under both runtimes: 12 attribute changes, plus 2 text changes on webcatalog.io and 4 on lexibird.com, with no nodes re-inserted. So the extra time goes into render and commit bookkeeping, not DOM writes.
Page load (navigation until network idle + 1.5 s): load came out better. Main-thread blocking time (the sum over 50 ms of each long task, from the trace) was:
| Page | React | Redact |
|---|---|---|
| lexibird.com/en/pricing | 1181–1458 ms | 418–595 ms |
| webcatalog.io/en/pricing | 973–1468 ms | 456–719 ms |
The longest task dropped from 377–500 ms to about 220 ms. Total sampled runtime CPU during load was higher under Redact (1.7–2.2 s against 0.45–0.51 s on webcatalog.io), but it was spread over shorter tasks. The Redact load profiles also reach a stack depth of about 378 frames, against 90–170 for React, through walkRetained (dom/walk.js:1), adoptHostDom and hydrateProps (hydration/full.js). That depth may inflate the sampling profiler's numbers, so we'd trust the trace's blocking times more than the CPU totals here.
One oddity: under Redact, a trivial scroll listener in our header showed 60–110 ms of self time during load, on every webcatalog.io round. It runs once on mount and calls setIsScrolled(window.scrollY > 0) from a useEffect. Under React it never appears among the top functions. It might be a synchronous re-render triggered by setState inside a passive effect during hydration being attributed to the caller, but we haven't confirmed that.
Lighthouse, for completeness
Mobile, default simulated throttling, third parties blocked; medians of 7 runs. The simulated TBT got worse under Redact, which contradicts the real-throttling traces above, so we'd discount it:
| Page | Score | LCP | TBT (simulated) | CLS |
|---|---|---|---|---|
| lexibird.com/en | 75 → 71 | 4.83 → 4.62 s | 290 → 413 ms | 0.001 → 0.001 |
| lexibird.com/en/pricing | 82 → 68 | 3.06 → 3.10 s | 467 → 1549 ms | 0 → 0 |
| lexibird.com/en/solutions/ai-translation | 80 → 70 | 4.69 → 3.30 s | 127 → 1002 ms | 0.001 → 0.001 |
| webcatalog.io/en/pricing | 79 → 67 | 3.72 → 3.69 s | 407 → 1073 ms | 0 → 0 |
| webcatalog.io/en/about | 72 → 83 | 4.39 → 3.65 s | 560 → 330 ms | 0 → 0 |
| webcatalog.io/en/legal/privacy-policy | 85 → 77 | 3.69 → 3.40 s | 386 → 727 ms | 0 → 0 |
Attached
redact-traces.zip contains round 1 for both sites and both runtimes:
*-load.trace.jsonand*-clicks.trace.json: DevTools traces you can load in Chrome's Performance panel.*.cpuprofile: V8 CPU profiles from a separate pass, so the tracer doesn't skew them.
The scripts are minified and we haven't attached our source maps; the function names above are mapped to Redact's sources (dist/). Both pricing pages are public (/en/pricing on each site) if you'd like to reproduce with your own builds.
Not verified
- Whether the React Compiler matters. It's on in both builds and may interact with Redact differently, for example through
react/compiler-runtime→@tanstack/redact/compiler-runtime. - Real devices. All numbers come from one shared Linux container with CPU throttling; we didn't measure on phones.
- 主要言語
- TypeScript
- スター
- 537
- フォーク
- 22
- 平均マージ
- 2時間 19分
- マージ済み PR(30日)
- 24
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/redact のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 42/100
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 18/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 18/100
メンテナーはふだん 1 日以内に返信
TanStack/redact の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
ready-for-agent
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
LucasSantana-Dev/Lucky#2951 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
danielmiessler/LifeOS#2567 ·
メンテナーはふだん 9 日以内に返信
-
refactor(react-pi): classify base64 images with core's parseBase64対応中かも @jimmyallinai が今日担当しました。 オープンmaintenance
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
assistant-ui/assistant-ui#10293 · コメント 2 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
brijeshb42/monaco-vim#175 ·