Skip to content

enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift()#3876

Merged
ntucker merged 1 commit intomasterfrom
perf-globalcache-preallocate
Apr 6, 2026
Merged

enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift()#3876
ntucker merged 1 commit intomasterfrom
perf-globalcache-preallocate

Conversation

@ntucker
Copy link
Copy Markdown
Collaborator

@ntucker ntucker commented Apr 5, 2026

Motivation

GlobalCache.getResults() calls unshift() to prepend the input reference to the dependency tracking array on every cache-miss denormalization. Array.prototype.unshift() is O(n) — it shifts every existing element right by one index. As the dependency array grows (especially for large normalized responses), this becomes a measurable bottleneck.

Identified via V8 profiling of the React benchmark (examples/benchmark-react with BENCH_V8_DEOPT=true).

Solution

Pre-allocate slot 0 with a PLACEHOLDER_DEP sentinel when the GlobalCache is constructed. In getResults(), replace the unshift() with a direct assignment to dependencies[0], turning the operation from O(n) to O(1). The paths() method is updated to skip slot 0 (the input reference entry) when extracting entity paths.

Benchmark results (isolated, React benchmark denormalizeLong variants):

  • denormalizeLong (cold): +1.3–3.2% ops/sec improvement
  • Zero-cost for cache-hit paths (no unshift was called there)
  • Bundle size neutral (adds ~30 bytes gzipped for the placeholder constant)

Open questions

N/A

Made with Cursor


Note

Medium Risk
Touches GlobalCache dependency/path tracking used to populate endpoint result caches; an off-by-one or placeholder misuse could cause subtle cache invalidation issues despite being a small change.

Overview
Improves denormalization cache-miss performance in GlobalCache.getResults() by removing Array.prototype.unshift() and instead pre-allocating dependency slot 0 (filled with the input reference on misses).

Updates paths() to skip the reserved slot and build the returned path list without including the input placeholder, and adds a changeset to publish this as a patch performance improvement.

Reviewed by Cursor Bugbot for commit 3eaf5f2. Bugbot is set up for automated code reviews on this repo. Configure here.

…ft()

GlobalCache.getResults() called unshift() on every cache-miss denormalization,
which is O(n) because it shifts all existing elements. Pre-allocate slot 0
with a placeholder and fill it in-place, turning the operation into O(1).

Benchmarks showed 1.3–3.2% improvement on cold-denormalize paths
(denormalizeLong variants).

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 5, 2026

🦋 Changeset detected

Latest commit: 3eaf5f2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@data-client/normalizr Patch
@data-client/core Patch
example-benchmark Patch
normalizr-github-example Patch
normalizr-redux-example Patch
normalizr-relationships Patch
@data-client/react Patch
@data-client/vue Patch
example-benchmark-react Patch
test-bundlesize Patch
coinbase-lite Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-site Ignored Ignored Apr 5, 2026 10:12pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 5, 2026

Size Change: +41 B (+0.05%)

Total Size: 80.6 kB

📦 View Changed
Filename Size Change
examples/test-bundlesize/dist/rdcClient.js 10.4 kB +41 B (+0.4%)
ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.46 kB
examples/test-bundlesize/dist/polyfill.js 307 B
examples/test-bundlesize/dist/rdcEndpoint.js 8 kB
examples/test-bundlesize/dist/react.js 59.7 kB
examples/test-bundlesize/dist/webpack-runtime.js 726 B

compressed-size-action

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Benchmark React

Details
Benchmark suite Current: 3eaf5f2 Previous: f5797b4 Ratio
data-client: getlist-100 186.93 ops/s (± 4.2%) 170.95 ops/s (± 4.9%) 0.91
data-client: getlist-500 48.31 ops/s (± 3.4%) 47.17 ops/s (± 9.3%) 0.98
data-client: update-entity 454.55 ops/s (± 5.1%) 444.66 ops/s (± 3.5%) 0.98
data-client: update-user 555.56 ops/s (± 6.2%) 416.67 ops/s (± 6.6%) 0.75
data-client: getlist-500-sorted 50.76 ops/s (± 4.3%) 52.49 ops/s (± 5.9%) 1.03
data-client: update-entity-sorted 416.67 ops/s (± 4.7%) 416.67 ops/s (± 5.0%) 1
data-client: update-entity-multi-view 416.67 ops/s (± 3.1%) 400 ops/s (± 6.6%) 0.96
data-client: list-detail-switch-10 12.79 ops/s (± 7.5%) 11.06 ops/s (± 5.9%) 0.86
data-client: update-user-10000 125.79 ops/s (± 5.7%) 94.34 ops/s (± 0.8%) 0.75
data-client: invalidate-and-resolve 56.82 ops/s (± 4.1%) 51.55 ops/s (± 3.4%) 0.91
data-client: unshift-item 322.58 ops/s (± 4.0%) 303.03 ops/s (± 4.3%) 0.94
data-client: delete-item 434.78 ops/s (± 3.6%) 400 ops/s (± 8.2%) 0.92
data-client: move-item 212.77 ops/s (± 8.1%) 202.04 ops/s (± 6.1%) 0.95

