Skip to content

replaceEqualDeep optimization#7462

Open
schiller-manuel wants to merge 1 commit into
mainfrom
perf-replaceEqualDeep
Open

replaceEqualDeep optimization#7462
schiller-manuel wants to merge 1 commit into
mainfrom
perf-replaceEqualDeep

Conversation

@schiller-manuel
Copy link
Copy Markdown
Collaborator

@schiller-manuel schiller-manuel commented May 21, 2026

replaceEqualDeep Bundle Optimization

Bundle Size

Target scenario baseline:

  • react-router.minimal: gzip 89179, initial 89039, raw 279330, brotli 77564

Target scenario final:

  • react-router.minimal: gzip 89159, initial 89020, raw 279298, brotli 77583
  • Delta: gzip -20, initial -19, raw -32, brotli +19

Full benchmark final deltas:

  • react-router.minimal: gzip -20, initial -19, raw -32, brotli +19
  • react-router.full: gzip -20, initial -21, raw -32, brotli -76
  • solid-router.minimal: gzip -28, initial -29, raw -32, brotli -53
  • solid-router.full: gzip -25, initial -23, raw -32, brotli -43
  • vue-router.minimal: gzip -27, initial -27, raw -32, brotli +4
  • vue-router.full: gzip -30, initial -29, raw -32, brotli -65
  • react-start.minimal: gzip -26, initial -21, raw -32, brotli -78
  • react-start.deferred-hydration: gzip -21, initial -20, raw -32, brotli -156
  • react-start.full: gzip -24, initial -22, raw -32, brotli +19
  • react-start.rsbuild.minimal: gzip -17, initial -17, raw -28, brotli -98
  • react-start.rsbuild.full: gzip -17, initial -17, raw -28, brotli -6
  • solid-start.minimal: gzip -29, initial -31, raw -32, brotli +32
  • solid-start.deferred-hydration: gzip -17, initial -17, raw -32, brotli +45
  • solid-start.full: gzip -23, initial -22, raw -32, brotli -39

Perf

Focused bench: @tanstack/router-core:test:perf -- tests/replace-equal-deep.bench.ts

  • Mixed router state: baseline 19.53 hz, current 20.74 hz
  • Plain object equal: baseline 4191.32 hz, current 4493.67 hz
  • Array matches equal: baseline 2249.29 hz, current 2350.51 hz
  • Long matches mixed: baseline 22.80 hz, current 23.05 hz
  • Primitive mismatch: baseline 9470.48 hz, current 9493.51 hz
  • Null search: baseline 599.22 hz, current 609.79 hz

Validation

  • @tanstack/router-core:test:unit: pass
  • @tanstack/router-core:test:types: pass
  • @tanstack/router-core:test:eslint: pass with existing warnings
  • tanstack-router-e2e-react-basic-file-based:test:e2e: 157 passed
  • git diff --check: pass

Attribution

  • Kept overload/alias removal: gzip -2 vs final without it.
  • Kept array equality count simplification: gzip -6 vs final without it.
  • Kept object recursion branch inversion: gzip -6 vs final without it.
  • Rejected Reflect.ownKeys: gzip win but slower on focused bench.
  • Rejected inline isPlainArray: only gzip -1 and worse raw/brotli.
  • Rejected merged fast-path branch: neutral after attribution.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of undefined values in object and array comparisons within router core.
  • Tests

    • Added comprehensive benchmark suite for deep equality operations.
    • Enhanced test coverage for edge cases including symbol keys and null-prototype objects.
  • Refactor

    • Optimized internal comparison logic for better referential equality preservation.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6aeb676c-141b-45da-908a-26404f55e104

📥 Commits

Reviewing files that changed from the base of the PR and between e2896b5 and 21dab5b.

📒 Files selected for processing (3)
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/replace-equal-deep.bench.ts
  • packages/router-core/tests/utils.test.ts

📝 Walkthrough

Walkthrough

This PR optimizes replaceEqualDeep by renaming the _next parameter to next, consolidating deep-recursion conditions, and refactoring equality-counting logic. getEnumerableOwnKeys is restructured with explicit early-return blocks. A new nullReplaceEqualDeep variant is introduced. Test coverage expands to handle undefined-value edge cases and null-prototype objects. A comprehensive benchmark suite validates the optimization against a baseline implementation.

Changes

replaceEqualDeep optimization and testing

