ReactNativePerformanceSkill rn-performance

React Native性能优化技巧,包括列表优化、组件记忆化、避免不必要的重新渲染等,旨在提升Expo/React Native应用的性能表现。

移动开发 0 次安装 0 次浏览 更新于 3/3/2026

React Native 性能优化

问题陈述

React Native 性能问题通常源于不必要的重新渲染、未优化的列表和在 JS 线程上进行的昂贵计算。此代码库具有性能关键区域(射击掌握、玩家列表)以及已建立的优化模式。


模式:FlatList 优化

keyExtractor - 稳定的键

// ✅ 正确:稳定的函数引用
const keyExtractor = useCallback((item: Session) => item.id, []);

<FlatList
  data={sessions}
  keyExtractor={keyExtractor}
  renderItem={renderItem}
/>

// ❌ 错误:每次渲染都创建新函数
<FlatList
  data={sessions}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}
/>

// ❌ 错误:使用索引(在重新排序/删除时会导致问题)
keyExtractor={(item, index) => `${index}`}

getItemLayout - 固定高度项

const ITEM_HEIGHT = 80;
const SEPARATOR_HEIGHT = 1;

const getItemLayout = useCallback(
  (data: Session[] | null | undefined, index: number) => ({
    length: ITEM_HEIGHT,
    offset: (ITEM_HEIGHT + SEPARATOR_HEIGHT) * index,
    index,
  }),
  []
);

<FlatList
  data={sessions}
  getItemLayout={getItemLayout}
  // ... 其他属性
/>

为什么重要: 没有 getItemLayout,FlatList 必须测量每个项目,导致滚动时的卡顿。

renderItem - 记忆化

// 提取为命名组件
const SessionItem = memo(function SessionItem({ 
  session, 
  onPress 
}: { 
  session: Session; 
  onPress: (id: string) => void;
}) {
  return (
    <Pressable onPress={() => onPress(session.id)}>
      <Text>{session.title}</Text>
    </Pressable>
  );
});

// 稳定的回调
const handlePress = useCallback((id: string) => {
  navigation.push(`/session/${id}`);
}, [navigation]);

// 稳定的 renderItem
const renderItem = useCallback(
  ({ item }: { item: Session }) => (
    <SessionItem session={item} onPress={handlePress} />
  ),
  [handlePress]
);

<FlatList
  data={sessions}
  renderItem={renderItem}
  // ...
/>

其他优化

<FlatList
  data={sessions}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  getItemLayout={getItemLayout}
  
  // 性能属性
  removeClippedSubviews={true}           // 卸载屏幕外项目
  maxToRenderPerBatch={10}               // 每批渲染的项目数
  windowSize={5}                         // 渲染窗口(屏幕)
  initialNumToRender={10}                // 初始渲染计数
  updateCellsBatchingPeriod={50}         // 批量更新延迟(ms)
  
  // 防止额外渲染
  extraData={selectedId}                 // 仅当此变化时重新渲染
/>

模式:FlashList 用于大型列表

何时使用: 1000+ 项目,复杂的项目组件,或 FlatList 仍然卡顿。

import { FlashList } from '@shopify/flash-list';

<FlashList
  data={players}
  renderItem={renderItem}
  estimatedItemSize={80}  // 必需 - 估计项目高度
  keyExtractor={keyExtractor}
/>

注意: 此代码库当前不使用 FlashList。考虑用于教练玩家列表。


模式:记忆化

useMemo - 昂贵的计算

// ✅ 正确:记忆化昂贵的计算
const sortedAndFilteredItems = useMemo(() => {
  return items
    .filter(item => item.active)
    .sort((a, b) => b.score - a.score)
    .slice(0, 100);
}, [items]);

// ❌ 错误:每次渲染都重新计算
const sortedAndFilteredItems = items
  .filter(item => item.active)
  .sort((a, b) => b.score - a.score);

// ❌ 错误:记忆化简单访问(开销 > 好处)
const userName = useMemo(() => user.name, [user.name]);

何时使用 useMemo:

  • 数组转换(filter, sort, map 链)
  • 传递给记忆化子组件的对象创建
  • O(n) 或更高复杂度的计算

useCallback - 稳定的函数引用

// ✅ 正确:子组件属性的稳定回调
const handlePress = useCallback((id: string) => {
  setSelectedId(id);
}, []);

// 传递给记忆化子组件
<MemoizedItem onPress={handlePress} />

// ❌ 错误:useCallback 与不稳定的依赖项
const handlePress = useCallback((id: string) => {
  doSomething(unstableObject); // unstableObject 每次渲染都变化
}, [unstableObject]); // 适得其反

何时使用 useCallback:

  • 传递给记忆化子组件的回调
  • 依赖数组中的回调
  • 会导致子组件重新渲染的事件处理程序

模式:React.memo

// 包装接收稳定属性的组件
const PlayerCard = memo(function PlayerCard({ 
  player, 
  onSelect 
}: Props) {
  return (
    <Pressable onPress={() => onSelect(player.id)}>
      <Text>{player.name}</Text>
      <Text>{player.rating}</Text>
    </Pressable>
  );
});

// 复杂属性的自定义比较
const PlayerCard = memo(
  function PlayerCard({ player, onSelect }: Props) {
    // ...
  },
  (prevProps, nextProps) => {
    // 返回 true 如果属性相等(跳过重新渲染)
    return (
      prevProps.player.id === nextProps.player.id &&
      prevProps.player.rating === nextProps.player.rating
    );
  }
);

何时使用 React.memo:

  • 列表项组件
  • 接收稳定原始属性的组件
  • 频繁渲染但很少变化的组件

何时不使用:

  • 总是接收新属性的组件
  • 简单组件(开销 > 好处)
  • 根级屏幕

模式:Zustand 选择器优化

问题: 选择整个商店会导致任何状态变化时重新渲染。

// ❌ 错误:在任何商店变化时重新渲染
const store = useAssessmentStore();
// 或
const { userAnswers, isLoading, retakeAreas, ... } = useAssessmentStore();

// ✅ 正确:仅在选定值变化时重新渲染
const userAnswers = useAssessmentStore((s) => s.userAnswers);
const isLoading = useAssessmentStore((s) => s.isLoading);

// ✅ 正确:多个值与浅比较
import { useShallow } from 'zustand/react/shallow';

const { userAnswers, isLoading } = useAssessmentStore(
  useShallow((s) => ({ 
    userAnswers: s.userAnswers, 
    isLoading: s.isLoading 
  }))
);

另见: rn-zustand-patterns/SKILL.md 了解更多 Zustand 模式。


模式:图像优化

import { Image } from 'expo-image';

// expo-image 提供缓存和性能优化
<Image
  source={{ uri: player.avatarUrl }}
  style={{ width: 50, height: 50 }}
  contentFit="cover"
  placeholder={blurhash}           // 加载时显示
  transition={200}                  // 淡入持续时间
  cachePolicy="memory-disk"         // 缓存策略
/>

// 对于列表,添加优先级
<Image
  source={{ uri: player.avatarUrl }}
  priority={isVisible ? 'high' : 'low'}
/>

模式:避免重新渲染

对象/数组稳定性

// ❌ 错误:每次渲染都创建新对象
<ChildComponent style={{ padding: 10 }} />
<ChildComponent config={{ enabled: true }} />

// ✅ 正确:稳定的引用
const style = useMemo(() => ({ padding: 10 }), []);
const config = useMemo(() => ({ enabled: true }), []);

<ChildComponent style={style} />
<ChildComponent config={config} />

// ✅ 正确:或使用 StyleSheet
const styles = StyleSheet.create({
  container: { padding: 10 },
});

<ChildComponent style={styles.container} />

子组件稳定性

// ❌ 错误:内联函数每次渲染都创建新元素
<Parent>
  {() => <Child />}
</Parent>

// ✅ 正确:稳定的元素
const child = useMemo(() => <Child />, [deps]);
<Parent>{child}</Parent>

模式:检测重新渲染

React DevTools Profiler

  1. 打开 React DevTools
  2. 转到 Profiler 标签
  3. 点击记录,交互,停止
  4. 查看 “Flamegraph” 以了解渲染时间
  5. 查找不必要渲染的组件

why-did-you-render

// 开发中设置
import React from 'react';

if (__DEV__) {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

// 标记特定组件进行跟踪
PlayerCard.whyDidYouRender = true;

控制台日志

// 快速检查重新渲染
function PlayerCard({ player }: Props) {
  console.log('PlayerCard render:', player.id);
  // ...
}

模式:在主线程之外进行重型计算

问题: JS 线程阻塞导致 UI 卡顿。

// ❌ 错误:阻塞 JS 线程
const result = heavyComputation(data); // 需要 500ms

// ✅ 正确:使用 InteractionManager
import { InteractionManager } from 'react-native';

InteractionManager.runAfterInteractions(() => {
  const result = heavyComputation(data);
  setResult(result);
});

// ✅ 正确:用于视觉更新的 requestAnimationFrame
requestAnimationFrame(() => {
  // 在当前帧后更新
});

性能检查表

在发布列表密集型屏幕之前:

  • [ ] FlatList 有 keyExtractor(稳定的回调)
  • [ ] FlatList 有 getItemLayout(如果是固定高度)
  • [ ] 列表项使用 React.memo 记忆化
  • [ ] 传递给项目的回调使用 useCallback
  • [ ] Zustand 选择器是特定的(不是整个商店)
  • [ ] 图像使用 expo-image 进行缓存
  • [ ] 没有内联对象/函数属性传递给记忆化子组件
  • [ ] Profiler 显示没有不必要的重新渲染

常见问题

问题 解决方案
列表滚动卡顿 添加 getItemLayout,记忆化项目
组件重新渲染太频繁 检查选择器的特定性,记忆化属性
初始渲染缓慢 减少 initialNumToRender,推迟计算
内存增长 检查状态累积,图像缓存
交互时 UI 冻结 将计算移出主线程

与其他技能的关系

  • rn-zustand-patterns:选择器优化模式
  • rn-styling:StyleSheet.create 用于稳定的样式引用