MetaMask / MetaMask/metamask-extension
[State Sync Redesign 1.3] Preserve controller keys in transport, remove debounce, add `requestAnimationFrame` batching
- Dominant language
- TypeScript
- Stars
- 13.2k
- Forks
- 5.6k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 451
Description
Replace background-side accumulation + 200ms debounce with per-controller-keyed patch delivery, background-side microtask batching, and UI-side `requestAnimationFrame` batching.
## Background changes
- In `setupControllerConnection`, subscribe to each controller via messenger.
- On `stateChange`, normalize and sanitize patches inline.
- Accumulate `{ [controllerKey]: patches }` in a pending batch. Flush via `queueMicrotask` — one port write per synchronous cascade, not one per controller.
- A single user action that touches N controllers synchronously produces 1 port write, not N.
## UI changes
- In `ui/index.js`, collect incoming keyed-patch messages in a pending batch.
- Flush on next `requestAnimationFrame` (~16ms). Multiple messages arriving within one frame are coalesced.
- Flatten `Record` to `Patch[]` for existing `updateMetamaskState` Redux dispatch.
- Attach `changedControllers: string[]` to the action payload (available for future use).
## Two-layer batching
| Layer | Batches | Latency | Handles |
|---|---|---|---|
| Background `queueMicrotask` | Synchronous cascades within one action | ~0ms | One action → N controllers |
| UI `requestAnimationFrame` | Multiple messages arriving within one frame | ≤16ms (React 17), sub-frame (React 18) | Async bursts, rapid-fire actions |
**Combined worst-case latency:** action completes → `stateChange` synchronously → microtask flush (~0ms) → IPC (sub-ms) → UI rAF (≤16ms) = **≤17ms**.
## Steps
1. In `setupControllerConnection`, subscribe to each controller via messenger. On `stateChange`, accumulate `{ [controllerKey]: patches }` in a pending batch.
2. Flush the pending batch via `queueMicrotask` — write one message per microtask boundary to `outStream`.
3. In `ui/index.js`, add pending batch queue + `requestAnimationFrame` flush before dispatching to Redux.
4. Remove `PatchStore` class.
5. Remove `this.sendUpdate = debounce(...)` and `privateSendUpdate`.
6. Retain Redux dispatch path (flatten keyed patches for existing `updateMetamaskState`).
## Files
- `app/scripts/metamask-controller.js`
- `app/scripts/lib/setupControllerConnection.ts` (or wherever per-controller subscriptions are wired)
- `app/scripts/lib/PatchStore.ts`
- `ui/index.js`
## Validation
- [ ] Latency improvement measurable in React DevTools profiler (200ms → ≤17ms)
- [ ] E2E tests pass with two-layer batching
- [ ] Controller keys visible in transport (debug logging)
- [ ] Startup burst: ~50+ controllers initializing produces ≤2-3 port writes (not 50+)
- [ ] Account sync burst: 100+ changes to same controller coalesced per frame on UI side
- [ ] No port message queue growth under sustained load (memory stable)
## Risk
Medium — changes the timing of all UI updates. Validate with E2E tests. Rollback: revert to debounce.
## Dependencies
None (Issues 1.1-1.2 are independent)
## Outcome
Latency drops from 0-200ms to 0-17ms. Controller keys preserved in transport. Redux path unchanged. Port message volume controlled via microtask coalescing.
## Context
Part of [State Sync Redesign — Epic 1: Transport optimization](#40059). See [ADR transport batching strategy](https://github.com/MetaMask/decisions/pull/124) for full design rationale.
Contributor guide
Research direction
Read app/scripts/lib/setupControllerConnection.ts, app/scripts/lib/PatchStore.ts, app/scripts/metamask-controller.js, and ui/index.js to trace current subscriptions, debounce, and Redux dispatch flow. Run the existing E2E tests before and after the change; done means keyed transport, microtask and requestAnimationFrame batching, the PatchStore and debounce paths removed, and the listed startup and account-sync behavior validated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, redux, typescript
- Domain
- backend, frontend, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100