Skip to content

Conversation

n3ps
Copy link
Contributor

@n3ps n3ps commented Oct 16, 2025

Description

When selectors return empty arrays or objects using the ?? or || operators, they create new instances every time the function is called if the value is undefined/null. This breaks reference equality and memoization and can cause excess re-renders.

Changelog

CHANGELOG entry: chore: use stable empty references

Related issues

Fixes:

Manual testing steps

  1. Go to this page...

Screenshots/Recordings

Before

useSelector(state => state.some.collection || []) // new array each time

After

// stable instance
const EMPTY = []

useSelector(state => state.some.collection || EMPTY)

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Replaces ad-hoc []/{} fallbacks with shared frozen constants across selectors to stabilize memoization and references.

  • Selectors:
    • Alerts: selectAlerts now returns EMPTY_ARRAY fallback.
    • Assets: use EMPTY_OBJECT for getMetamaskState and in balance-related selectors (account tree, rates, tokens, currency rates) instead of {}; remove local EMPTY_OBJ in favor of shared constant.
    • NFTs: default allNftContracts/allNfts to EMPTY_OBJECT.
    • General selectors: selectNftsByChainId and selectNonZeroUnusedApprovalsAllowList now return EMPTY_ARRAY when empty.
  • Shared utils:
    • Add ui/selectors/shared.ts exporting frozen EMPTY_ARRAY and EMPTY_OBJECT; import and use where needed.

Written by Cursor Bugbot for commit 9272811. This will update automatically on new commits. Configure here.

Copy link
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@n3ps n3ps added the team-core-extension-ux Core Extension UX team label Oct 16, 2025
@n3ps n3ps marked this pull request as ready for review October 16, 2025 00:10
@n3ps n3ps enabled auto-merge October 16, 2025 00:10
cursor[bot]

This comment was marked as outdated.

@metamaskbot
Copy link
Collaborator

📊 Page Load Benchmark Results

Current Commit: 970ded4 | Date: 10/16/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.06s (±77ms) 🟡 | historical mean value: 1.05s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 747ms (±74ms) 🟢 | historical mean value: 734ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 77ms (±14ms) 🟢 | historical mean value: 80ms ⬇️ (historical data)
📈 Detailed Results
Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.06s 77ms 1.02s 1.41s 1.30s 1.41s
domContentLoaded 747ms 74ms 711ms 1.08s 959ms 1.08s
firstPaint 77ms 14ms 60ms 200ms 88ms 200ms
firstContentfulPaint 77ms 14ms 60ms 200ms 88ms 200ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms

Results generated automatically by MetaMask CI

@metamaskbot
Copy link
Collaborator

