MetaMask / MetaMask/metamask-mobile

Memoize new Map result in selectRelatedChainIdsByTransactionId

Open
#31,354 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area-performance Sev3 size-S ta-triaged team-confirmations team-transactions
Dominant language
TypeScript
Stars
3k
Forks
1.7k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

> **Performance audit finding** · Severity: **Medium** · Effort: Easy · Fix risk: Simple · Test safety net: Partial
> Owner: `@MetaMask/confirmations`
> File: `app/selectors/transactionController.ts:120`

### What is this about?

`selectRelatedChainIdsByTransactionId` is a plain `createSelector` whose result function returns a freshly-built `new Map(...)`. A Map is a new reference on every recompute, and two Maps with identical entries are never `===`. The result also nests `new Set` per transaction. The selector recomputes whenever the transactions array changes.

**Why it matters**

The transactions array changes on virtually every transaction lifecycle event. Each recompute returns a brand-new Map (and new inner arrays), breaking memoization for every consumer even when the related-chain mapping is unchanged.

### Scenario

N/A — see Technical Details.

### Design

N/A — internal performance change; no UI/design impact.

### Technical Details

**Evidence**

`app/selectors/transactionController.ts:120`
```ts
export const selectRelatedChainIdsByTransactionId = createSelector(
selectTransactionsStrict,
(transactions) => {
const transactionsById = new Map(
transactions.map((tx) => [tx.id, tx]),
);
return new Map(
transactions
.map((tx) => { /* ... */ return [tx.id, [...new Set(chainIds)]] ... })
.filter(([, chainIds]) => chainIds.length > 0),
);
},
);
```

**Fix**

Return a plain object (`Record`) so `createDeepEqualSelector` can deep-compare and keep the reference stable, and reconstruct a Map at the call site if a Map is required:
```ts
export const selectRelatedChainIdsByTransactionId = createDeepEqualSelector(
selectTransactionsStrict,
(transactions) => {
const result: Record = {};
// ...populate result[tx.id] = [...] ...
return result;
},
);
```

### Threat Modeling Framework

N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.

### Acceptance Criteria

- `app/selectors/transactionController.test.ts:197` asserts entries via `.get(...)` but not referential stability. Add a test asserting `toBe` across two equal-content transaction arrays. Profiler: confirm consumers stop re-rendering on unrelated transaction updates.

### References

- File: `app/selectors/transactionController.ts:120`
- Source: MetaMask Mobile performance audit — finding `selector-related-chain-ids-by-transaction-id`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations
- Status: **UNVALIDATED**

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading app/selectors/transactionController.ts:120 and run the related tests in app/selectors/transactionController.test.ts:197. Inspect selector call sites to verify whether they require a Map, then update the selector and its test so equal-content transaction arrays preserve the same result reference without changing entries.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
mobile, performance, testing-qa
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.