Layer / File(s) Summary
replaceEqualDeep and getEnumerableOwnKeys refactoring
packages/router-core/src/utils.ts
Parameter signature changed from _next to next in both overloads and implementation. Loop logic consolidated with combined p && n && typeof p === 'object' && typeof n === 'object' condition for deep recursion; equality counting now conditional on referential equality of recursive result. getEnumerableOwnKeys refactored into explicit early-return blocks for string property and symbol enumerability checks.
Unit test coverage for undefined-handling and nullReplaceEqualDeep
packages/router-core/tests/utils.test.ts
Extended replaceEqualDeep tests for undefined appending/removal in arrays, undefined key removal in objects, symbol-key existence tracking, and reuse of equal nested values. New test section validates nullReplaceEqualDeep behavior on null-prototype objects and nested value reuse.
Benchmark suite with baseline implementation and assertions
packages/router-core/tests/replace-equal-deep.bench.ts
New Vitest benchmark file with baseline deep-replacement logic, enumerable-key enumeration, and recursive reuse patterns. Constructs router-state fixtures (arrays, symbols, non-enumerable properties, null-prototype objects). Asserts optimized implementations match baseline outputs and preserve object identity. Runs benchmarks across mixed, array, long-array, primitive, and null-prototype scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through deep-replacement trees,
Where next now dances where _next did please,
With null-prototype cousins and benchmark cheer,
The optimization bounds through undefined's frontier!
Equal and swift, referential and clear! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'replaceEqualDeep optimization' directly and clearly describes the main change: optimizing the replaceEqualDeep function. It is concise, specific, and summarizes the primary purpose of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf-replaceEqualDeep

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 21, 2026

View your CI Pipeline Execution ↗ for commit 21dab5b

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 9m 34s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 18s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-21 20:33:36 UTC

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 21, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7462

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7462

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7462

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7462

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7462

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7462

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7462

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7462

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7462

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7462

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7462

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7462

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7462

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7462

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7462

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7462

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7462

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7462

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7462

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7462

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7462

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7462

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7462

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7462

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7462

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7462

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7462

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7462

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7462

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7462

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7462

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7462

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7462

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7462

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7462

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7462

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7462

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7462

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7462

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7462

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7462

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7462

commit: 21dab5b

@github-actions
Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: c72f338b7846
  • Measured at: 2026-05-21T20:24:22.666Z
  • Baseline source: history:65b4abe65bc2
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.28 KiB -22 B (-0.02%) 87.14 KiB 273.98 KiB 75.94 KiB ███████▄▄▄▄▁
react-router.full 90.81 KiB -11 B (-0.01%) 90.67 KiB 285.48 KiB 78.83 KiB ▇▇▇▇▇▇█▂▂▂▂▁
solid-router.minimal 35.51 KiB -17 B (-0.05%) 35.39 KiB 106.29 KiB 31.95 KiB ▆▆▆████▃▃▃▃▁
solid-router.full 40.25 KiB -17 B (-0.04%) 40.13 KiB 120.49 KiB 36.16 KiB ▆▆▆▆▆▆▇████▁
vue-router.minimal 53.30 KiB -19 B (-0.03%) 53.18 KiB 151.43 KiB 47.86 KiB ██████▅▇▇▇▇▁
vue-router.full 58.43 KiB -19 B (-0.03%) 58.30 KiB 167.59 KiB 52.42 KiB ██████▇████▁
react-start.minimal 102.00 KiB -18 B (-0.02%) 101.87 KiB 322.44 KiB 88.24 KiB ▃▃▃▆▆▆▅████▁
react-start.deferred-hydration 103.05 KiB -18 B (-0.02%) 102.18 KiB 324.08 KiB 89.18 KiB ████▆▆▆▆▁
react-start.full 105.41 KiB -15 B (-0.01%) 105.27 KiB 332.75 KiB 91.07 KiB ▇▇▇████▃▃▃▃▁
react-start.rsbuild.minimal 99.62 KiB -18 B (-0.02%) 99.45 KiB 316.90 KiB 85.71 KiB ▇▇▇▇▇▇▅████▁
react-start.rsbuild.full 102.91 KiB -18 B (-0.02%) 102.74 KiB 327.31 KiB 88.46 KiB ██████▇▇▇▇▇▁
solid-start.minimal 49.65 KiB -24 B (-0.05%) 49.52 KiB 152.42 KiB 43.87 KiB ▃▃▃███▇▅▅▅▅▁
solid-start.deferred-hydration 53.73 KiB -12 B (-0.02%) 50.38 KiB 160.96 KiB 47.74 KiB ███▇▂▂▂▂▁
solid-start.full 55.44 KiB -20 B (-0.04%) 55.30 KiB 169.31 KiB 48.73 KiB ▂▂▂███▆▇▇▇▇▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a possible root cause for your failed CI:

We classified this failure as an environment issue rather than a code change. The failing test (preserves scroll position after a force reload on a visible boundary) lives outside the touched project and failed due to a 5-second polling timeout on a scroll position assertion — a transient condition consistent with a slow CI environment. The replaceEqualDeep optimization is behavior-neutral and no direct link to scroll restoration logic was found.

No code changes were suggested for this issue.

Trigger a rerun:

Rerun CI

Nx Cloud View detailed reasoning on Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 21, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing perf-replaceEqualDeep (21dab5b) with main (65b4abe)2

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

  2. No successful run was found on main (e2896b5) during the generation of this report, so 65b4abe was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

1 participant