| name | performance |
| description | WHAT: Performance optimization with memoization, React.memo, and FlatList techniques. WHEN: profiler shows slow renders, preventing re-renders, optimizing list performance. KEYWORDS: performance, useMemo, useCallback, React.memo, FlatList, getItemLayout, optimization, profile, windowSize. |
Performance Optimization Patterns
Core Principles
Optimize strategically, not prematurely. Use React Profiler to identify bottlenecks, then apply targeted optimizations with useRef, useMemo, useCallback, React.memo, and FlatList optimization techniques.
Why: Strategic optimization prevents unnecessary complexity while addressing real performance issues. Measure first, optimize second.
When to Use This Skill
Use performance optimization when:
- Profiler shows slow renders (>16ms)
- Components re-render unnecessarily
- Expensive computations run on every render
- List rendering causes lag
- Tracking values across renders without triggering updates
- Preventing callback recreation
- Need to preserve referential equality
Don't optimize prematurely: Write clean code first, profile to find bottlenecks, then optimize strategically.
useRef: Tracking Without Re-renders
Track Values Across Renders
Use useRef to store values that persist across renders without triggering re-renders.
import { useRef, useEffect } from 'react';
const MyComponent = () => {
const hasProcessedInitialLoad = useRef(false);
const lastProcessedCount = useRef(0);
useEffect(() => {
const shouldProcess =
!isLoadingSharedUrls &&
hasSharedUrls &&
(!hasProcessedInitialLoad.current ||
sharedUrls.length > lastProcessedCount.current);
if (shouldProcess) {
hasProcessedInitialLoad.current = true;
lastProcessedCount.current = sharedUrls.length;
processSharedUrls(sharedUrls);
}
}, [isLoadingSharedUrls, hasSharedUrls, sharedUrls]);
};
Why: useRef provides mutable storage without triggering re-renders. Perfect for flags, counters, and tracking state.
Production Example: git-resources/shared-mobile-modules/src/operations/meal-selection-listener/useMealSelectionListener.ts:64
Store Previous Values
Track previous values for comparison logic.
const MyComponent = ({ selection }) => {
const lastSuccessfulSelectionRef = useRef<CartFragmentFragment | undefined>(
undefined
);
const previousSelectionRef = useRef<CartFragmentFragment | undefined>(
undefined
);
useEffect(() => {
if (selection) {
if (!lastSuccessfulSelectionRef.current) {
lastSuccessfulSelectionRef.current = selection;
}
if (previousSelectionRef.current) {
lastSuccessfulSelectionRef.current = previousSelectionRef.current;
}
previousSelectionRef.current = selection;
}
}, [selection]);
};
Why: Enables comparison logic, error recovery, and undo functionality without state updates.
Production Example: git-resources/shared-mobile-modules/src/operations/meal-selection-listener/useMealSelectionListener.ts:64
Store Component References
Store refs to child components or DOM elements.
import { useRef, useEffect } from 'react';
import { FlatList } from 'react-native';
const MyComponent = ({ currentSlideIndex, slides }) => {
const flatListRef = useRef<FlatList>(null);
useEffect(() => {
if (
flatListRef.current &&
currentSlideIndex >= 0 &&
currentSlideIndex < slides.length
) {
flatListRef.current.scrollToOffset({
offset: currentSlideIndex * screenWidth,
animated: true,
});
}
}, [currentSlideIndex, slides.length]);
return (
<FlatList
ref={flatListRef}
data={slides}
renderItem={renderSlide}
// ...
/>
);
};
Why: Access component methods imperatively (e.g., scrollToOffset, focus, measure).
Production Example: git-resources/shared-mobile-modules/src/features/app-onboarding/components/Body.tsx:92
useMemo: Expensive Computations
Memoize Filtered/Sorted Data
Use useMemo to cache expensive filtering or sorting operations.
import { useMemo } from 'react';
const useMealSelectionInfo = () => {
const { config, totalMealKitItemsSize, totalAddonsItemsSize, errors } =
useMealSelection();
return useMemo(() => {
if (!config) {
return undefined;
}
const minMealsSize = config.variations.minMealsSize;
const maxMealsSize = config.variations.maxMealsSize;
const isMaxReached =
maxMealsSize > 0 && maxMealsSize === totalMealKitItemsSize;
const mealsNeededToReachMinimum = Math.max(
(minMealsSize ?? 0) - totalMealKitItemsSize,
0
);
const isBelowMinimum = Boolean(
errors?.some(
(error) => error.type === UpdateCartErrorType.BelowMinimum
) && mealsNeededToReachMinimum > 0
);
return {
isBelowMinimum,
isMaxReached,
totalMealKitItemsSize,
totalAddonsItemsSize,
minRequiredMealKitItemsSize: config.variations.minMealsSize,
maxSelectedMealKitItemSize: maxMealsSize,
mealsNeededToReachMinimum,
};
}, [config, totalMealKitItemsSize, totalAddonsItemsSize, errors]);
};
Why: Prevents recalculating complex derived state on every render. Only recomputes when dependencies change.
Production Example: git-resources/shared-mobile-modules/src/operations/use-selection-info/useMealSelectionInfo.ts:24
Memoize Context Values
Use useMemo for context provider values to prevent unnecessary re-renders of consumers.
import { useMemo } from 'react';
const MyProvider = ({ children }) => {
const [state, setState] = useState(initialState);
const contextValue = useMemo(
() => ({
state,
setState,
}),
[state]
);
return (
<MyContext.Provider value={contextValue}>
{children}
</MyContext.Provider>
);
};
Why: Context value changes trigger re-renders in all consumers. Memoization prevents unnecessary updates.
Memoize Transformed Data
Cache data transformations that depend on specific inputs.
const useMealSelectionListener = ({ deliveryId }) => {
const { selection } = useMealSelection();
const selectionToSave = useMemo(
() => transformSelectionForSave(selection),
[selection]
);
const [updateCart] = useUpdateCartMutation({
variables: {
selectionInput: selectionToSave ?? [],
},
});
};
Why: Expensive transformations only run when input data changes, not on every render.
Production Example: git-resources/shared-mobile-modules/src/operations/meal-selection-listener/useMealSelectionListener.ts:58
useCallback: Stable Callbacks
Memoize Event Handlers
Use useCallback for event handlers passed as props to prevent child re-renders.
import { useCallback, useState } from 'react';
const useRefresh = (options: {
refetchFunctions: Array<() => Promise<unknown>>;
isRefetching?: boolean;
}) => {
const { refetchFunctions, isRefetching } = options;
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
const isRefreshing = Boolean(isManualRefreshing || isRefetching);
const handleRefresh = useCallback(async () => {
try {
setIsManualRefreshing(true);
await Promise.all(refetchFunctions.map((fn) => fn()));
} catch (error) {
console.error('Error refreshing data:', error);
} finally {
setIsManualRefreshing(false);
}
}, [refetchFunctions]);
return {
isRefreshing,
handleRefresh,
};
};
Why: Stable callback reference prevents unnecessary re-renders when passed to child components.
Production Example: git-resources/shared-mobile-modules/src/operations/refresh/useRefresh.ts:20
Memoize Callbacks for Dependencies
Create stable callbacks used in useEffect or other hooks' dependencies.
const useMealSelectionListener = ({ deliveryId }) => {
const handleSelectionUpdate = useCallback(
(newSelection: CartFragmentFragment, isError?: boolean) => {
if (isError) {
overrideSelection(newSelection);
} else {
overrideStateFields(newSelection);
}
lastSuccessfulSelectionRef.current = newSelection;
},
[overrideSelection, overrideStateFields]
);
const handleErrorUpdate = useCallback(
(errors: Parameters<typeof setErrors>[0]) => {
setErrors(errors);
},
[setErrors]
);
const { onCompleted, onError } = useMemo(
() =>
createMutationHandlers({
onSelectionUpdate: handleSelectionUpdate,
onError: handleErrorUpdate,
}),
[handleSelectionUpdate, handleErrorUpdate, ]
);
};
Why: Stable callback references prevent dependency arrays from changing unnecessarily.
Production Example: git-resources/shared-mobile-modules/src/operations/meal-selection-listener/useMealSelectionListener.ts:104
React.memo: Component Memoization
Memoize List Items
Use React.memo for expensive list item components to prevent unnecessary re-renders.
import React from 'react';
export const ProductListItem = React.memo(
({
item,
variant,
itemContainerStyle,
position,
}: ProductListingComponentProps) => {
const styles = useZestStyles(stylesConfig);
if (isWidget(item)) {
return (
<View style={[styles.widgetItemWrapper, itemContainerStyle]}>
<WidgetContainer widget={item} />
</View>
);
}
const cardVariant = variant(item);
const enhancedCardVariant = hasActionHandler(cardVariant)
? enhanceCardVariantWithPosition(cardVariant, position)
: cardVariant;
return (
<AnalyticsWrapper
parameters={{
recipe_position: position,
ui_element: SOURCE.LIST,
}}
>
<View style={styles.productItemWrapper}>
<ProductCard data={enhancedCardVariant} />
</View>
</AnalyticsWrapper>
);
}
);
Why: List items re-render frequently as user scrolls. Memoization prevents renders when props haven't changed.
Production Example: git-resources/shared-mobile-modules/src/features/product-listing-feature/components/ProductListItem.tsx:61
Custom Comparison Function
Provide custom comparison for fine-grained control over re-renders.
import React from 'react';
const RecipeCard = React.memo(
({ recipe, onPress }) => {
return (
<Card onPress={() => onPress(recipe.id)}>
<Text>{recipe.name}</Text>
<Text>{recipe.description}</Text>
</Card>
);
},
(prevProps, nextProps) => {
return (
prevProps.recipe.id === nextProps.recipe.id &&
prevProps.recipe.updatedAt === nextProps.recipe.updatedAt
);
}
);
Why: Custom comparison ignores irrelevant prop changes (e.g., callback references) while detecting actual data changes.
FlatList Optimization
Use getItemLayout for Fixed Heights
Provide getItemLayout for lists with fixed-height items for instant scrolling.
import { FlatList } from 'react-native';
const ITEM_HEIGHT = 120;
const MyList = ({ items }) => {
return (
<FlatList
data={items}
renderItem={({ item }) => <ItemCard item={item} />}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
keyExtractor={(item) => item.id}
/>
);
};
Why: getItemLayout enables instant scroll-to-index without measuring, dramatically improving performance for long lists.
Production Example: git-resources/shared-mobile-modules/src/features/app-onboarding/components/Body.tsx:144
Adjust Window Size
Control how many items are rendered outside the viewport.
<FlatList
data={items}
renderItem={renderItem}
getItemLayout={getItemLayout}
windowSize={5}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
removeClippedSubviews={true}
/>
Why: Reduces memory usage and improves scrolling performance by limiting rendered items.
Extract Callbacks from renderItem
Use separate callback functions instead of inline arrow functions in renderItem.
<FlatList
data={items}
renderItem={({ item }) => <ItemCard item={item} onPress={handlePress} />}
/>
const renderItem = ({ item }: { item: Item }) => (
<ItemCard item={item} onPress={handlePress} />
);
<FlatList
data={items}
renderItem={renderItem}
/>
Why: Stable renderItem reference prevents FlatList from re-creating all list items on parent re-render.
Production Example: git-resources/shared-mobile-modules/src/features/app-onboarding/components/Body.tsx:121
Empty Dependency Arrays
Document Why Dependencies Are Empty
Always document eslint-disable comments for empty dependency arrays.
const mergedAttributes = useMemo(() => {
return {
...parentAttributes,
...props.attributes,
squad: props.squad,
};
}, []);
Why: Empty arrays are intentional optimization decisions that need explanation for future maintainers.
useEffect(() => {
if (autoStart) {
startTrace();
}
}, []);
Why: Documents that the effect should only run once, not on every dependency change.
Performance Measurement
Use React Profiler
Wrap components with React Profiler to measure render performance.
import { Profiler } from 'react';
const onRenderCallback = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
if (actualDuration > 16) {
console.warn(`Slow render in ${id}: ${actualDuration}ms (${phase})`);
}
};
const App = () => (
<Profiler id="RecipeList" onRender={onRenderCallback}>
<RecipeListScreen />
</Profiler>
);
Why: Profiler identifies slow components objectively. Only optimize components that actually perform poorly.
Use DEV Guards
Add development-only performance logging.
useEffect(() => {
if (__DEV__) {
const start = performance.now();
processData();
const end = performance.now();
console.log(`[PERF] processData took ${(end - start).toFixed(2)}ms`);
} else {
processData();
}
}, [processData]);
Why: Performance logging in development helps identify bottlenecks without impacting production.
Common Mistakes to Avoid
❌ Don't optimize prematurely:
const SimpleText = React.memo(({ text }) => <Text>{text}</Text>);
const doubled = useMemo(() => count * 2, [count]);
✅ Do optimize strategically:
const RecipeCard = React.memo(ExpensiveRecipeCard);
const filteredRecipes = useMemo(() => {
return recipes.filter(complexFilterLogic);
}, [recipes]);
❌ Don't forget dependencies:
const handleClick = useCallback(() => {
console.log(userId);
}, []);
const total = useMemo(() => {
return price * quantity;
}, []);
✅ Do include all dependencies:
const handleClick = useCallback(() => {
console.log(userId);
}, [userId]);
const total = useMemo(() => {
return price * quantity;
}, [price, quantity]);
❌ Don't use React.memo without reason:
const Button = React.memo(SimpleButton);
const Text = React.memo(SimpleText);
const Icon = React.memo(SimpleIcon);
✅ Do use React.memo for expensive components:
const ProductCard = React.memo(ExpensiveProductCard);
const RecipeCard = React.memo(
ExpensiveRecipeCard,
(prev, next) => prev.recipe.id === next.recipe.id
);
❌ Don't create new objects in dependencies:
const filteredData = useMemo(() => {
return filterData(data, { status: 'active' });
}, [data, { status: 'active' }]);
✅ Do use stable references:
const STATUS = 'active';
const filteredData = useMemo(() => {
return filterData(data, { status: STATUS });
}, [data, STATUS]);
const filterConfig = useMemo(() => ({ status: 'active' }), []);
const filteredData = useMemo(() => {
return filterData(data, filterConfig);
}, [data, filterConfig]);
❌ Don't miss getItemLayout for lists:
<FlatList
data={items}
renderItem={renderItem}
/>
✅ Do provide getItemLayout:
const ITEM_HEIGHT = 100;
<FlatList
data={items}
renderItem={renderItem}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
Quick Reference
useRef patterns:
const flagRef = useRef(false);
const previousValueRef = useRef(value);
const componentRef = useRef<FlatList>(null);
useMemo patterns:
const filtered = useMemo(() => filter(data), [data]);
const contextValue = useMemo(() => ({ state }), [state]);
const transformed = useMemo(() => transform(input), [input]);
useCallback patterns:
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
const handleRefresh = useCallback(async () => {
await refetch();
}, [refetch]);
React.memo patterns:
const ListItem = React.memo(ExpensiveItem);
const Card = React.memo(
CardComponent,
(prev, next) => prev.id === next.id
);
FlatList optimization:
<FlatList
data={items}
renderItem={renderItem}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
windowSize={5}
maxToRenderPerBatch={10}
/>
Empty dependency documentation:
}, []);
Strategy:
- Write clean code first
- Profile to find bottlenecks (React Profiler)
- Optimize strategically
- Measure improvement
- Document decisions
Key Libraries:
- React Native 0.75.4
- TypeScript 5.1.6
For production examples, see references/examples.md.