Merged
Conversation
brunobar79
approved these changes
Jun 3, 2019
Contributor
brunobar79
left a comment
There was a problem hiding this comment.
Simple and elegant. 👏 👏 👏
Collaborator
10 tasks
7 tasks
github-merge-queue bot
pushed a commit
that referenced
this pull request
Jan 23, 2026
… 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>
wantedsystem
pushed a commit
that referenced
this pull request
Jan 27, 2026
… 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>
github-merge-queue bot
pushed a commit
that referenced
this pull request
Mar 11, 2026
…#39309) ## **Description** `ShieldSubscriptionProvider` was creating a new context value object on every render and recreating `evaluateCohortEligibility` whenever any of its 10 dependencies changed, causing unnecessary re-renders of the Home component subtree. Two fixes applied: 1. **Stabilized callback with ref pattern** -- Moved the `evaluateCohortEligibility` implementation into a ref updated via `useEffect`. The public callback delegates to the ref, keeping a stable reference (`useCallback` with empty deps) while still accessing current closure values. 2. **Memoized context value** -- Wrapped the provider value object in `useMemo` keyed on the now-stable callback, preventing a new object reference on every render. This is fix 6 of 6 in the [P0 Global Cascade Re-render Fixes epic](https://github.com/MetaMask/MetaMask-planning/issues/6669) ([RCA](https://docs.google.com/document/d/1KrzqL1IEQb-ctOKpOxhjBVr_-5jDXTgwC8a4r1ICBg0/edit?tab=t.5g1bh4tznel0), [Profiling Report](https://docs.google.com/document/d/1zlTg8rWYUFKZAsjQF3I9o7KaMA6uq7qcYjdfWKIZIJU/edit?tab=t.0#heading=h.dxmx8vunwu2s)). Together, the 6 memoization fixes break the cascade chain originating in Routes/Home that propagated through the entire component tree on every navigation and state update. [](https://codespaces.new/MetaMask/metamask-extension/pull/39309?quickstart=1) ## **Profiling Results** (all 6 fixes combined -- [Full Report](https://docs.google.com/document/d/1zlTg8rWYUFKZAsjQF3I9o7KaMA6uq7qcYjdfWKIZIJU/edit?tab=t.0#heading=h.dxmx8vunwu2s)) We ran 7 rounds of controlled A/B benchmarks over 41 hours. Results confirm the cascade was the dominant source of interactive UI latency. WDYR audit: 0 cascade re-renders on treatment (RCA measured 125-200 per action on baseline). > Measured on test builds (`yarn build:test`) with React StrictMode, Power User persona (30 accounts). All p-values from Welch's t-test, confirmed by Mann-Whitney U. Effect sizes: Cohen's d = 0.93-5.11 (large to very large). n=5-20 per metric. ### Per-interaction improvements | Flow | Baseline | Treatment | Improvement | p | |------|----------|-----------|-------------|---| | Account switching (5 accts) | 3.1s | 1.7s | **-1.4s, 47% faster** | 0.044 | | Asset detail nav (ETH) | 83ms | 37ms | **-46ms, 55% faster** | 0.044 | | Asset detail nav (SOL) | 75ms | 31ms | **-44ms, 59% faster** | <0.001 | | Send flow (e2e) | 928ms | 804ms | -124ms, 13% faster | 0.044 | | Send: token select to form | 25ms | 13ms | **-12ms, 49% faster** | 0.040 | ### Cascade compounding under sustained interaction Route cycling benchmark (3 round-trips: Send -> Assets -> Swap): | Segment | Baseline | Treatment | Improvement | p | |---------|----------|-----------|-------------|---| | **Total (6 transitions)** | **19.9s** | **2.7s** | **-17.2s, 7x faster** | 0.004 | | sendToHome | 6.4s | 723ms | -5.7s, 89% faster | 0.018 | | assetDetailsToHome | 6.3s | 659ms | -5.7s, 90% faster | 0.004 | | swapToHome | 5.0s | 787ms | -4.2s, 84% faster | 0.001 | | homeToSwap | 877ms | 227ms | -650ms, 74% faster | 0.002 | | homeToAssetDetails | 628ms | 236ms | -392ms, 62% faster | 0.004 | | homeToSend | 673ms | 82ms | -591ms, 88% faster | 0.046 | The 7x is a **compounding effect** across successive transitions, not a per-transition effect. Isolated transitions show only 10-15% improvement. Each navigation accumulates subscription/listener state on baseline, escalating re-render overhead -- consistent with the RCA's cascade chain model (Routes -> Home -> all children). This compounding behavior is itself a key finding: it means any remaining or future cascade leak will degrade **non-linearly** with session length, even if it looks trivial in a single benchmark. **Production estimate:** StrictMode roughly doubles render work. The 7x measured in dev builds is expected to land at ~2-7x in production depending on wallet size, tree depth, and GC timing. ### Metrics to watch after merge - **CI (`metamask-performance`):** `assetClickToPriceChart` should show a step-change (83->37ms dev). `cascadeFixProfiling` preset can be added for ongoing regression tracking. - **Production Sentry (RUM):** Best signal at **p75/p95 navigation latency** and **INP p95**, segmented by users with 10+ accounts. Expect improvement on route transitions, account switching, token list rendering. Signal will be diffuse due to network variance and device heterogeneity -- don't expect a clean step-function. - **TBT:** Cascade generates clusters of consecutive long tasks (>50ms) on every state change. Fix eliminates these. Should show up in Sentry performance dashboard once long-task tracking is enabled. ## **Changelog** CHANGELOG entry: Fix memoization issue in top-level context provider that was causing cascading re-renders. ## **Related issues** Fixes: MetaMask/MetaMask-planning#6638 Part of epic: [MetaMask-planning#6669 -- P0 Global Cascade Re-render Fixes](https://github.com/MetaMask/MetaMask-planning/issues/6669) Sibling PRs: - [#39310](#39310) -- Memoize MetaMetrics context ([#6640](https://github.com/MetaMask/MetaMask-planning/issues/6640)) - [#39311](#39311) -- Stabilize `withRouterHooks` params/location ([#6671](https://github.com/MetaMask/MetaMask-planning/issues/6671)) - [#39312](#39312) -- Fix `getPendingApprovals` selector ([#6673](https://github.com/MetaMask/MetaMask-planning/issues/6673)) - [#39313](#39313) -- Fix `pendingConfirmationsSortedSelector` ([#6674](https://github.com/MetaMask/MetaMask-planning/issues/6674)) - [#39314](#39314) -- Remove dead What's New modal code ([#6670](https://github.com/MetaMask/MetaMask-planning/issues/6670)) ## **Manual testing steps** ## **Screenshots/Recordings** ## **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] > **Medium Risk** > Changes how `evaluateCohortEligibility` is wired (ref-backed stable callback) and how the context value is memoized, which could alter when/with-what state eligibility checks run if the ref is not kept in sync. Added tests reduce risk but this touches subscription/cohort gating that affects modal display behavior. > > **Overview** > Reduces cascading re-renders from `ShieldSubscriptionProvider` by making the exported `evaluateCohortEligibility` function reference-stable and memoizing the context `value` object. > > Refactors `evaluateCohortEligibility` to delegate through a `useRef`-stored implementation (updated during render so it’s available in commit-phase callbacks) while keeping the public callback `useCallback([])`. Adds a new Jest test suite validating child rendering, stable callback/context identity across re-renders, and that the stable callback still observes updated selector values. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 50a2fed. 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR updates how sync with mobile works.
A new code will be generated each 30 seconds of QR view opened, if there is no communication with mobile app
Communication goes as follows:
cipherKeyandchannelNameto first communication with mobilecipherKeyandchannelName, for the extension to connect.