MetaMask / MetaMask/metamask-mobile

Spike: Web Socket Performance Audit

Open
#21,476 0 comments 0 reactions 0 assignees View on GitHub
area-performance INVALID-ISSUE-TEMPLATE team-mobile-platform
Dominant language
TypeScript
Stars
3k
Forks
1.7k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

# WebSocket Balance Updates - Performance Review Spike

## TL;DR

WebSocket implementation provides real-time balance updates that:
- **Reduces backend load** by 40-97% through decreased Token Balances polling frequency (3min → 5min) and event-driven updates
- **Improves UX** with instant balance visibility instead of waiting for polling cycles
- **For user-initiated transactions:** UX improvement may be less dramatic since hooks already provide updates, but still **reduces data bandwidth** through selective balance updates and decreased polling frequency
- **Critical for non-user-initiated transactions** (received transfers, bridge deposits, on-ramp purchases, perps withdrawals) where hooks don't fire—WebSocket is the only real-time update mechanism
- **Future architecture:** While this first version maintains polling/hooks for reliability, it serves as a scaffold for future iterations where polling/hooks can be removed when WebSocket is active, further reducing backend load

---

## What is this about?

This spike reviews performance improvements from WebSocket-based real-time token balance updates on **MetaMask Mobile** (iOS & Android).

## Key Benefits

- **Real-time updates** - Instant balance visibility after transactions
- **Reduced network traffic** - Polling intervals: 3min → 5min (AccountsAPI/JSONRPC)
- **Better battery efficiency** - Fewer background HTTP requests
- **Intelligent fallback** - Automatic polling when WebSocket unavailable

## What WebSocket Does

- Real-time balance updates for all transaction types
- Works alongside existing hooks (doesn't replace them)
- Reduces background polling frequency

**Important:** WebSocket supplements existing hooks—both systems run independently.

**Future Architecture:** This first version maintains pooling/hooks for reliability and serves as a scaffold for future iterations. Once WebSocket stability is proven, a later iteration can remove polling/hooks when WebSocket is active, further reducing backend load and client-side overhead.

---

## Why WebSocket? Hook & Polling Costs

### Transaction Hooks Are Costly

Transaction confirmation hooks (`NotificationManager._confirmedCallback`) trigger HTTP polling that is expensive in multiple dimensions:

**How Hooks Work:**
- `TransactionController` tracks pending transactions using `PendingTransactionTracker`
- **Block tracker** polls for latest block and emits events that trigger polling for transaction receipts
- Block tracker has a polling interval (typically 10-20 seconds) - it does **NOT** track every single block
- On **fast blockchains** (Arbitrum ~0.25s blocks), the block tracker may skip many blocks between polls
- However, when transactions confirm, hooks still fire for each confirmed transaction
- **Problem:** Even with block tracker polling intervals, confirmed transactions trigger expensive HTTP polling (AccountsAPI/JSONRPC) for balance updates

**Message Size:**
- **JSONRPC:** Each token requires separate `eth_call` → N individual HTTP requests with full request/response headers
- **AccountsAPI:** Single batch call from client, but **costly at the backend level** → Backend must execute multiple `eth_call` requests to blockchain nodes, aggregate results, and query databases

**Backend Load:**
- **JSONRPC:** N separate RPC calls to blockchain nodes (one per token)
- **AccountsAPI:** Batch calls still translate to N blockchain RPC calls on backend
- **Database Queries:** Backend must query databases for token metadata, prices, and account state
- **Processing:** Each poll cycle requires parsing, validation, and aggregation across multiple data sources

**Scale Impact:**
- Every user transaction triggers these costly operations
- **Fast blockchains amplify this** - more blocks = more hook triggers = more HTTP requests
- High-frequency traders on fast chains multiply this cost exponentially
- Backend infrastructure must handle burst load during network congestion

### Background Polling Is Costly

Regular polling (every 3 minutes) compounds these costs:

- **480 AccountsAPI calls/day** → 480 batch requests → thousands of backend blockchain calls
- **10,080+ JSONRPC calls/day** (20 tokens) → 10,080+ individual RPC requests
- Continuous DB queries even when no balance changes occurred
- Backend must process and return data regardless of whether balances changed

### WebSocket Reduces These Costs

- **Event-driven:** Only sends updates when balances actually change
- **Selective updates:** Only changed token balances, not full portfolio
- **Backend efficiency:** Backend can push updates without client polling
- **Reduced DB load:** Fewer queries when leveraging blockchain event streams
- **Reduced polling:** 3min → 5min intervals = 40% reduction in background load

---

## Scenario

### Test Scenarios for Performance Profiling

#### Scenario 1: Single Native Token Balance Update (In-App Transaction)

**Context:** User sends ETH from within MetaMask Mobile (Send flow or Swap)

**Before (HTTP Polling + Transaction Hook):**
- User completes ETH transfer
- Transaction confirmed hook fires → `NotificationManager._confirmedCallback` triggers
- `AccountTrackerController.refresh()` → HTTP request (AccountsAPI or JSONRPC)
- `TokenBalancesController.updateBalances()` → HTTP request (AccountsAPI or JSONRPC)
- Balance updated and displayed
- **Network:** 2 HTTP requests

**After (WebSocket + Transaction Hook):**
- User completes ETH transfer (user-initiated transaction)
- **WebSocket event arrives** (provides balance update independently)
- Balance updated in state from WebSocket
- **Transaction confirmed hook fires** (operates independently)
- Hook triggers HTTP polling as normal (`AccountTrackerController.refresh()`, `TokenBalancesController.updateBalances()` etc.)
- **Network:** Hooks still make HTTP requests (WebSocket doesn't prevent this)
- **Key difference:** User may see balance update from WebSocket while hooks execute in parallel

**Important:** WebSocket provides balance updates independently. Transaction hooks still execute and make HTTP requests normally. WebSocket does NOT prevent or reduce hook-triggered HTTP requests.

**Measure:** Latency improvement, network load, battery impact, WebSocket vs hook timing

---

#### Scenario 2: Multiple Token Updates (Mixed Native + ERC20)

**Context:** User sends/receives ETH + 3 ERC20 tokens in single transaction or across multiple transactions

**Before (HTTP Polling + Transaction Hooks):**
- Transaction confirmed → `NotificationManager._confirmedCallback` fires
- Triggers multiple HTTP requests:
- `AccountTrackerController.refresh()` → 1 HTTP request (AccountsAPI or JSONRPC)
- `TokenBalancesController.updateBalances()` → 1 HTTP request (AccountsAPI or JSONRPC batch)
- `TokenDetectionController.detectTokens()` → 1 HTTP request (ERC20 only)
- **Total:** 3 HTTP requests
- 3 separate state updates → Multiple UI re-renders

**After (WebSocket + Transaction Hooks):**
- WebSocket event arrives with all balance changes
- Single atomic state update (ETH + ERC20 tokens at once) from WebSocket
- Single UI re-render from WebSocket update
- **Transaction hook still fires** and makes HTTP requests (3 requests as before)
- **Key benefit:** User sees atomic balance update from WebSocket while hooks execute in parallel

**Important:** WebSocket provides atomic balance updates. Transaction hooks still execute and make the same 3 HTTP requests. WebSocket does NOT reduce hook-triggered network requests. The benefit is atomic balance updates to UI, not reduced network load.

**Measure:** Update latency, render count, UI smoothness, atomic vs sequential updates

---

#### Scenario 3: High-Frequency Trading Activity (Mobile Power User)

**Setup:**
- Power user actively trading (10 transactions per minute)
- Portfolio with 20+ tokens across multiple chains
- Mix of user-initiated swaps (6/min) and non-user-initiated balance changes (4/min)

**Before (HTTP Polling + Transaction Hooks):**
- **User-initiated transactions (6/min):** Each triggers hooks
- 6 × (AccountTracker + TokenBalances + TokenDetection) = **18 HTTP requests/min from hooks alone**
- **Non-user-initiated transactions (4/min):** No hooks, rely on polling (AccountsAPI or JSONRPC)
- Received transfers, contract interactions, yields, etc.
- Continuous polling every 3min = **~7 HTTP requests/min** (20+ tokens)
- **Total:** **25+ HTTP requests/min**
- **Issues:** Battery drain from constant HTTP, racing conditions, stale data for non-user-initiated transactions

**After (WebSocket + Transaction Hooks):**
- **User-initiated transactions (6/min):**
- WebSocket delivers balance updates
- **Hooks still fire and make 18 HTTP requests/min** (same as before)
- **Non-user-initiated transactions (4/min):**
- WebSocket events provide updates (not possible before)
- **Backup polling:** 5-minute interval = **~1 HTTP request/min** (fallback only)
- **Total:** **18 HTTP from hooks + 1 from polling + WebSocket = ~19 requests/min**

**Important:** WebSocket does NOT reduce hook-triggered HTTP requests. All 18 hook HTTP requests still occur. WebSocket adds real-time updates for non-user-initiated transactions.

**Measure:** Request reduction, CPU usage, battery drain, app responsiveness, racing conditions

---

#### Scenario 4: New Token Detection (Receiving New ERC20 Token)

**Context:** User receives an ERC20 token they've never held before (e.g., receives LINK for first time)

**Before (HTTP Polling + Transaction Hooks):**

**In-app receive:**
- Transaction confirmed → `NotificationManager._confirmedCallback` fires
- `TokenDetectionController.detectTokens()` triggered → HTTP request
- New token discovered → Metadata read from TokenListController cache
- `TokenBalancesController.updateBalances()` triggered → HTTP balance fetch
- **2 HTTP requests** (detection + balance; metadata cached)

**Non-user-initiated receive (airdrop, sent by friend):**
- No hooks triggered (not user-initiated)
- Wait for polling cycle (AccountsAPI or JSONRPC)
- Detection runs → Balance fetch
- **1-2 HTTP requests**
- User sees: Nothing → Eventually token appears

**After (WebSocket + Transaction Hooks):**
- WebSocket event received → **Balance included in event**
- Balance stored → **Shown with generic token icon**
- Token not tracked → Triggers detection asynchronously (doesn't block)
- Detection runs in background → Metadata loaded
- Icon/details update smoothly

**Measure:** Time to visibility, detection latency, progressive loading UX, works for all transaction types

---

#### Scenario 5: Network Instability / WebSocket Disconnect

**Context:** WebSocket connection fails or becomes unstable; system must fall back gracefully

**Test Sequence:**

1. **WebSocket Active → Sudden Disconnect**
- Connection drops (WiFi → Cellular, server restart, app backgrounded)
- Transaction hooks must work independently

2. **Fallback Period (WebSocket Down)**
- Transaction hooks continue to function
- **Polling:** Interval reduces from 5min back to 3min (AccountsAPI or JSONRPC)
- **Result:** Degraded but functional (same as pre-WebSocket behavior)

3. **WebSocket Reconnection**
- Connection restored
- Missed events delivered (if backend queued them)
- Polling interval increases back to 5min
- Real-time updates resume

**Polling Interval Transitions:**
```
Normal: WebSocket Active → 5min polling (backup only)

Disconnect Detected (5s debounce) → 3min polling (active fallback)

Reconnect + Stable (5s debounce) → 5min polling (backup only)
```

**Important:** Hooks MUST function independently of WebSocket. Hooks are the fallback, not optional.

**Measure:** Fallback response time, data integrity, reconnection time, polling transitions, hook independence

---

#### Scenario 6: Large Portfolio (100+ Tokens) with AccountsAPI

**Setup:**
- Mobile user with 100+ ERC20 tokens across multiple chains
- User performs 5 transactions (1 native ETH, 4 different ERC20 tokens)
- **AccountsAPI enabled** - makes single batch call for all token balances

**Before (HTTP Polling + Transaction Hooks with AccountsAPI):**

**Polling baseline:**
- **1 AccountsAPI call** every 3min (fetches ALL 100+ token balances in single request)
- **480 requests/day** for background polling
- Much more efficient than individual token calls
- Still significant battery drain from frequency

**Transaction hooks (5 transactions):**
- Each transaction triggers:
- `AccountTrackerController.refresh()` → 1 HTTP request for native balance
- `TokenBalancesController.updateBalances()` → **1 AccountsAPI call** (batch fetches ALL 100+ tokens)
- `TokenDetectionController.detectTokens()` → 1 HTTP request (for ERC20)
- **5 transactions = 15 HTTP requests** (5 × 3 controllers)
- Each AccountsAPI call must process large response (100+ tokens)

**Total impact:** Hooks (15) + daily polling (480) = **495 requests/day**

**After (WebSocket + Transaction Hooks):**
- **WebSocket events (5 transactions):**
- Each event contains **ONLY changed balances** (1-2 tokens per event, not 100+)
- Selective updates → Small payloads
- **Transaction hooks (5 transactions):**
- **Hooks still fire and make all HTTP requests** (same as before)
- **15 HTTP requests from hooks** (including 5 AccountsAPI calls)
- **Polling frequency reduced:** 3min → 5min backup only (480 → 288 requests/day)

**Important:** WebSocket does NOT reduce hook-triggered HTTP requests. All 15 hook HTTP requests (including AccountsAPI calls) still occur. Benefit is selective updates + reduced polling frequency.

**Measure:** Payload size, processing time, backend latency, battery drain, selective vs full portfolio updates

---

#### Scenario 7: User-Initiated Transactions (Standard Swap, Send, DApp)

**Use Case:** User initiates transaction through MetaMask Mobile (swap, send, DApp approval)

**Before (HTTP Polling + Transaction Hooks):**
- Transaction confirms on-chain
- Hook fires (`NotificationManager._confirmedCallback`)
- Hook triggers polling (AccountsAPI or JSONRPC)
- Balance updated in UI after HTTP requests complete

**After (WebSocket + Transaction Hooks):**
- Transaction confirms on-chain
- **WebSocket event received**
- Balance updated from WebSocket (user sees this)
- **Hook fires** (in parallel)
- **Hook still makes HTTP requests** as before

**Important:** WebSocket provides balance update to user. Transaction hooks still execute and make HTTP requests normally. Both systems operate independently.

**Measure:** Transaction completion latency, user-perceived completion time, WebSocket vs hook timing

---

#### Scenario 8: Smart Transaction Swaps (Gasless Swaps via Relayer)

**Use Case:** User initiates gasless swap where relayer submits transaction on-chain

**Key Points:**
- Smart transactions flow through TransactionController once `minedHash` received
- Same hooks and events fire as regular transactions
- Only difference: relayer submits instead of direct user broadcast

**Before:** Relayer submits → Transaction confirmed → Hooks fire → HTTP polling → Balance updated

**After:** Relayer submits → Transaction confirmed → WebSocket → Instant balance update + Hooks execute

**Measure:** Latency from `minedHash` to balance visibility, relayer vs WebSocket timing

---

#### Scenario 9: EIP-7702 Gasless Transactions (Delegation-Based Gas Payment)

**Use Case:** User initiates transaction where gas paid by another token via EIP-7702 delegations

**Key Points:**
- EIP-7702 transactions flow through TransactionController once `transactionHash` received
- Same hooks and events fire as regular transactions
- Uses Delegation7702PublishHook and relay service for submission

**Before:** Relay submits → Transaction confirmed → Hooks fire → HTTP polling → Balance updated

**After:** Relay submits → Transaction confirmed → WebSocket → Instant balance update + Hooks execute

**Measure:** Latency from `transactionHash` to balance visibility, delegation setup time, relay vs WebSocket timing

---

### Non-User-Initiated Transaction Scenarios (The Hook Gap)

**Context:** Scenarios 10-13 highlight use cases where transaction confirmation hooks DON'T fire because the transaction wasn't initiated by the user through MetaMask's TransactionController. These scenarios represent the critical gap that WebSocket fills.

**Why Hooks Don't Help:**
- `NotificationManager._confirmedCallback` only fires for user-initiated transactions
- Non-user-initiated transactions = No `TransactionController:transactionConfirmed` event
- **Only AccountsAPI/JSONRPC polling or WebSocket can detect balance changes from these**

---

#### Scenario 10: Received Transfers (Friend Sends Tokens)

**Use Case:** User receives tokens sent directly from another wallet

**Mobile App State:** App backgrounded or closed, user not expecting transfer

**Before (HTTP Polling ONLY - No Hooks):**
- Friend sends tokens
- **No hooks triggered** (not user-initiated)
- User opens MetaMask later
- App resumes → Polling cycle starts (AccountsAPI or JSONRPC)
- Balance updates after polling completes

**After (WebSocket):**
- Friend sends tokens
- **WebSocket event received**
- Balance updated in background

**Measure:** Detection latency, balance accuracy on app open, background operation reliability

---

#### Scenario 11: Bridge Transactions (Cross-Chain Transfers)

**Use Case:** User bridges assets from one chain to another (e.g., ETH mainnet → Arbitrum)

**Mobile App State:** User initiated bridge on external service, app closed during completion (5-15 min)

**Before (HTTP Polling ONLY - No Hooks):**
- Bridge transaction completes
- **No hooks triggered** (external bridge)
- User opens MetaMask
- App resumes → Polling starts (AccountsAPI or JSONRPC)
- Balance updates after polling completes

**After (WebSocket):**
- Bridge transaction completes
- **WebSocket event received**
- Destination chain balance updated

**Measure:** Cross-chain latency, balance accuracy when user checks, support queries about "missing funds"

---

#### Scenario 12: On-Ramp Purchases (Fiat → Crypto)

**Use Case:** User purchases crypto via on-ramp provider (MoonPay, Wyre, Transak)

**Mobile App State:** Purchase on external service, app closed during processing (2-15 min)

**Before (HTTP Polling ONLY - No Hooks):**
- Payment processes, crypto sent to wallet
- **No hooks triggered** (external on-ramp)
- User opens MetaMask to check
- App resumes → Polling starts (AccountsAPI or JSONRPC)
- Balance updates after polling completes

**After (WebSocket):**
- Payment processes, crypto sent to wallet
- **WebSocket event received**
- Balance updated

**Measure:** On-ramp latency, first-time user confidence, support tickets for "missing purchases"

---

#### Scenario 13: Perps Withdrawals (Hyperliquid Integration)

**Use Case:** User withdraws funds from Hyperliquid perpetuals exchange back to wallet

**Real-World Context:**
- Withdrawal is contract-initiated (Hyperliquid contract → user wallet)
- Not tracked by TransactionController

**Before (HTTP Polling ONLY - No Hooks):**
- User withdraws from Hyperliquid
- **No hooks triggered** (contract-initiated, not user-initiated through MetaMask)
- User opens MetaMask to verify
- App resumes → Polling cycle starts (AccountsAPI or JSONRPC)
- Balance updates after polling completes

**After (WebSocket):**
- User withdraws from Hyperliquid
- **WebSocket event received**
- Balance updated

**Measure:** Perps withdrawal latency, user anxiety during high-value withdrawals, Hyperliquid integration UX

---

## Design

### Performance Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ BEFORE (HTTP Polling Only) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ AccountsAPI (Every 3 minutes): │
│ ┌────────────────────────────────────────┐ │
│ │ HTTP Request to Accounts API │ │
│ │ - Returns all account balances │ │
│ │ - Native + Token balances │ │
│ └────────────────────────────────────────┘ │
│ │
│ JSONRPC (Every 3 minutes): │
│ ┌────────────────────────────────────────┐ │
│ │ 1. eth_getBalance (Native Balance) │ │
│ │ 2. eth_call (Token 1 Balance) │ │
│ │ 3. eth_call (Token 2 Balance) │ │
│ │ ... │ │
│ │ N. eth_call (Token N Balance) │ │
│ └────────────────────────────────────────┘ │
│ │
│ Total per cycle: N × HTTP latency │
│ Network overhead: N × (request + response size) │
│ State updates: N (one per token) │
│ UI re-renders: N (one per update) │
│ │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ AFTER (WebSocket + Backup Polling) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ On Transaction/Balance Change: │
│ ┌────────────────────────────────────────┐ │
│ │ 1. WebSocket Event (All Tokens) │ │
│ │ 2. Single Atomic State Update │ │
│ │ 3. Single UI Re-render │ │
│ └────────────────────────────────────────┘ │
│ │
│ Backup Polling (Every 5 minutes): │
│ ┌────────────────────────────────────────┐ │
│ │ HTTP Requests (only if WS failed) │ │
│ └────────────────────────────────────────┘ │
│ │
│ Network overhead: 1 WebSocket message │
│ State updates: 1 (atomic) │
│ UI re-renders: 1 │
│ Polling frequency: Reduced (3min → 5min when WS active) │
│ │
└─────────────────────────────────────────────────────────────────┘
```

### Performance Optimization Points

1. **Atomic State Updates**
- All token balances updated in single operation
- Reduces state change notifications
- Prevents partial/intermediate states

2. **Selective Updates**
- Only changed balances trigger state updates
- Address checksum validation cached
- Balance comparison before update

3. **Intelligent Polling**
- Increases interval when WebSocket active
- Restores default when WebSocket down
- Debounced status changes prevent thrashing

4. **Efficient Error Handling**
- Errors don't block other updates
- Fallback polling per-chain
- Silent recovery

---

## Technical Details

### Performance Metrics to Capture

#### 1. Network Performance

**HTTP Polling Baseline:**

**AccountsAPI:**
```javascript
Polling Interval: 3 minutes (180 seconds)
Requests per cycle: 1 (returns all balances)
Request size: ~500 bytes (with headers)
Response size: ~200-1000 bytes per token
Daily requests: 480 requests/day per account
Daily bandwidth: ~10-25 MB/day per account
```

**JSONRPC:**
```javascript
Polling Interval: 3 minutes (180 seconds)
Requests per cycle: 1 (eth_getBalance) + N (eth_call per token)
Request size: ~500 bytes per request (with headers)
Response size: ~200-1000 bytes per token
Daily requests (20 tokens): 10,080 requests/day per account (21 requests × 480 cycles)
Daily bandwidth: ~10-25 MB/day per account
```

**WebSocket Performance:**
```javascript
WebSocket connection: Persistent (minimal overhead)
Message size: ~500-2000 bytes (multiple tokens)
Messages per transaction: 1
Backup polling: Every 300 seconds (83% reduction)
Daily requests (backup only): 288 requests/day per account
Daily bandwidth: ~5-15 MB/day per account
Savings: ~85-90% bandwidth reduction
```

#### 2. Key Metrics to Measure

| Metric | Before | After | Expected Improvement |
|--------|--------|-------|---------------------|
| Balance Latency | Polling-dependent | Event-driven | Instant updates |
| State Updates | N individual | 1 atomic | N → 1 |
| UI Renders | N renders | 1 render | Reduced flicker |
| Polling (AccountsAPI) | 480 requests/day | 288 requests/day | 40% reduction |
| Polling (JSONRPC 20 tokens) | 10,080 requests/day | 288 requests/day | 97% reduction |
| Battery/CPU | High frequency | Low frequency | Measurable reduction |

### Testing Approach

**Load Testing:** 1, 10, 50, 100+ token updates
**Network Testing:** Message size, reconnection, bandwidth
**Mobile Testing:** Battery, app resume, background ops
**Tools:** Xcode Instruments, Android Studio Profiler, React Native Performance Monitor

---

## Threat Modeling Framework

| Risk | Threat | Likelihood | Mitigation |
|------|--------|------------|------------|
| **Message Flooding** | Excessive balance updates | **Unlikely** (blockchain constraints limit transaction frequency, even on faster chains) | Rate limiting, debouncing, circuit breaker |
| **Large Updates** | 1000+ tokens in single update | **Unlikely** (blockchain gas limits and practical user portfolios constrain this) | Chunking, virtual scrolling, pagination |
| **Memory Leaks** | Subscriptions not cleaned up | Moderate | Proper cleanup, timer clearance, profiling |
| **Fallback Thrashing** | Rapid connect/disconnect | Moderate | 5s debounce, exponential backoff, jitter |

**Security:** Maintain all validations (balance, checksum, CAIP) despite performance overhead

---

## Acceptance Criteria

### Performance Metrics
- [ ] Balance update latency (transaction → UI)
- [ ] State update time (10 vs 100+ tokens)
- [ ] Network reduction (requests/day)
- [ ] Memory stability (1000+ updates)
- [ ] CPU/battery efficiency
- [ ] Polling frequency changes
- [ ] UI responsiveness

### Mobile-Specific
- [ ] App resume balance visibility
- [ ] External transaction detection (perps, bridge, on-ramp)
- [ ] Battery drain comparison
- [ ] WebSocket reconnection time
- [ ] Low power mode functionality
- [ ] Network condition handling
- [ ] App lifecycle transitions

### Functional
- [ ] All existing tests pass
- [ ] Fallback polling works
- [ ] No data loss during failures
- [ ] Atomic state updates
- [ ] Debouncing prevents thrashing

### Testing
- [ ] Performance tests for critical paths
- [ ] Load tests (50+, 100+ tokens)
- [ ] Mobile profiling (iOS & Android)
- [ ] External transaction scenarios
- [ ] Before vs After benchmarks documented

### Stakeholder review needed before the work gets merged

- [x] Engineering (needed in most cases)
- [ ] Design
- [x] Product
- [ ] QA (automation tests are required to pass before merging PRs but not all changes are covered by automation tests - please review if QA is needed beyond automation tests)
- [ ] Security
- [ ] Legal
- [ ] Marketing
- [ ] Management (please specify)
- [ ] Other (please specify)

### References

_No response_

Contributor guide

Open the contributing guide

Research direction

Start by locating NotificationManager._confirmedCallback, AccountTrackerController.refresh(), TokenBalancesController.updateBalances(), and the WebSocket balance-update entry point. Profile the listed transaction, token, fallback, and large-portfolio scenarios on iOS and Android. Done means measured latency, request volume, rendering, battery, reconnection, and data-integrity results are documented against the stated baseline.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile-dev, performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.