Builds ready [970ded4]
UI Startup Metrics (1250 ± 72 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyHomeuiStartup1250111015147212911388
load107495612726811161212
domContentLoaded106895212666811081206
domInteractive1913123121739
firstPaint75180128441510931162
backgroundConnect2512372909255272
firstReactRender26184872743
getState1553971932
initialActions51496615
loadScripts822693100166859944
setupStore962231015
WebpackHomeuiStartup8347121155728521015
load62857796971631829
domContentLoaded61957194267625782
domInteractive161166101437
firstPaint17354928171190604
backgroundConnect22106792742
firstReactRender27165483135
getState1043851115
initialActions3013246
loadScripts61756993165623770
setupStore951631113
FirefoxBrowserifyHomeuiStartup14561275191911815171663
load1221107314408212761377
domContentLoaded1221107314408212761376
domInteractive1062629250114247
firstPaintNaNNaNNaNNaNNaNNaN
backgroundConnect332168103857
firstReactRender30256463140
getState12420627818
initialActions51647415
loadScripts1199105314118112501340
setupStore12565111146
WebpackHomeuiStartup16021380210813916511945
load1356120716078714041512
domContentLoaded1355120616078714041512
domInteractive100303897297344
firstPaintNaNNaNNaNNaNNaNNaN
backgroundConnect371992134260
firstReactRender352698113653
getState104569930
initialActions121228374125
loadScripts1332118015768613771492
setupStore166193251161
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 58 Bytes (0%)
  • ui: 329 Bytes (0%)
  • common: 662 Bytes (0.01%)

Copy link
Contributor

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

When a variable is destructured from ?? {}, the purpose is to safely assign it with undefined without triggering the "property of undefined" JS TypeError.

Since the reference of the empty object in these cases are never used beyond each destructuring assignment, and also not by React, these aren't cases where the EMPTY_OBJECT substitution would be useful.

Copy link
Contributor

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

Intermediate variables not used outside of the selector, or downstream by React.

Copy link
Contributor

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

createSelector memoizes its output based on its input arguments.

https://reselect.js.org/api/weakmapmemoize/

So the references of create{,DeepEqual}Selector output are already stable without needing these changes.

@MajorLift
Copy link
Contributor

👍 Similar optimizations might be more impactful if they're made in React components and hooks to reactive variables that require referential equality rather than upstream in the selectors.

@n3ps n3ps force-pushed the n3ps/reselect-empty branch from 970ded4 to 9272811 Compare October 16, 2025 14:31
@n3ps
Copy link
Contributor Author

n3ps commented Oct 16, 2025

createSelector memoizes its output based on its input arguments.

https://reselect.js.org/api/weakmapmemoize/

So the references of create{,DeepEqual}Selector output are already stable without needing these changes.

Thanks for the review @MajorLift! Updated the objects, but I think the arrays are still valid:

https://reselect.js.org/usage/handling-empty-array-results/

@n3ps n3ps requested a review from MajorLift October 16, 2025 14:35
@metamaskbot
Copy link
Collaborator

📊 Page Load Benchmark Results

Current Commit: 9272811 | Date: 10/16/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.00s (±74ms) 🟡 | historical mean value: 1.05s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 702ms (±85ms) 🟢 | historical mean value: 737ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 79ms (±40ms) 🟢 | historical mean value: 77ms ⬆️ (historical data)
📈 Detailed Results
Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.00s 74ms 956ms 1.32s 1.24s 1.32s
domContentLoaded 702ms 85ms 659ms 1.24s 944ms 1.24s
firstPaint 79ms 40ms 60ms 472ms 88ms 472ms
firstContentfulPaint 79ms 40ms 60ms 472ms 88ms 472ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms

Results generated automatically by MetaMask CI

@metamaskbot
Copy link
Collaborator

Builds ready [9272811]
UI Startup Metrics (1260 ± 57 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyHomeuiStartup1260114614645712931347
load108297912245111131170
domContentLoaded107595112185211071163
domInteractive18144551726
firstPaint63080123144411021154
backgroundConnect25824237213262269
firstReactRender2817129142942
getState1663882131
initialActions60577719
loadScripts82471097050854915
setupStore1074551119
WebpackHomeuiStartup807677101765836931
load61054584773616780
domContentLoaded60254084272607774
domInteractive161170101437
firstPaint17251781163167569
backgroundConnect21104172734
firstReactRender25163673235
getState941731115
initialActions308248
loadScripts59953884072605772
setupStore1051631215
FirefoxBrowserifyHomeuiStartup14271229203412714881665
load1206105914538212691347
domContentLoaded1206105914538212691347
domInteractive1013428847107219
firstPaintNaNNaNNaNNaNNaNNaN
backgroundConnect332290103851
firstReactRender30245963144
getState957510825
initialActions42193412
loadScripts1185104414278112461329
setupStore176235341059
WebpackHomeuiStartup16221440201312016811894
load1383123116308914381563
domContentLoaded1383123016308914371562
domInteractive101314106999333
firstPaintNaNNaNNaNNaNNaNNaN
backgroundConnect3921121184376
firstReactRender372688143685
getState945351016
initialActions62589417
loadScripts1357121616028614041546
setupStore13576121239
Bundle size diffs [🚀 Bundle size reduced!]
  • background: -71.86 KiB (-1.58%)
  • ui: 1.38 KiB (0.02%)
  • common: 57.44 KiB (0.69%)

Copy link
Contributor

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

LGTM! Going forward, React Compiler should take care of this codebase-wide for most relevant cases, but it would be good to see if there are exceptions where manually applying these changes would make a difference for performance.

@n3ps n3ps added this pull request to the merge queue Oct 16, 2025
Merged via the queue into main with commit eca2a3c Oct 16, 2025
170 checks passed
@n3ps n3ps deleted the n3ps/reselect-empty branch October 16, 2025 17:55
@github-actions github-actions bot locked and limited conversation to collaborators Oct 16, 2025
@metamaskbot metamaskbot added the release-13.6.0 Issue or pull request that will be included in release 13.6.0 label Oct 16, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-13.6.0 Issue or pull request that will be included in release 13.6.0 size-S team-core-extension-ux Core Extension UX team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants