Performance is critical for a smooth user experience in Rainbow Wallet. This guide covers performance best practices and common optimization techniques.
Measure First Always measure before optimizing. Use profiling tools to identify bottlenecks.
Optimize Critical Paths Focus on user-facing features and startup performance.
Lazy Load Load heavy dependencies and features only when needed.
Minimize Re-renders Reduce unnecessary React re-renders with proper memoization.
Import Optimization
Avoid Barrel Exports
Barrel files (index.ts) hurt performance by:
Defeating tree-shaking
Triggering cascading module loads
Increasing bundle size
Bad:
// ❌ Importing from barrel file loads entire module
import { Button } from '@/components' ;
Good:
// ✅ Direct imports enable tree-shaking
import { Button } from '@/components/Button' ;
Barrel files are strictly forbidden in Rainbow. ESLint will catch them.
Lazy Load Heavy Dependencies
Load expensive libraries only when needed:
// ❌ Bad: Import heavy library at module level
import ImageManipulator from 'expo-image-manipulator' ;
export async function processImage ( uri : string ) {
return ImageManipulator . manipulateAsync ( uri , [ ... ]);
}
// ✅ Good: Lazy load when needed
export async function processImage ( uri : string ) {
const ImageManipulator = await import ( 'expo-image-manipulator' );
return ImageManipulator . manipulateAsync ( uri , [ ... ]);
}
Use Type-Only Imports
Help bundlers optimize by marking type imports:
// ✅ Type-only imports are stripped at build time
import { type UserData } from '@/types/user' ;
import { type ComponentProps } from 'react' ;
// ❌ Regular imports may include runtime code
import { UserData } from '@/types/user' ;
Memoization
Use memoization to prevent unnecessary re-renders and recalculations:
React.memo for Components
import { memo } from 'react' ;
// Without memo: re-renders on every parent render
export function TokenRow ({ token } : { token : Token }) {
return < View >{token. symbol } </ View > ;
}
// With memo: only re-renders when token changes
export const TokenRow = memo ( function TokenRow ({
token
} : {
token : Token
}) {
return < View >{token. symbol } </ View > ;
});
useMemo for Expensive Computations
import { useMemo } from 'react' ;
function TokenList ({ tokens } : { tokens : Token [] }) {
// ❌ Bad: Sorts on every render
const sortedTokens = tokens . sort (( a , b ) => b . balance - a . balance );
// ✅ Good: Only sorts when tokens change
const sortedTokens = useMemo (
() => tokens . sort (( a , b ) => b . balance - a . balance ),
[ tokens ]
);
return < List data ={ sortedTokens } />;
}
useCallback for Functions
import { useCallback } from 'react' ;
function TokenList ({ tokens , onSelect } : Props ) {
// ❌ Bad: Creates new function on every render
const handlePress = ( token : Token ) => {
onSelect ( token );
analytics . track ( 'token_selected' );
};
// ✅ Good: Stable function reference
const handlePress = useCallback (( token : Token ) => {
onSelect ( token );
analytics . track ( 'token_selected' );
}, [ onSelect ]);
return < List data ={ tokens } onItemPress ={ handlePress } />;
}
use-memo-one
For shallow comparisons, use use-memo-one:
import { useMemo } from 'use-memo-one' ;
// More efficient than React.useMemo for simple deps
const value = useMemo (
() => computeValue ( a , b ),
[ a , b ]
);
List Rendering
Optimize large lists with proper virtualization:
Use FlashList
Rainbow uses @shopify/flash-list for performant lists:
import { FlashList } from '@shopify/flash-list' ;
function TokenList ({ tokens } : { tokens : Token [] }) {
return (
< FlashList
data = { tokens }
renderItem = {({ item }) => <TokenRow token = { item } /> }
estimatedItemSize = { 60 }
// Optimize with getItemType for heterogeneous lists
getItemType = {(item) => item. type }
/>
);
}
Optimize Item Components
import { memo } from 'react' ;
// ✅ Memoize list items
export const TokenRow = memo ( function TokenRow ({
token
} : {
token : Token
}) {
return (
< View >
< Text >{token. symbol } </ Text >
< Text >{token. balance } </ Text >
</ View >
);
});
// ✅ Use stable keys
function TokenList ({ tokens } : { tokens : Token [] }) {
return (
< FlashList
data = { tokens }
renderItem = {({ item }) => <TokenRow token = { item } /> }
// Use stable unique ID, not array index
keyExtractor = {(item) => item. uniqueId }
/>
);
}
Avoid Inline Objects and Functions
function Component () {
// ❌ Bad: Creates new object/function on every render
return (
< View style = {{ flex : 1 , padding : 16 }} >
< Button onPress = {() => console.log( 'pressed' )} />
</View>
);
}
// ✅ Good: Stable references
const styles = { flex: 1 , padding: 16 };
function Component () {
const handlePress = useCallback (() => {
console . log ( 'pressed' );
}, []);
return (
< View style = { styles } >
< Button onPress = { handlePress } />
</ View >
);
}
Use Selective Subscriptions
Subscribe to only the state you need:
import { useWalletStore } from '@/state/wallet' ;
// ❌ Bad: Re-renders on any wallet state change
function Component () {
const wallet = useWalletStore ();
return < Text >{wallet. address } </ Text > ;
}
// ✅ Good: Only re-renders when address changes
function Component () {
const address = useWalletStore ( state => state . address );
return < Text >{ address } </ Text > ;
}
Use createDerivedStore
For computed state, use createDerivedStore instead of selectors:
import { createDerivedStore } from '@/state/internal/createDerivedStore' ;
import { useWalletStore } from '@/state/wallet' ;
import { usePricesStore } from '@/state/prices' ;
// ✅ Efficient: Computed once and cached
export const useTotalBalance = createDerivedStore (
[ useWalletStore , usePricesStore ],
( wallet , prices ) => {
return wallet . tokens . reduce (
( sum , token ) => sum + ( prices [ token . address ] || 0 ) * token . balance ,
0
);
}
);
Use createQueryStore for Async Data
Replace React Query + Zustand with createQueryStore:
import { createQueryStore } from '@/state/internal/createQueryStore' ;
// ✅ Efficient: Combines fetching + state
export const useTokenPrice = createQueryStore ({
queryKey : ({ address }) => [ 'tokenPrice' , address ],
queryFn : async ({ address }) => {
const response = await fetch ( `/api/price/ ${ address } ` );
return response . json ();
},
// Automatic refetch on address change
staleTime: 60000 ,
});
Defer Non-Critical Work
Use InteractionManager to defer work until after initial render:
import { InteractionManager } from 'react-native' ;
import { useEffect } from 'react' ;
function App () {
useEffect (() => {
InteractionManager . runAfterInteractions (() => {
// Defer non-critical initialization
initializeAnalytics ();
preloadHeavyAssets ();
});
}, []);
return < MainApp />;
}
Lazy Load Screens
Load screens only when navigating to them:
import { lazy , Suspense } from 'react' ;
// ✅ Lazy load heavy screens
const SettingsScreen = lazy (() => import ( '@/screens/Settings' ));
const NFTScreen = lazy (() => import ( '@/screens/NFT' ));
function Navigation () {
return (
< Stack . Navigator >
< Stack . Screen name = "Home" component = { HomeScreen } />
< Stack . Screen name = "Settings" >
{() => (
< Suspense fallback = {<Loading />} >
< SettingsScreen />
</ Suspense >
)}
</ Stack . Screen >
</ Stack . Navigator >
);
}
Use Hermes Engine
Rainbow uses Hermes for better startup performance:
Faster app startup
Reduced memory usage
Better runtime performance
Hermes is enabled by default in React Native 0.70+.
Batch API Calls
Combine multiple requests when possible:
// ❌ Bad: Multiple sequential requests
async function loadData () {
const user = await fetchUser ();
const tokens = await fetchTokens ();
const prices = await fetchPrices ();
return { user , tokens , prices };
}
// ✅ Good: Parallel requests
async function loadData () {
const [ user , tokens , prices ] = await Promise . all ([
fetchUser (),
fetchTokens (),
fetchPrices (),
]);
return { user , tokens , prices };
}
Cache Aggressively
Use appropriate cache strategies:
import { createQueryStore } from '@/state/internal/createQueryStore' ;
const useTokenMetadata = createQueryStore ({
queryKey : ({ address }) => [ 'metadata' , address ],
queryFn: fetchMetadata ,
// Token metadata rarely changes
staleTime: 24 * 60 * 60 * 1000 , // 24 hours
cacheTime: Infinity ,
});
import { useDebouncedValue } from 'use-debounce' ;
function SearchScreen () {
const [ search , setSearch ] = useState ( '' );
const [ debouncedSearch ] = useDebouncedValue ( search , 300 );
const { data } = useTokenSearch ({ query: debouncedSearch });
return (
<>
< SearchInput value = { search } onChangeText = { setSearch } />
< Results data = { data } />
</>
);
}
Use Faster Image
Rainbow uses @candlefinance/faster-image for better performance:
import { FasterImage } from '@candlefinance/faster-image' ;
< FasterImage
source = {{ uri : tokenImageUrl }}
style = {{ width : 40 , height : 40 }}
// Optimize with proper resize mode
resizeMode = "cover"
// Cache indefinitely for static images
cachePolicy = "immutable"
/>
Optimize Image Sizes
Request appropriately sized images:
import ImgixClient from 'imgix-core-js' ;
const imgix = new ImgixClient ({ domain: 'rainbow.imgix.net' });
// ✅ Request image at exact display size
const imageUrl = imgix . buildURL ( 'token.png' , {
w: 80 , // width in pixels
h: 80 , // height in pixels
fit: 'crop' ,
auto: 'format,compress' ,
});
Use Reanimated
Use react-native-reanimated for performant animations:
import Animated , {
useAnimatedStyle ,
withTiming
} from 'react-native-reanimated' ;
function AnimatedComponent ({ visible } : { visible : boolean }) {
const style = useAnimatedStyle (() => ({
opacity: withTiming ( visible ? 1 : 0 , { duration: 200 }),
}));
return < Animated . View style ={ style }>{ ... } </ Animated . View > ;
}
Avoid Layout Animations
Layout animations are expensive. Use transform instead:
// ❌ Bad: Layout animation (expensive)
const style = useAnimatedStyle (() => ({
width: withTiming ( expanded ? 200 : 100 ),
}));
// ✅ Good: Transform animation (60fps)
const style = useAnimatedStyle (() => ({
transform: [{
scaleX: withTiming ( expanded ? 2 : 1 ),
}],
}));
Custom Styling Engine
Rainbow uses a custom styling engine (styled-thing) instead of styled-components for better performance:
import { Box , Text } from '@/design-system' ;
// ✅ Uses optimized styling engine
< Box padding = "16px" backgroundColor = "surface" >
< Text size = "16px" weight = "bold" > Fast styling </ Text >
</ Box >
From src/framework/ui/styled-thing/README.md:
Custom styling engine created as a performance replacement for styled-components. styled-components transforms style objects into CSS strings, generates class name hashes, and parses them back into objects for React Native. This implementation operates directly on style objects, resulting in significant performance improvement, particularly on Android.
Rainbow includes performance tracking:
import { useStartTime , useEndTime } from '@/performance/tracking' ;
function Screen () {
useStartTime ( 'screen_load' );
useEffect (() => {
useEndTime ( 'screen_load' );
}, []);
return < View >{ ... } </ View > ;
}
Rainbow uses @shopify/react-native-performance:
import { Performance } from '@shopify/react-native-performance' ;
// Measure custom operations
Performance . mark ( 'operation_start' );
await heavyOperation ();
Performance . mark ( 'operation_end' );
Performance . measure ( 'operation' , 'operation_start' , 'operation_end' );
Enable profiling in React DevTools
Record a session
Identify components with long render times
Optimize with memoization
Flipper
Use Flipper for debugging:
Network inspector
Performance monitor
Memory profiler
Layout inspector
Hermes Profiler
# Enable profiling
yarn ios --configuration Release
# In app, shake device > Enable Sampling Profiler
# Record profile
# Download profile and analyze with Chrome DevTools
Verification Checklist
Before submitting performance-related changes:
Test performance on actual devices:
# Test on release build (more accurate)
yarn ios --configuration Release
yarn android --variant=release
# Monitor performance
# - Startup time
# - Time to interactive
# - List scroll performance
# - Animation frame rate
# - Memory usage
Solutions:
Use FlashList instead of FlatList
Memoize list items
Reduce item complexity
Use getItemType for heterogeneous lists
Issue: Slow Screen Navigation
Solutions:
Lazy load screens
Defer heavy operations
Reduce initial render complexity
Use loading states
Issue: High Memory Usage
Solutions:
Clear caches periodically
Unsubscribe from stores
Release image resources
Remove event listeners
Issue: Jank During Animations
Solutions:
Use Reanimated instead of Animated
Run animations on UI thread
Avoid layout animations
Use transform instead of layout changes
Additional Resources
Code Conventions Learn about coding standards
React Native Performance Official performance guide
Reanimated Docs Animation performance guide
FlashList High-performance list component