Skip to content

Commit 4a528d0

Browse files
authored
perf(perps): improved navigation time on low/middle range android devices (MetaMask#22701)
## **Description** This PR improves navigation performance in the Perps feature to provide a smoother, more responsive experience on lower-end Android devices. **Problem:** Navigation between Perps screens can feel sluggish on lower-end devices, particularly when: - Tapping market cards to view market details - Opening order screens via Long/Short buttons - Transitions lack visual feedback and feel unresponsive **Solution:** Implemented two targeted optimizations: 1. **Navigation Animations** - Enable smooth screen transitions with proper animation timing to provide visual feedback during navigation 2. **Memoize Market Lookups** - Pre-compute market data lookups in PerpsCard (200-300 items) to eliminate blocking array searches during user interactions **Impact:** - Smoother navigation transitions with visual feedback - Eliminated blocking array searches during user taps (200-300 item lookups) - Improved perceived performance on lower-end devices ## **Changelog** CHANGELOG entry: Improved Perps navigation performance for smoother screen transitions on lower-end devices ## **Related issues** Fixes: [Internal investigation - no public issue] ## **Manual testing steps** ```gherkin Feature: Perps Navigation Performance Scenario: user navigates from home to market details Given user is on Perps home screen with market cards visible When user taps on any market card (e.g., BTC-USD) Then screen transition should feel smooth and immediate And market details view should appear without noticeable delay Scenario: user opens order screen from market details Given user is viewing market details for any asset When user taps the "Long" or "Short" button Then order screen should appear with smooth animation And screen should load without noticeable delay Scenario: user navigates between multiple markets quickly Given user is on Perps home screen When user taps multiple different market cards in quick succession Then each navigation should feel responsive And there should be no UI freezing between taps Scenario: user tests on lower-end Android device Given user has a mid-to-low-end Android device (e.g., 4GB RAM) When user performs all navigation flows in Perps Then navigation should feel snappy with smooth animations And tapping market cards should respond immediately without lag ``` ## **Screenshots/Recordings** ### **Before** <!-- Add before recordings when testing on device --> ### **After** <!-- Add after recordings when testing on device --> ## **Technical Details** ### Navigation Animations **File:** `app/components/UI/Perps/routes/index.tsx` - Enable navigation animations for smooth screen transitions - Provides visual feedback during navigation ### Memoized Market Lookups **File:** `app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx` - Added `useMemo` to pre-compute market lookups: ```typescript const market = useMemo( () => markets.find((m) => m.symbol === symbol), [markets, symbol], ); ``` - Eliminates blocking array search during user taps (markets array typically contains 200-300 items) ## **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 (performance improvements - visual testing required) - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. ## **Testing Notes for Reviewers** **Focus Areas:** 1. Test on lower-end Android device (4GB RAM or less) if possible 2. Pay attention to navigation "feel" - should be immediate and smooth with animated transitions 3. Test rapid tapping of market cards - should remain responsive 4. No regression in functionality - all data should still load correctly **What to look for:** - ✅ Smooth animated transitions between screens - ✅ Instant response when tapping market cards or Long/Short buttons - ✅ No lag when tapping cards in quick succession - ❌ No frozen transitions or unresponsive UI - ❌ No missing data or broken functionality <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Defers non-critical PerpsOrderView data subscriptions until after first render, memoizes PerpsCard market lookup, enables default screen animations, and updates tests to await deferred data. > > - **PerpsOrderView (`PerpsOrderView.tsx`)**: > - *Deferred data loading*: Introduces `isDataReady` (via `requestAnimationFrame`) to delay `usePerpsMarketData`, `usePerpsLivePrices`, and `usePerpsTopOfBook` subscriptions until after initial render; updates `usePerpsMeasurement` conditions accordingly. > - Minor refactors: computed styles memoization unchanged behavior; validation/messages and handlers unchanged but aligned with deferred data. > - **PerpsCard (`PerpsCard.tsx`)**: > - *Performance*: Adds `useMemo` for market lookup and simplifies `handlePress` deps to use memoized `market`. > - **Navigation (`routes/index.tsx`)**: > - Removes `animationEnabled: false` to allow default transitions. > - **Tests (`PerpsOrderView.test.tsx`)**: > - Mocks `usePerpsLivePrices`; switches to async assertions (`findByText`) for values shown after deferred readiness. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit faffdca. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4b55670 commit 4a528d0

4 files changed

Lines changed: 53 additions & 35 deletions

File tree

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ jest.mock('../../hooks', () => ({
131131
usePerpsTrading: jest.fn(),
132132
usePerpsNetwork: jest.fn(),
133133
usePerpsPrices: jest.fn(),
134+
usePerpsLivePrices: jest.fn(() => ({
135+
ETH: { price: '3000', percentChange24h: '2.5' },
136+
BTC: { price: '3000', percentChange24h: '2.5' },
137+
})),
134138
usePerpsPaymentTokens: jest.fn(),
135139
usePerpsConnection: jest.fn(() => ({
136140
isConnected: true,
@@ -1244,7 +1248,9 @@ describe('PerpsOrderView', () => {
12441248
expect(placeOrderButton).toBeDefined();
12451249

12461250
// Verify validation errors are shown (indicating disabled state)
1247-
expect(screen.getByText('Insufficient balance')).toBeDefined();
1251+
// Wait for error to appear after isDataReady becomes true
1252+
const errorText = await screen.findByText('Insufficient balance');
1253+
expect(errorText).toBeDefined();
12481254
});
12491255

12501256
it('disables button when order is placing', async () => {
@@ -2839,7 +2845,7 @@ describe('PerpsOrderView', () => {
28392845
expect(orderTypeText).toBeOnTheScreen();
28402846
});
28412847

2842-
it('should display correct asset and price in header', () => {
2848+
it('should display correct asset and price in header', async () => {
28432849
// Arrange - Mock specific asset data
28442850
(usePerpsOrderContext as jest.Mock).mockReturnValue({
28452851
...defaultMockHooks.usePerpsOrderContext,
@@ -2849,7 +2855,7 @@ describe('PerpsOrderView', () => {
28492855
},
28502856
});
28512857

2852-
const { getByTestId, getByText } = render(
2858+
const { getByTestId, findByText } = render(
28532859
<SafeAreaProvider initialMetrics={initialMetrics}>
28542860
<TestWrapper>
28552861
<PerpsOrderView />
@@ -2860,7 +2866,9 @@ describe('PerpsOrderView', () => {
28602866
// Assert - Should display asset in header title using testID to avoid duplicate text matches
28612867
const headerTitle = getByTestId('perps-order-header-asset-title');
28622868
expect(headerTitle).toHaveTextContent('Long BTC');
2863-
expect(getByText('$3,000')).toBeOnTheScreen(); // Price from mock data
2869+
// Wait for price to appear after isDataReady becomes true
2870+
const priceText = await findByText('$3,000');
2871+
expect(priceText).toBeOnTheScreen();
28642872
});
28652873
});
28662874
});

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ const PerpsOrderViewContentBase: React.FC = () => {
160160
[styles.fixedBottomContainer, insets.bottom],
161161
);
162162

163+
// Deferred loading: Load non-critical data after UI renders
164+
const [isDataReady, setIsDataReady] = useState(false);
165+
useEffect(() => {
166+
// Defer data loading to next frame for faster initial render
167+
requestAnimationFrame(() => {
168+
setIsDataReady(true);
169+
});
170+
}, []);
171+
163172
const [selectedTooltip, setSelectedTooltip] =
164173
useState<PerpsTooltipContentKey | null>(null);
165174

@@ -216,9 +225,9 @@ const PerpsOrderViewContentBase: React.FC = () => {
216225
* updating leverage after positions load to prevent protocol violations.
217226
*/
218227

219-
// Market data hook with automatic error toast handling
228+
// Market data hook with automatic error toast handling (deferred)
220229
const { marketData, isLoading: isLoadingMarketData } = usePerpsMarketData({
221-
asset: orderForm.asset,
230+
asset: isDataReady ? orderForm.asset : '', // Defer until UI renders
222231
showErrorToast: true,
223232
});
224233

@@ -274,23 +283,23 @@ const PerpsOrderViewContentBase: React.FC = () => {
274283
},
275284
});
276285

277-
// Get real-time price data using new stream architecture
286+
// Get real-time price data using new stream architecture (deferred)
278287
// Uses single WebSocket subscription with component-level debouncing
279288
const prices = usePerpsLivePrices({
280-
symbols: [orderForm.asset],
289+
symbols: isDataReady ? [orderForm.asset] : [], // Defer subscription
281290
throttleMs: 1000,
282291
});
283292
const currentPrice = prices[orderForm.asset];
284293

285-
// Get top of book data for maker/taker fee determination
294+
// Get top of book data for maker/taker fee determination (deferred)
286295
const currentTopOfBook = usePerpsTopOfBook({
287-
symbol: orderForm.asset,
296+
symbol: isDataReady ? orderForm.asset : '', // Defer subscription
288297
});
289298

290-
// Track screen load with unified hook
299+
// Track screen load with unified hook (measure data loading, not initial render)
291300
usePerpsMeasurement({
292301
traceName: TraceName.PerpsOrderView,
293-
conditions: [!!currentPrice, !!account],
302+
conditions: [isDataReady, !!currentPrice, !!account],
294303
});
295304

296305
const assetData = useMemo(() => {

app/components/UI/Perps/components/PerpsCard/PerpsCard.tsx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { TouchableOpacity, View } from 'react-native';
33
import { useNavigation, type NavigationProp } from '@react-navigation/native';
44
import Text, {
@@ -78,32 +78,34 @@ const PerpsCard: React.FC<PerpsCardProps> = ({
7878
labelText = strings('perps.order.limit');
7979
}
8080

81+
// Memoize market lookup to avoid array search on every press
82+
const market = useMemo(
83+
() => markets.find((m) => m.symbol === symbol),
84+
[markets, symbol],
85+
);
86+
8187
const handlePress = useCallback(() => {
8288
if (onPress) {
8389
onPress();
84-
} else if (markets.length > 0 && symbol) {
85-
// Find the market data for this symbol
86-
const market = markets.find((m) => m.symbol === symbol);
87-
if (market) {
88-
let initialTab: 'position' | 'orders' | undefined;
89-
if (order) {
90-
initialTab = 'orders';
91-
} else if (position) {
92-
initialTab = 'position';
93-
}
94-
// Navigate to market details with the full market data
95-
// When navigating from a tab, we need to navigate through the root stack
96-
navigation.navigate(Routes.PERPS.ROOT, {
97-
screen: Routes.PERPS.MARKET_DETAILS,
98-
params: {
99-
market,
100-
initialTab,
101-
source,
102-
},
103-
});
90+
} else if (market) {
91+
let initialTab: 'position' | 'orders' | undefined;
92+
if (order) {
93+
initialTab = 'orders';
94+
} else if (position) {
95+
initialTab = 'position';
10496
}
97+
// Navigate to market details with the full market data
98+
// When navigating from a tab, we need to navigate through the root stack
99+
navigation.navigate(Routes.PERPS.ROOT, {
100+
screen: Routes.PERPS.MARKET_DETAILS,
101+
params: {
102+
market,
103+
initialTab,
104+
source,
105+
},
106+
});
105107
}
106-
}, [onPress, markets, symbol, navigation, order, position, source]);
108+
}, [onPress, market, navigation, order, position, source]);
107109

108110
if (!position && !order) {
109111
return null;

app/components/UI/Perps/routes/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ const PerpsScreenStack = () => (
126126
options={{
127127
title: strings('perps.markets.title'),
128128
headerShown: false,
129-
animationEnabled: false,
130129
}}
131130
/>
132131

0 commit comments

Comments
 (0)