Performance audit finding · Severity: Medium · Effort: Easy · Fix risk: Simple · Test safety net: Partial
Owner: @MetaMask/swaps-engineers
File: app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx:41
What is this about?
QuoteList renders the quote-selector rows by mapping over data and spreading props into a non-memoized QuoteRow. Each QuoteRow subscribes to the store (useSelector(selectDestToken)) and runs useDisplayCurrencyValue (which itself reads 5 selectors). Because QuoteRow is not wrapped in React.memo and the parent rebuilds the data array (each element a fresh object including a fresh loading/selected flag) whenever quotes/loading change, all rows re-render and re-run their per-row selectors on every quote refresh tick.
Why it matters
Quotes refresh on a polling cadence (see getQuoteRefreshRate). On each refresh the QuoteSelectorView.data useMemo recomputes (depends on validQuotes, isLoading, etc.), producing new row prop objects, so every QuoteRow re-renders and each re-runs useDisplayCurrencyValue's 5 selector reads. With several quotes this is a multiplied cost on a live-updating list.
Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Evidence
app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx:8
export const QuoteList = ({ data }: Props) =>
data.map((quote) => <QuoteRow key={quote.quoteRequestId} {...quote} />);
app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx:41
export const QuoteRow = ({ ... }: QuoteRowProps) => {
const destToken = useSelector(selectDestToken);
const formattedReceiveAmountFiat = useDisplayCurrencyValue(receiveAmount, destToken);
// ...
<TouchableOpacity onPress={() => onPress(quoteRequestId)}> // inline arrow per render
Fix
- Wrap
QuoteRow in React.memo.
- Lift
useSelector(selectDestToken) (and ideally the currency/market reads in useDisplayCurrencyValue) into QuoteSelectorView once and pass the already-formatted strings down as props, so rows become pure presentational components that bail out via memo when their props are unchanged.
- Avoid recreating per-row prop objects when only an unrelated quote changed (compute
selected/loading so unchanged rows keep stable props).
Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
- Render
QuoteList with N quotes, trigger a refresh that changes only one quote, and assert (render-count spy) the other rows do not re-render. Profiler: confirm useDisplayCurrencyValue selector reads no longer fire for every row on each refresh.
References
- File:
app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx:41
- Source: MetaMask Mobile performance audit — finding
swaps-memoization-quote-row-not-memoized-inline-onpress
- Owner (CODEOWNERS / best-effort): @MetaMask/swaps-engineers
- Status: UNVALIDATED
What is this about?
QuoteListrenders the quote-selector rows by mapping overdataand spreading props into a non-memoizedQuoteRow. EachQuoteRowsubscribes to the store (useSelector(selectDestToken)) and runsuseDisplayCurrencyValue(which itself reads 5 selectors). BecauseQuoteRowis not wrapped inReact.memoand the parent rebuilds thedataarray (each element a fresh object including a freshloading/selectedflag) whenever quotes/loading change, all rows re-render and re-run their per-row selectors on every quote refresh tick.Why it matters
Quotes refresh on a polling cadence (see
getQuoteRefreshRate). On each refresh theQuoteSelectorView.datauseMemorecomputes (depends onvalidQuotes,isLoading, etc.), producing new row prop objects, so everyQuoteRowre-renders and each re-runsuseDisplayCurrencyValue's 5 selector reads. With several quotes this is a multiplied cost on a live-updating list.Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Evidence
app/components/UI/Bridge/components/QuoteSelectorView/QuoteList.tsx:8app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx:41Fix
QuoteRowinReact.memo.useSelector(selectDestToken)(and ideally the currency/market reads inuseDisplayCurrencyValue) intoQuoteSelectorViewonce and pass the already-formatted strings down as props, so rows become pure presentational components that bail out via memo when their props are unchanged.selected/loadingso unchanged rows keep stable props).Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
QuoteListwith N quotes, trigger a refresh that changes only one quote, and assert (render-count spy) the other rows do not re-render. Profiler: confirmuseDisplayCurrencyValueselector reads no longer fire for every row on each refresh.References
app/components/UI/Bridge/components/QuoteSelectorView/QuoteRow.tsx:41swaps-memoization-quote-row-not-memoized-inline-onpress