Skip to content

QuoteRow is not memoized and receives an inline onPress per item in QuoteList #31367

Description

@andrepimenta

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    To be fixed

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions