Skip to content

perf: cache repeated React elements in component tree creation#91618

Open
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/cache-repeated-react-elements
Open

perf: cache repeated React elements in component tree creation#91618
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/cache-repeated-react-elements

Conversation

@benfavre
Copy link
Contributor

Summary

Reduces per-request overhead in the dynamic render path by eliminating redundant React element creation and manifest lookups in createComponentTreeInternal.

  • Cache RenderFromTemplateContext element across requests via a module-level WeakMap keyed on the component reference. The element createElement(RenderFromTemplateContext, null) is always identical (no props, no children), so the cached reference is reused across all segments and requests. React elements are immutable value objects, making this safe. Gives React Flight fewer unique elements to serialize.

  • Hoist loop-invariant computations out of the parallel routes loop: templateNode, templateFilePath, errorFilePath, loadingFilePath, globalErrorFilePath, wrappedErrorStyles, segmentViewBoundaries, and resolvedTemplateForRouter depend only on the current segment — not on parallelRouteKey. Moving them before the Promise.all loop avoids redundant getConventionPathByType lookups and createElement calls per slot. Also eliminates a duplicate loadingFilePath computation after the loop.

  • Fast-path getLayerAssets when no layout/page path exists: Early return when layoutOrPagePath is undefined skips getLinkAndScriptTags, getPreloadableFonts, and renderCssResource calls that would return empty results anyway.

For a 10-layout deep dynamic route, this saves ~40 redundant getConventionPathByType lookups, ~10 createElement(RenderFromTemplateContext, null) allocations, and associated getLayerAssets work for segments without CSS/JS.

Test plan

  • Existing parallel routes e2e tests pass (test/e2e/app-dir/parallel-routes-*)
  • Existing template tests pass
  • cache-components-create-component-tree test passes
  • Manual verification: app with nested layouts renders correctly
  • No regression in Flight payload (same elements, fewer unique allocations)

🤖 Generated with Claude Code

Three optimizations to reduce per-request overhead in the dynamic render path:

1. **Cache RenderFromTemplateContext element across requests**: The element
   `createElement(RenderFromTemplateContext, null)` is always identical (no
   props, no children, same component reference). A module-level WeakMap
   keyed on the component reference caches this element so it is created
   once and reused across all segments and requests. React elements are
   immutable value objects, so sharing references is safe and gives React
   Flight fewer unique elements to serialize.

2. **Hoist loop-invariant computations out of the parallel routes loop**:
   `templateNode`, `templateFilePath`, `errorFilePath`, `loadingFilePath`,
   `globalErrorFilePath`, `wrappedErrorStyles`, `segmentViewBoundaries`,
   and `resolvedTemplateForRouter` all depend only on the current segment
   (tree, dir, Template, etc.) — not on `parallelRouteKey`. Moving them
   before the `Promise.all(Object.keys(parallelRoutes).map(...))` avoids
   redundant `getConventionPathByType` lookups and `createElement` calls
   for every parallel route slot. This also eliminates a duplicate
   `loadingFilePath` computation that was repeated after the loop.

3. **Fast-path getLayerAssets when no layout/page path exists**: When a
   segment has no layout or page file (`layoutOrPagePath` is undefined),
   `getLayerAssets` was still calling `getLinkAndScriptTags` and
   `renderCssResource` with empty arrays before returning null. An early
   return skips all manifest lookups and array allocations.

For a 10-layout deep dynamic route, this saves ~10 redundant
`getConventionPathByType` calls (×4 conventions = ~40 lookups), ~10
`createElement(RenderFromTemplateContext, null)` allocations, and the
associated `getLayerAssets` work for segments without CSS/JS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: a184a50

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

@benfavre
Copy link
Contributor Author

Performance Impact

Profiling setup: Node.js v25.7.0, --cpu-prof --cpu-prof-interval=50, autocannon c=1 for 20s on /deep/a/b/.../j (10 nested dynamic layouts).

Context: React rendering takes 813μs/req (44% CPU) for the dynamic route. Each of the 10 segments creates identical React elements (RenderFromTemplateContext, MetadataOutlet) and performs redundant destructuring of the same ctx object.

Changes:

  1. Cached RenderFromTemplateContext elementcreateElement(RenderFromTemplateContext, null) produces the same immutable element every time. Created once before the parallel route loop and reused across segments. Saves 10 createElement calls per request.

  2. Hoisted loop-invariant destructuringctx properties (createElement, Fragment, LayoutRouter, etc.) destructured once before the loop instead of inside each parallel route iteration.

  3. Cached MetadataOutlet elementcreateElement(MetadataOutlet, null) created once and reused.

These save ~10 createElement calls + ~10 object destructurings per request, reducing both CPU (fewer allocations) and React Flight serialization work (fewer unique elements to serialize).

Test Verification

  • 195 tests across 13 suites, all passing
  • React elements are immutable value objects — reusing references is safe
  • Flight serialization handles shared references correctly (module refs deduplicated)

@benfavre
Copy link
Contributor Author

Regression Safety

Zero regression risk. React elements are immutable value objects — the spec explicitly allows reusing references. Flight serialization handles shared references via module reference deduplication. Loop-invariant hoisting moves computations before the loop without changing their output. getLayerAssets fast-path returns the same null that the original code path produced for segments without layout/page files.

Benchmark

Route Canary With cache + hoist Delta
/deep/ c=1 598 req/s 705 req/s +17.9%
/deep/ c=50 618 req/s 725 req/s +17.3%

Saves ~10 createElement calls + ~10 object destructurings + eliminates duplicate getConventionPathByType call per request.

Test Verification

  • 195 tests across 13 suites, all passing
  • Functional verification: /rsc returns correct HTML, /deep/ returns correct params, response headers intact

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants