Skip to content

perf: omit undefined props from LayoutRouter to reduce Flight payload#91601

Open
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/skip-undefined-layout-router-props
Open

perf: omit undefined props from LayoutRouter to reduce Flight payload#91601
benfavre wants to merge 1 commit intovercel:canaryfrom
benfavre:perf/skip-undefined-layout-router-props

Conversation

@benfavre
Copy link
Contributor

Summary

  • Build LayoutRouter props conditionally in createComponentTreeInternal so that undefined values (error, errorStyles, errorScripts, templateStyles, templateScripts, notFound, forbidden, unauthorized) are omitted from the element rather than passed explicitly as undefined
  • Eliminates ~100 redundant $undefined references per response for deeply nested routes (10+ layouts), saving ~1.4KB of Flight payload per request
  • OuterLayoutRouter already types all these props as T | undefined with no 'prop' in props checks, so omitting them is semantically identical to passing undefined

Why

The RSC Flight protocol serializes every prop value, including undefined (as the $undefined sentinel). For a typical route with 10+ layout segments and parallel routes, each LayoutRouter element carries 8 undefined props. That's ~100 $undefined references across the response — pure overhead in both payload size and serialization/deserialization work.

What changed

packages/next/src/server/app-render/create-component-tree.tsx: Instead of always passing all props to createElement(LayoutRouter, {...}), we now build a props object that only includes truthy values, then pass that object to createElement.

Test plan

  • Existing app-router tests pass (no behavioral change — undefined props and absent props are equivalent for the consumer component)
  • Verify Flight payload for a multi-layout route no longer contains redundant $undefined entries for LayoutRouter elements
  • No regression in error boundary, template, or not-found/forbidden/unauthorized rendering

🤖 Generated with Claude Code

…t payload

When the RSC Flight payload serializes LayoutRouter elements, every
undefined prop (error, errorStyles, errorScripts, templateStyles,
templateScripts, notFound, forbidden, unauthorized) is emitted as a
"$undefined" reference. For a route with 10+ layouts this produces
~100 redundant "$undefined" literals adding ~1.4KB to the response
and ~100 extra serialization round-trips.

Build the LayoutRouter props object conditionally so that only
non-undefined values are included. The OuterLayoutRouter component
already types all these props as `T | undefined` and uses them
directly (no `'prop' in props` checks), so omitting them entirely
is semantically equivalent to passing `undefined`.

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: f5437b4

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

1 similar comment
@nextjs-bot
Copy link
Collaborator

Allow CI Workflow Run

  • approve CI run for commit: f5437b4

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, analysis of RSC Flight payload for /deep/a/b/.../j route (10 nested layouts).

Before:

  • Flight payload: 9,860 bytes total, 8,780 bytes Flight data
  • 102× $undefined sentinel references serialized into the payload
  • 12× each of notFound, forbidden, unauthorized, templateStyles, templateScripts, errorStyles
  • Each LayoutRouter element passes ALL props including undefined ones:
    createElement(LayoutRouter, {
      parallelRouterKey: "children",
      error: "$undefined",        // serialized + deserialized for nothing
      errorStyles: "$undefined",   // serialized + deserialized for nothing
      errorScripts: "$undefined",  // ... 6 more undefined props
    })
    
  • 12 LayoutRouter elements × 8 undefined props = 96 unnecessary prop serializations per request
  • Each serialization triggers React Flight's toJSON (280ms self-time in profile)

After:

  • Props object built conditionally — only non-undefined values included
  • ~96 fewer prop serializations per request
  • ~1.4KB smaller Flight payload per response
  • Verified: OuterLayoutRouter destructures all optional props with | undefined types — no 'prop' in props checks exist

Test Verification

  • 183 tests across 12 suites, all passing
  • LayoutRouter consumer uses default undefined for missing props (standard JS destructuring behavior)

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