Conversation
20fb03a to
69fb1f5
Compare
69fb1f5 to
4a9c5cb
Compare
|
@danjm this is ready for review—I've added it to CI but I'm still debugging it. It does pass locally so maybe we can leave out the CI job for now? |
|
@whymarrh I agree we can leave out the CI for now. |
|
Can you add some basic documentation for running these tests locally? |
|
Do we need to commit all of these fixture files/ |
danjm
left a comment
There was a problem hiding this comment.
Code looks fine. Suggested addition of documentation and questioned the committing of all the fixture files.
da163e5 to
de52680
Compare
Whatever is needed for a test to run should be committed to the repo, yes. It's the best solution I can come up with, but I'm open to alternatives.
I've added brief info for running the tests locally and for generating the fixture files. |
…bal cascading re-renders (#39312) ## **Description** `getPendingApprovals` was creating a new array on every call via `Object.values()`, breaking memoization in the Routes component and triggering cascade re-renders across ~50-60 child components on every state change. **Changes:** - Wrapped `getPendingApprovals` in `createSelector` to memoize the `Object.values()` transformation - Added helper function `getPendingApprovalsObject` to extract the pendingApprovals object - Returns stable reference when underlying `pendingApprovals` object is unchanged **Implementation:** ```typescript // Before: new array every call export function getPendingApprovals(state: ApprovalsMetaMaskState) { return Object.values(state.metamask.pendingApprovals ?? {}); } // After: memoized transformation const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) => state.metamask.pendingApprovals ?? {}; export const getPendingApprovals = createSelector( getPendingApprovalsObject, (approvals) => Object.values(approvals), ); ``` **Testing:** - Added unit tests verifying reference equality on repeated calls with identical state - Tests verify invalidation when pendingApprovals actually change - All existing tests pass (664 selector tests across 31 suites) - Covers edge cases (empty object, null/undefined values) ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by fixing approvals selector memoization ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/6673 - Part of [#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) — Break Global Re-render Cascade ## **Manual testing steps** 1. Open MetaMask extension 2. Navigate to any page that triggers state changes (e.g., switching networks, accounts) 3. Observe improved responsiveness compared to before (Routes component and children no longer re-render unnecessarily) 4. Verify approvals functionality works correctly (no behavioral changes) ## **Screenshots/Recordings** ### **Before** N/A - Performance improvement (internal optimization, no UI changes) ### **After** N/A - Performance improvement (internal optimization, no UI changes) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Optimizes approvals selectors to avoid unnecessary renders by stabilizing outputs and handling empty/null states. > > - Wraps `getPendingApprovals` with `createSelector` and adds `getPendingApprovalsObject` using `EMPTY_OBJECT` fallback > - Converts `pendingApprovalsSortedSelector` to a memoized selector that sorts a copied array > - Leaves existing APIs intact while returning stable references when `pendingApprovals` is unchanged > - Adds unit tests validating memoization, change invalidation, and empty/null behavior > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4984401. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
…bal cascading re-renders (#39312) ## **Description** `getPendingApprovals` was creating a new array on every call via `Object.values()`, breaking memoization in the Routes component and triggering cascade re-renders across ~50-60 child components on every state change. **Changes:** - Wrapped `getPendingApprovals` in `createSelector` to memoize the `Object.values()` transformation - Added helper function `getPendingApprovalsObject` to extract the pendingApprovals object - Returns stable reference when underlying `pendingApprovals` object is unchanged **Implementation:** ```typescript // Before: new array every call export function getPendingApprovals(state: ApprovalsMetaMaskState) { return Object.values(state.metamask.pendingApprovals ?? {}); } // After: memoized transformation const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) => state.metamask.pendingApprovals ?? {}; export const getPendingApprovals = createSelector( getPendingApprovalsObject, (approvals) => Object.values(approvals), ); ``` **Testing:** - Added unit tests verifying reference equality on repeated calls with identical state - Tests verify invalidation when pendingApprovals actually change - All existing tests pass (664 selector tests across 31 suites) - Covers edge cases (empty object, null/undefined values) ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by fixing approvals selector memoization ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/6673 - Part of [#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) — Break Global Re-render Cascade ## **Manual testing steps** 1. Open MetaMask extension 2. Navigate to any page that triggers state changes (e.g., switching networks, accounts) 3. Observe improved responsiveness compared to before (Routes component and children no longer re-render unnecessarily) 4. Verify approvals functionality works correctly (no behavioral changes) ## **Screenshots/Recordings** ### **Before** N/A - Performance improvement (internal optimization, no UI changes) ### **After** N/A - Performance improvement (internal optimization, no UI changes) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Optimizes approvals selectors to avoid unnecessary renders by stabilizing outputs and handling empty/null states. > > - Wraps `getPendingApprovals` with `createSelector` and adds `getPendingApprovalsObject` using `EMPTY_OBJECT` fallback > - Converts `pendingApprovalsSortedSelector` to a memoized selector that sorts a copied array > - Leaves existing APIs intact while returning stable references when `pendingApprovals` is unchanged > - Adds unit tests validating memoization, change invalidation, and empty/null behavior > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4984401. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
… to prevent global cascading re-renders (#39313) ## **Description** Fixed `pendingConfirmationsSortedSelector` to use proper memoization with `createSelector`. This selector was creating new arrays on every call, breaking memoization and triggering cascade re-renders throughout the confirmation flow and Routes components. **What is the reason for the change?** The selector was implemented as a plain function that returned new array references on every invocation (via `.filter()` and `.sort()`), causing unnecessary re-renders in all dependent components even when the underlying state hadn't changed. **What is the improvement/solution?** Wrapped the selector with `createSelector` from reselect to memoize the transformation. Now the selector only recalculates when `getPendingApprovals` returns a different reference, preventing cascade re-renders and improving performance across the confirmation flow. ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by fixing confirmations selector memoization ## **Related issues** Fixes: Part of P0 effort to break global re-render cascade (#6669) ## **Manual testing steps** 1. This is a performance optimization with no visible UI changes 2. The memoization behavior is verified through unit tests 3. All existing confirmation flow functionality remains unchanged ## **Screenshots/Recordings** N/A - This is a performance optimization with no visual changes. The improvement is verified through unit tests that confirm the selector returns the same reference when state is unchanged. ### **Before** Selector created new arrays on every call, causing unnecessary re-renders: ```typescript export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) { return getPendingApprovals(state) .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) .sort((a1, a2) => a1.time - a2.time); } ``` ### **After** Selector uses memoization to return stable references: ```typescript export const pendingConfirmationsSortedSelector = createSelector( getPendingApprovals, (approvals) => approvals .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) .sort((a1, a2) => a1.time - a2.time), ); ``` ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[P0] Fix `pendingConfirmationsSortedSelector` (unmemoized)</issue_title> > <issue_description>## Summary > > Fix `pendingConfirmationsSortedSelector` to use proper memoization. This is a **P0 critical fix** required to break the global re-render cascade. > > **Parent Epic:** [#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) — Break Global Re-render Cascade > **Depends on:** Fix for `getPendingApprovals` (#6673) > > ## Problem > > `pendingConfirmationsSortedSelector` is a plain function that creates 2 new arrays on every call: > > ```typescript > // ui/pages/confirmations/selectors/confirm.ts > export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) { > return getPendingApprovals(state) > .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) // ❌ NEW ARRAY > .sort((a1, a2) => a1.time - a2.time); // ❌ NEW ARRAY > } > ``` > > **Used in Routes** to determine pending confirmation count and navigation. > > This breaks memoization and causes cascade re-renders across all children on every state change. > > ## Solution > > Wrap in `createSelector` to memoize the transformation: > > ```typescript > export const pendingConfirmationsSortedSelector = createSelector( > getPendingApprovals, > (approvals) => > approvals > .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) > .sort((a1, a2) => a1.time - a2.time), > ); > ``` > > ## Files > > - `ui/pages/confirmations/selectors/confirm.ts` > > ## Acceptance Criteria > > - [ ] Selector returns same reference when underlying state unchanged > - [ ] Unit test verifies memoization > - [ ] No regression in existing tests > > ## Estimated Effort > > 0.5 hours > > ## Labels > > `perf`, `selector`, `P0`, `team-confirmations`</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes MetaMask/MetaMask-planning#6674 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Improves selector performance and adds tests around confirmation sorting/filtering. > > - **Memoizes** `pendingConfirmationsSortedSelector` with `reselect` using `getPendingApprovals`, preserving sort-by-time and confirmation-type filtering while returning stable references > - **Tests**: verify sorted output, memoization (same reference when unchanged, new reference on state change), and filtering of non-confirmation approvals; `oldestPendingConfirmationSelector` behavior validated > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 88f2be0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
…bal cascading re-renders (#39312) ## **Description** `getPendingApprovals` was creating a new array on every call via `Object.values()`, breaking memoization in the Routes component and triggering cascade re-renders across ~50-60 child components on every state change. **Changes:** - Wrapped `getPendingApprovals` in `createSelector` to memoize the `Object.values()` transformation - Added helper function `getPendingApprovalsObject` to extract the pendingApprovals object - Returns stable reference when underlying `pendingApprovals` object is unchanged **Implementation:** ```typescript // Before: new array every call export function getPendingApprovals(state: ApprovalsMetaMaskState) { return Object.values(state.metamask.pendingApprovals ?? {}); } // After: memoized transformation const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) => state.metamask.pendingApprovals ?? {}; export const getPendingApprovals = createSelector( getPendingApprovalsObject, (approvals) => Object.values(approvals), ); ``` **Testing:** - Added unit tests verifying reference equality on repeated calls with identical state - Tests verify invalidation when pendingApprovals actually change - All existing tests pass (664 selector tests across 31 suites) - Covers edge cases (empty object, null/undefined values) ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by fixing approvals selector memoization ## **Related issues** Fixes: MetaMask/MetaMask-planning#6673 - Part of [#6669](MetaMask/MetaMask-planning#6669) — Break Global Re-render Cascade ## **Manual testing steps** 1. Open MetaMask extension 2. Navigate to any page that triggers state changes (e.g., switching networks, accounts) 3. Observe improved responsiveness compared to before (Routes component and children no longer re-render unnecessarily) 4. Verify approvals functionality works correctly (no behavioral changes) ## **Screenshots/Recordings** ### **Before** N/A - Performance improvement (internal optimization, no UI changes) ### **After** N/A - Performance improvement (internal optimization, no UI changes) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Optimizes approvals selectors to avoid unnecessary renders by stabilizing outputs and handling empty/null states. > > - Wraps `getPendingApprovals` with `createSelector` and adds `getPendingApprovalsObject` using `EMPTY_OBJECT` fallback > - Converts `pendingApprovalsSortedSelector` to a memoized selector that sorts a copied array > - Leaves existing APIs intact while returning stable references when `pendingApprovals` is unchanged > - Adds unit tests validating memoization, change invalidation, and empty/null behavior > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4984401. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
… to prevent global cascading re-renders (#39313) ## **Description** Fixed `pendingConfirmationsSortedSelector` to use proper memoization with `createSelector`. This selector was creating new arrays on every call, breaking memoization and triggering cascade re-renders throughout the confirmation flow and Routes components. **What is the reason for the change?** The selector was implemented as a plain function that returned new array references on every invocation (via `.filter()` and `.sort()`), causing unnecessary re-renders in all dependent components even when the underlying state hadn't changed. **What is the improvement/solution?** Wrapped the selector with `createSelector` from reselect to memoize the transformation. Now the selector only recalculates when `getPendingApprovals` returns a different reference, preventing cascade re-renders and improving performance across the confirmation flow. ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by fixing confirmations selector memoization ## **Related issues** Fixes: Part of P0 effort to break global re-render cascade (#6669) ## **Manual testing steps** 1. This is a performance optimization with no visible UI changes 2. The memoization behavior is verified through unit tests 3. All existing confirmation flow functionality remains unchanged ## **Screenshots/Recordings** N/A - This is a performance optimization with no visual changes. The improvement is verified through unit tests that confirm the selector returns the same reference when state is unchanged. ### **Before** Selector created new arrays on every call, causing unnecessary re-renders: ```typescript export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) { return getPendingApprovals(state) .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) .sort((a1, a2) => a1.time - a2.time); } ``` ### **After** Selector uses memoization to return stable references: ```typescript export const pendingConfirmationsSortedSelector = createSelector( getPendingApprovals, (approvals) => approvals .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) .sort((a1, a2) => a1.time - a2.time), ); ``` ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[P0] Fix `pendingConfirmationsSortedSelector` (unmemoized)</issue_title> > <issue_description>## Summary > > Fix `pendingConfirmationsSortedSelector` to use proper memoization. This is a **P0 critical fix** required to break the global re-render cascade. > > **Parent Epic:** [#6669](MetaMask/MetaMask-planning#6669) — Break Global Re-render Cascade > **Depends on:** Fix for `getPendingApprovals` (#6673) > > ## Problem > > `pendingConfirmationsSortedSelector` is a plain function that creates 2 new arrays on every call: > > ```typescript > // ui/pages/confirmations/selectors/confirm.ts > export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) { > return getPendingApprovals(state) > .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) // ❌ NEW ARRAY > .sort((a1, a2) => a1.time - a2.time); // ❌ NEW ARRAY > } > ``` > > **Used in Routes** to determine pending confirmation count and navigation. > > This breaks memoization and causes cascade re-renders across all children on every state change. > > ## Solution > > Wrap in `createSelector` to memoize the transformation: > > ```typescript > export const pendingConfirmationsSortedSelector = createSelector( > getPendingApprovals, > (approvals) => > approvals > .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType)) > .sort((a1, a2) => a1.time - a2.time), > ); > ``` > > ## Files > > - `ui/pages/confirmations/selectors/confirm.ts` > > ## Acceptance Criteria > > - [ ] Selector returns same reference when underlying state unchanged > - [ ] Unit test verifies memoization > - [ ] No regression in existing tests > > ## Estimated Effort > > 0.5 hours > > ## Labels > > `perf`, `selector`, `P0`, `team-confirmations`</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes MetaMask/MetaMask-planning#6674 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Improves selector performance and adds tests around confirmation sorting/filtering. > > - **Memoizes** `pendingConfirmationsSortedSelector` with `reselect` using `getPendingApprovals`, preserving sort-by-time and confirmation-type filtering while returning stable references > - **Tests**: verify sorted output, memoization (same reference when unchanged, new reference on state change), and filtering of non-confirmation approvals; `oldestPendingConfirmationSelector` behavior validated > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 88f2be0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
…eferences (#39311) ## **Description** This PR fixes a critical performance issue where `useParams()` and `useLocation()` hooks return new object references on every render, even when values are identical. The `withRouterHooks` HOC was passing these unstable references directly to wrapped components, breaking `React.memo` optimization and triggering cascade re-renders across ~40 components per cycle, with multiple cascade cycles (5+) per user action. **What is the reason for the change?** The unstable object references from router hooks were causing unnecessary re-renders throughout the application, significantly slowing down all user actions. **What is the improvement/solution?** Added memoization to stabilize `params` and `location` prop references: - For `params`: Track keys and values as separate string primitives to detect actual content changes - For `location`: Track individual properties (pathname, search, hash, state) to detect route changes - Both use `useMemo` to return the same object reference when content hasn't changed ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by stabilizing props references used in routes ## **Related issues** Part of Break Global Re-render Cascade epic ## **Manual testing steps** 1. Use the extension and navigate between different routes 2. Perform user actions that trigger route changes (e.g., sending transactions, switching accounts) 3. With performance profiling or React DevTools, observe that wrapped components using `withRouterHooks` no longer re-render unnecessarily when route values haven't changed 4. Verify all existing functionality works as expected (navigation, routing, etc.) ## **Screenshots/Recordings** N/A - This is a performance optimization with no visible UI changes. The improvement is observable through React DevTools Profiler showing reduced re-render counts. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[P0] Memoize `withRouterHooks` HOC (params/location stability)</issue_title> > <issue_description>## Summary > > Stabilize object references from `useParams()` and `useLocation()` in `withRouterHooks` HOC. This is a **P0 critical fix** required to break the global re-render cascade. > > **Parent Epic:** [#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) — Break Global Re-render Cascade > > ## Problem > > `withRouterHooks` HOC passes `useParams()` and `useLocation()` results directly to wrapped components. These hooks return **new object references on every render** even when the values are identical, breaking `React.memo` and causing cascade re-renders. > > **WDYR Analysis shows:** > - `props.params` flagged as "different objects that are equal by value" > - This triggers on ~40 components per cascade cycle > - Multiple cascade cycles per user action (5+) > > ## Current Code > > ```typescript > // ui/helpers/higher-order-components/with-router/with-router.component.js > const ComponentWithRouterHooks = (props) => { > const hookNavigate = useNavigate(); > const hookLocation = useLocation(); > const hookParams = useParams(); > > return ( > <WrappedComponent > navigate={props.navigate ?? hookNavigate} > location={props.location ?? hookLocation} > params={props.params ?? hookParams} // New object every render! > /> > ); > }; > ``` > > ## Solution > > Memoize router props to maintain stable references: > > ```typescript > import { useMemo } from 'react'; > > const ComponentWithRouterHooks = (props) => { > const hookNavigate = useNavigate(); > const hookLocation = useLocation(); > const hookParams = useParams(); > > // Stabilize params object > const stableParams = useMemo( > () => props.params ?? hookParams, > [props.params, JSON.stringify(hookParams)], > ); > > // Stabilize location object > const stableLocation = useMemo( > () => props.location ?? hookLocation, > [ > props.location, > hookLocation.pathname, > hookLocation.search, > hookLocation.hash, > hookLocation.state, > ], > ); > > return ( > <WrappedComponent > {...props} > navigate={props.navigate ?? hookNavigate} > location={stableLocation} > params={stableParams} > /> > ); > }; > ``` > > ## Files > > - `ui/helpers/higher-order-components/with-router/with-router.component.js` > > ## Acceptance Criteria > > - [ ] `params` and `location` props maintain stable references when values unchanged > - [ ] Unit tests verify memoization works correctly > - [ ] No regression in routing behavior > - [ ] WDYR no longer flags "different objects that are equal by value" for params/location > > ## Estimated Effort > > 2 hours > > ## Labels > > `perf`, `HOC`, `P0`, `team-extension-platform`</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes MetaMask/MetaMask-planning#6671 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches a widely used routing HOC and changes how `location` updates propagate (ignoring `location.key`), which could affect components that implicitly relied on key-based updates. Logic is straightforward and covered by new unit tests, but regressions would be broadly visible in navigation-related UI. > > **Overview** > `withRouterHooks` now memoizes router-derived props so wrapped components receive **referentially stable** `params` and `location` values when their meaningful contents haven’t changed. > > This introduces a new `useShallowEqualityCheck` hook for shallow-reference stabilization (used for `params`) and adds `useLocationStable` to keep `location` stable while *intentionally ignoring* `location.key` changes. Tests were expanded to assert memoization behavior, precedence of explicitly passed `params`/`location` props, and to cover edge cases like comma-containing param values and same-path navigations where only `location.key` changes. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7fd9991. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
…eferences (#39311) ## **Description** This PR fixes a critical performance issue where `useParams()` and `useLocation()` hooks return new object references on every render, even when values are identical. The `withRouterHooks` HOC was passing these unstable references directly to wrapped components, breaking `React.memo` optimization and triggering cascade re-renders across ~40 components per cycle, with multiple cascade cycles (5+) per user action. **What is the reason for the change?** The unstable object references from router hooks were causing unnecessary re-renders throughout the application, significantly slowing down all user actions. **What is the improvement/solution?** Added memoization to stabilize `params` and `location` prop references: - For `params`: Track keys and values as separate string primitives to detect actual content changes - For `location`: Track individual properties (pathname, search, hash, state) to detect route changes - Both use `useMemo` to return the same object reference when content hasn't changed ## **Changelog** CHANGELOG entry: Fixed critical performance issue slowing down all user actions by stabilizing props references used in routes ## **Related issues** Part of Break Global Re-render Cascade epic ## **Manual testing steps** 1. Use the extension and navigate between different routes 2. Perform user actions that trigger route changes (e.g., sending transactions, switching accounts) 3. With performance profiling or React DevTools, observe that wrapped components using `withRouterHooks` no longer re-render unnecessarily when route values haven't changed 4. Verify all existing functionality works as expected (navigation, routing, etc.) ## **Screenshots/Recordings** N/A - This is a performance optimization with no visible UI changes. The improvement is observable through React DevTools Profiler showing reduced re-render counts. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[P0] Memoize `withRouterHooks` HOC (params/location stability)</issue_title> > <issue_description>## Summary > > Stabilize object references from `useParams()` and `useLocation()` in `withRouterHooks` HOC. This is a **P0 critical fix** required to break the global re-render cascade. > > **Parent Epic:** [#6669](MetaMask/MetaMask-planning#6669) — Break Global Re-render Cascade > > ## Problem > > `withRouterHooks` HOC passes `useParams()` and `useLocation()` results directly to wrapped components. These hooks return **new object references on every render** even when the values are identical, breaking `React.memo` and causing cascade re-renders. > > **WDYR Analysis shows:** > - `props.params` flagged as "different objects that are equal by value" > - This triggers on ~40 components per cascade cycle > - Multiple cascade cycles per user action (5+) > > ## Current Code > > ```typescript > // ui/helpers/higher-order-components/with-router/with-router.component.js > const ComponentWithRouterHooks = (props) => { > const hookNavigate = useNavigate(); > const hookLocation = useLocation(); > const hookParams = useParams(); > > return ( > <WrappedComponent > navigate={props.navigate ?? hookNavigate} > location={props.location ?? hookLocation} > params={props.params ?? hookParams} // New object every render! > /> > ); > }; > ``` > > ## Solution > > Memoize router props to maintain stable references: > > ```typescript > import { useMemo } from 'react'; > > const ComponentWithRouterHooks = (props) => { > const hookNavigate = useNavigate(); > const hookLocation = useLocation(); > const hookParams = useParams(); > > // Stabilize params object > const stableParams = useMemo( > () => props.params ?? hookParams, > [props.params, JSON.stringify(hookParams)], > ); > > // Stabilize location object > const stableLocation = useMemo( > () => props.location ?? hookLocation, > [ > props.location, > hookLocation.pathname, > hookLocation.search, > hookLocation.hash, > hookLocation.state, > ], > ); > > return ( > <WrappedComponent > {...props} > navigate={props.navigate ?? hookNavigate} > location={stableLocation} > params={stableParams} > /> > ); > }; > ``` > > ## Files > > - `ui/helpers/higher-order-components/with-router/with-router.component.js` > > ## Acceptance Criteria > > - [ ] `params` and `location` props maintain stable references when values unchanged > - [ ] Unit tests verify memoization works correctly > - [ ] No regression in routing behavior > - [ ] WDYR no longer flags "different objects that are equal by value" for params/location > > ## Estimated Effort > > 2 hours > > ## Labels > > `perf`, `HOC`, `P0`, `team-extension-platform`</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes MetaMask/MetaMask-planning#6671 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches a widely used routing HOC and changes how `location` updates propagate (ignoring `location.key`), which could affect components that implicitly relied on key-based updates. Logic is straightforward and covered by new unit tests, but regressions would be broadly visible in navigation-related UI. > > **Overview** > `withRouterHooks` now memoizes router-derived props so wrapped components receive **referentially stable** `params` and `location` values when their meaningful contents haven’t changed. > > This introduces a new `useShallowEqualityCheck` hook for shallow-reference stabilization (used for `params`) and adds `useLocationStable` to keep `location` stable while *intentionally ignoring* `location.key` changes. Tests were expanded to assert memoization behavior, precedence of explicitly passed `params`/`location` props, and to cover edge cases like comma-containing param values and same-path navigations where only `location.key` changes. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7fd9991. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
Closes #6548
This PR adds a e2e test using fixture data that exercises the send flow, sending 1 ETH from one account to another on a local test chain.