MetaMask / MetaMask/metamask-mobile

Memoize SnapInterfaceContext value and stabilize its handler functions

Open Beginner friendly
#31,288 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev3 size-S ta-triaged team-core-platform
Dominant language
TypeScript
Stars
3k
Forks
1.7k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

> **Performance audit finding** · Severity: **High** · Effort: Easy · Fix risk: Simple · Test safety net: Partial
> Owner: `@MetaMask/core-platform`
> File: `app/components/Snaps/SnapInterfaceContext.tsx:172`

### What is this about?

`SnapInterfaceContextProvider` builds an inline object literal for `SnapInterfaceContext.Provider value={{ ... }}`, and every function inside it (`handleEvent`, `getValue`, `handleInputChange`, `setCurrentFocusedInput`) is re-created on every render because they are declared inline in the component body with no `useCallback`. Both the object and the function refs change every render.

**Why it matters**

`useSnapInterfaceContext` is consumed in ~18 files that render Snap UI components (inputs, dropdowns, buttons, forms). Snap interfaces re-render on every interface-state update; each update creates fresh handler refs and a fresh context object, forcing every Snap UI element in the tree to re-render even when its own value is unchanged. For form-heavy Snap dialogs this multiplies re-renders across the whole rendered interface.

### Scenario

N/A — see Technical Details.

### Design

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

### Technical Details

**Evidence**

`app/components/Snaps/SnapInterfaceContext.tsx:172`
```tsx
return (

```
The handlers are plain functions defined at `:118` (`handleEvent`), `:139` (`handleInputChange`), `:155` (`getValue`), `:167` (`setCurrentFocusedInput`) — none memoized.

**Fix**

Wrap each handler in `useCallback` (deps: `snapId`, `interfaceId`, `controllerMessenger`; the others read refs) and wrap the provider value in `useMemo`:
```tsx
const handleEvent = useCallback(..., [snapId, interfaceId]);
const handleInputChange = useCallback(..., [snapId, interfaceId]);
const getValue = useCallback(..., [initialState]);
const setCurrentFocusedInput = useCallback(..., []);
const value = useMemo(
() => ({ handleEvent, getValue, handleInputChange, setCurrentFocusedInput, focusedInput: focusedInput.current, snapId }),
[handleEvent, getValue, handleInputChange, setCurrentFocusedInput, snapId],
);
```
Note `focusedInput.current` is a ref read — it will only reflect updates when the provider re-renders, which is the existing behavior, so this is no regression.

### Threat Modeling Framework

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

### Acceptance Criteria

- - Run `yarn jest app/components/Snaps/SnapInterfaceContext.test.tsx`.
- Open a Snap dialog with multiple inputs, type in one input, and confirm with the Profiler that sibling inputs no longer re-render on each keystroke.

### References

- File: `app/components/Snaps/SnapInterfaceContext.tsx:172`
- Source: MetaMask Mobile performance audit — finding `context-snap-interface-inline-value`
- Owner (CODEOWNERS / best-effort): @MetaMask/core-platform
- Status: **UNVALIDATED**

Contributor guide

Open the contributing guide

Research direction

Start in app/components/Snaps/SnapInterfaceContext.tsx at the handler definitions and provider value around line 172, then read app/components/Snaps/SnapInterfaceContext.test.tsx. Run the named Jest test, apply the memoization change described in the issue, and verify with the Profiler that sibling inputs do not re-render on each keystroke in a multi-input Snap dialog.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile, performance
Issue type
Refactor
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.