This comment was automatically generated by workflow using github-action-benchmark.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.10%. Comparing base (467a5f6) to head (3eaf5f2).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3876   +/-   ##
=======================================
  Coverage   98.10%   98.10%           
=======================================
  Files         153      153           
  Lines        2899     2904    +5     
  Branches      564      564           
=======================================
+ Hits         2844     2849    +5     
  Misses         11       11           
  Partials       44       44           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Benchmark

Details
Benchmark suite Current: 3eaf5f2 Previous: 467a5f6 Ratio
normalizeLong 446 ops/sec (±1.40%) 454 ops/sec (±1.16%) 1.02
normalizeLong Values 407 ops/sec (±0.27%) 413 ops/sec (±0.34%) 1.01
denormalizeLong 283 ops/sec (±3.05%) 291 ops/sec (±2.38%) 1.03
denormalizeLong Values 260 ops/sec (±2.35%) 263 ops/sec (±2.52%) 1.01
denormalizeLong donotcache 994 ops/sec (±0.18%) 1004 ops/sec (±0.18%) 1.01
denormalizeLong Values donotcache 745 ops/sec (±0.19%) 751 ops/sec (±0.14%) 1.01
denormalizeShort donotcache 500x 1586 ops/sec (±0.13%) 1599 ops/sec (±0.12%) 1.01
denormalizeShort 500x 838 ops/sec (±2.33%) 849 ops/sec (±2.31%) 1.01
denormalizeShort 500x withCache 6301 ops/sec (±1.10%) 6326 ops/sec (±0.11%) 1.00
queryShort 500x withCache 2718 ops/sec (±0.22%) 2676 ops/sec (±0.44%) 0.98
buildQueryKey All 54089 ops/sec (±0.35%) 54520 ops/sec (±0.33%) 1.01
query All withCache 6070 ops/sec (±0.26%) 6705 ops/sec (±0.25%) 1.10
denormalizeLong with mixin Entity 279 ops/sec (±2.07%) 275 ops/sec (±2.32%) 0.99
denormalizeLong withCache 7754 ops/sec (±0.22%) 6826 ops/sec (±0.15%) 0.88
denormalizeLong Values withCache 5017 ops/sec (±0.12%) 5043 ops/sec (±0.58%) 1.01
denormalizeLong All withCache 5848 ops/sec (±0.25%) 6426 ops/sec (±0.24%) 1.10
denormalizeLong Query-sorted withCache 6140 ops/sec (±0.17%) 6738 ops/sec (±0.13%) 1.10
denormalizeLongAndShort withEntityCacheOnly 1776 ops/sec (±0.28%) 1677 ops/sec (±0.18%) 0.94
denormalize bidirectional 50 5723 ops/sec (±2.16%) 5868 ops/sec (±1.91%) 1.03
denormalize bidirectional 50 donotcache 42268 ops/sec (±0.45%) 41809 ops/sec (±0.77%) 0.99
getResponse 4628 ops/sec (±0.76%) 4524 ops/sec (±0.62%) 0.98
getResponse (null) 10704259 ops/sec (±0.70%) 10391914 ops/sec (±1.16%) 0.97
getResponse (clear cache) 265 ops/sec (±2.06%) 267 ops/sec (±1.99%) 1.01
getSmallResponse 3305 ops/sec (±1.13%) 3161 ops/sec (±1.56%) 0.96
getSmallInferredResponse 2523 ops/sec (±0.22%) 2443 ops/sec (±0.51%) 0.97
getResponse Collection 4528 ops/sec (±0.50%) 4560 ops/sec (±0.38%) 1.01
get Collection 4703 ops/sec (±0.30%) 4567 ops/sec (±0.50%) 0.97
get Query-sorted 5239 ops/sec (±0.19%) 5219 ops/sec (±0.24%) 1.00
setLong 457 ops/sec (±0.22%) 458 ops/sec (±0.30%) 1.00
setLongWithMerge 258 ops/sec (±0.24%) 257 ops/sec (±0.25%) 1.00
setLongWithSimpleMerge 271 ops/sec (±0.45%) 273 ops/sec (±0.43%) 1.01
setSmallResponse 500x 944 ops/sec (±0.19%) 934 ops/sec (±0.21%) 0.99

This comment was automatically generated by workflow using github-action-benchmark.

@ntucker ntucker merged commit 7d28629 into master Apr 6, 2026
27 checks passed
@ntucker ntucker deleted the perf-globalcache-preallocate branch April 6, 2026 01:24
@github-actions github-actions Bot mentioned this pull request Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant