Commit 6fb3cb3
feat: add real-time WebSocket streaming to Perps with performance optimizations (MetaMask#18430)
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->
## **Description**
This PR implements WebSocket-based real-time data streaming for the
Perps feature, replacing the previous polling-based approach. The
implementation evolved significantly from the original plan to include
critical performance optimizations and architectural improvements that
eliminate unnecessary re-renders and provide flexible update control.
**Key improvements:**
1. **Pure WebSocket Architecture**: Migrated from hybrid REST/WebSocket
to pure WebSocket for all live data (prices, positions, orders, fills)
2. **Flexible Throttling**: Made throttling optional with smart defaults
- instant updates for user actions (orders/positions), throttled for
high-frequency data (prices)
3. **Re-render Optimization**: Created isolated leaf components for
frequently updating data to prevent parent component re-renders
4. **TP/SL Fix**: Fixed root cause of Take Profit/Stop Loss data not
displaying by extracting from `triggerPx` field instead of broken
`isPositionTpsl` flag
5. **WebSocket Pre-warming**: Pre-establishes positions and orders
subscriptions when entering Perps environment to eliminate empty initial
states
## **Changelog**
CHANGELOG entry: Fixed Perps live data updates and significantly
improved performance by implementing WebSocket streaming with optimized
re-rendering
**Latest fixes:**
- Eliminated duplicate WebSocket subscriptions by implementing shared
webData2 connection for positions and orders
- Single WebSocket connection now provides both positions (with TP/SL)
and orders data
- Fixed pre-warming to create persistent subscriptions that stay alive
throughout Perps session
- Pre-warm subscriptions now use no-op callbacks to maintain connections
and continuous caching
- Fixed reference counting with separate position/order subscriber
tracking to prevent premature disconnection
## **Related issues**
Fixes: Implementation of PR#3 from perps_stream_architecture_v3.md plan
## **Manual testing steps**
```gherkin
Feature: Perps WebSocket Streaming
Scenario: User views live price updates without UI lag
Given user is on the Perps Market Details view
And market prices are changing rapidly
When user observes the price display
Then prices update smoothly every 1-2 seconds
And parent components do not re-render
And UI remains responsive
Scenario: User places and cancels orders with instant feedback
Given user has the Orders tab open in Market Details
When user places a new order
Then order appears instantly in the list without delay
When user cancels an order
Then order disappears instantly from the list
Scenario: User views positions with TP/SL values
Given user has open positions with Take Profit and Stop Loss set
When user views the Positions tab
Then TP/SL values are displayed correctly for each position
And values update in real-time via WebSocket
Scenario: Positions are available immediately on mount
Given user navigates to Perps environment
When any component requests positions data
Then positions are available immediately from cache
And there is no empty state for ~10 seconds
And components render with data from the start
Scenario: Funding countdown updates without parent re-renders
Given user is viewing a market with funding payments
When funding countdown timer ticks
Then only the countdown text updates
And parent components remain stable (no re-renders)
```
## **Screenshots/Recordings**
### **Before**
- Excessive re-renders every second from funding countdown
- 30-second delay for order cancellation updates
- TP/SL values not displaying (isPositionTpsl always false)
- Parent components re-rendering on every price update
- Empty positions array for ~10 seconds on initial mount
### **After**
- Isolated countdown component - no parent re-renders
- Instant order updates (0ms throttle)
- TP/SL values correctly extracted and displayed
- Leaf components handle updates without affecting parents
- Positions and orders available immediately from pre-warmed cache
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
## Technical Details
### Files Changed Summary
**New Components (3):**
- `FundingCountdown` - Isolated timer component preventing parent
re-renders
- `LivePriceDisplay` - Real-time price display component
- `LivePriceHeader` - Market header with live price updates
**New Stream Hooks (4):**
- `usePerpsLiveOrders` - Real-time order updates (0ms default throttle)
- `usePerpsLivePositions` - Real-time position updates (0ms default
throttle)
- `usePerpsLivePrices` - Real-time price updates (1000ms default
throttle)
- `usePerpsLiveFills` - Real-time fill updates (0ms default throttle)
**Removed Polling Hooks (2):**
- `usePerpsOpenOrders` - Replaced by `usePerpsLiveOrders`
- `usePerpsPositions` - Replaced by `usePerpsLivePositions`
**Core Infrastructure:**
- `PerpsStreamManager` - Enhanced with optional throttling
- `HyperLiquidSubscriptionService` - Pure WebSocket implementation
### Performance Metrics
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| WebSocket Connections | N per component | 1 per data type | ~90%
reduction |
| Parent Re-renders | Every update | Never | 100% reduction |
| Order Update Latency | 30 seconds | Instant | 30s improvement |
| Price Update Frequency | Uncontrolled | 1-2 seconds | Balanced |
### Architecture Improvements
1. **Optional Throttling Pattern**
```typescript
// Instant updates for user actions
const orders = usePerpsLiveOrders({ throttleMs: 0 }); // default
// Throttled updates for high-frequency data
const prices = usePerpsLivePrices({ throttleMs: 1000 }); // default
```
2. **Leaf Component Pattern**
```typescript
// Before: Parent re-renders every second
<Text>{fundingCountdown}</Text>
// After: Only leaf component re-renders
<FundingCountdown />
```
3. **TP/SL Data Extraction**
```typescript
// Fixed: Extract from triggerPx instead of broken isPositionTpsl
if (order.triggerPx) {
if (order.orderType?.includes('Take Profit')) {
existing.takeProfitPrice = order.triggerPx;
} else if (order.orderType?.includes('Stop')) {
existing.stopLossPrice = order.triggerPx;
}
}
```
4. **Persistent Pre-warming with Single Connection**
```typescript
// Pre-warm creates REAL subscriptions that persist throughout session
public prewarm(): () => void {
// Creates subscription with no-op callback to keep connection alive
this.prewarmUnsubscribe = this.subscribe({
callback: () => {}, // Keeps connection alive for caching
throttleMs: 0
});
return this.prewarmUnsubscribe; // Cleanup function for when leaving Perps
}
```
5. **Shared webData2 Subscription with Proper Reference Counting**
```typescript
// Separate counters for positions and orders prevent premature disconnection
private positionSubscriberCount = 0;
private orderSubscriberCount = 0;
// Only cleanup when BOTH have zero subscribers
private cleanupSharedWebData2Subscription(): void {
const totalSubscribers = this.positionSubscriberCount + this.orderSubscriberCount;
if (totalSubscribers <= 0 && this.sharedWebData2Subscription) {
// Safe to disconnect - no subscribers left
}
}
```
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0a9c378 commit 6fb3cb3
79 files changed
Lines changed: 3692 additions & 1985 deletions
File tree
- app/components/UI/Perps
- Views
- PerpsMarketDetailsView
- PerpsOrderView
- PerpsPositionsView
- PerpsTabView
- components
- FundingCountdown
- LivePriceDisplay
- PerpsClosePositionBottomSheet
- PerpsLimitPriceBottomSheet
- PerpsMarketHeader
- PerpsMarketStatisticsCard
- PerpsMarketTabs
- PerpsOpenOrderCard
- PerpsPositionCard
- PerpsTPSLBottomSheet
- PerpsTabControlBar
- controllers
- providers
- types
- hooks
- stream
- providers
- __mocks__
- services
- utils
- locales/languages
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
81 | 101 | | |
82 | 102 | | |
83 | 103 | | |
84 | 104 | | |
85 | 105 | | |
| 106 | + | |
86 | 107 | | |
87 | 108 | | |
88 | 109 | | |
| |||
92 | 113 | | |
93 | 114 | | |
94 | 115 | | |
95 | | - | |
| 116 | + | |
96 | 117 | | |
97 | 118 | | |
98 | 119 | | |
99 | 120 | | |
100 | 121 | | |
101 | | - | |
| 122 | + | |
102 | 123 | | |
103 | 124 | | |
104 | 125 | | |
| |||
110 | 131 | | |
111 | 132 | | |
112 | 133 | | |
| 134 | + | |
113 | 135 | | |
114 | 136 | | |
115 | 137 | | |
116 | 138 | | |
117 | | - | |
| 139 | + | |
118 | 140 | | |
119 | 141 | | |
120 | 142 | | |
| |||
130 | 152 | | |
131 | 153 | | |
132 | 154 | | |
| 155 | + | |
| 156 | + | |
133 | 157 | | |
134 | 158 | | |
135 | 159 | | |
| |||
Lines changed: 250 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
14 | 17 | | |
15 | 18 | | |
16 | 19 | | |
| |||
691 | 694 | | |
692 | 695 | | |
693 | 696 | | |
694 | | - | |
| 697 | + | |
| 698 | + | |
695 | 699 | | |
696 | | - | |
697 | 700 | | |
698 | 701 | | |
699 | 702 | | |
| |||
0 commit comments