Chapter 08

性能优化

理解 JS/UI/Shadow 三线程模型,用 React Native DevTools 找到真正的性能瓶颈,系统性优化 FlatList、渲染、Hermes 引擎与启动速度。

RN 性能问题的根源

React Native 的性能问题大多源于一个核心矛盾:JavaScript 是单线程的,而移动应用的渲染需要每秒 60 帧(高刷屏 120 帧)的更新频率,每帧只有约 16ms(或 8ms)。当 JS 线程在某一帧内耗时超过这个阈值,就会掉帧,用户感知为卡顿。

在 RN 0.77.x 的新架构(New Architecture)默认开启后,Fabric 渲染器和 JSI 已经大幅改善了 JS 线程与 UI 线程的通信效率,但 JS 线程本身的单线程性质不变,大量计算仍会导致帧率下降。

常见的 JS 线程性能杀手包括:在渲染函数里进行复杂计算、频繁创建新对象(内联样式、内联函数)、不必要的组件重渲染、大量图片解码、同步读取 AsyncStorage 等。

核心名词解释

JS Thread(JavaScript 线程)
运行 React 渲染逻辑、业务代码、状态管理的主线程。CPU 密集任务会阻塞此线程,导致动画卡顿和 UI 无响应。所有 React 组件的 render 函数都在此线程执行。
UI Thread(原生 UI 线程)
操作系统的主线程,负责绘制屏幕像素、处理触摸事件。动画通过 useNativeDriver 或 Reanimated 在此线程运行时,即使 JS 线程繁忙也不掉帧。这是保持动画流畅的关键。
Shadow Thread(布局线程)
运行 Yoga 布局引擎的后台线程,负责将 Flexbox 样式计算为具体的坐标和尺寸,然后同步给 UI 线程渲染。新架构中 Fabric 将布局计算与渲染更紧密地结合。
FPS(每秒帧数)
动画流畅度指标。60fps = 每帧 16.7ms,120fps = 每帧 8.3ms。低于 60fps 时用户开始感知卡顿,低于 30fps 时明显不流畅。React Native DevTools 的 Performance 面板可实时显示 JS 和 UI 帧率。
Hermes
Meta 专为 React Native 开发的 JavaScript 引擎,采用 AOT(Ahead-of-Time)字节码编译,相比 JavaScriptCore 显著减少首屏启动时间(TTI)和内存占用,是 RN 0.64+ 的默认引擎。在 0.77.x 中 Hermes 与新架构深度集成。
React.memo
高阶组件,对函数组件进行浅层 props 比较,如果 props 没有变化则跳过重渲染。用于 FlatList 的 renderItem 组件可避免列表滚动时的大量无效渲染。注意:浅比较对对象和数组引用进行比较,不是内容比较。
useMemo / useCallback
useMemo 缓存计算结果(避免重复昂贵计算),useCallback 缓存函数引用(避免子组件因函数引用变化触发重渲染)。只在有明确性能问题时使用,不要过度使用——每个 Hook 调用本身也有额外开销。
VirtualizedList
FlatList 和 SectionList 的底层实现,实现了列表虚拟化(只渲染可见区域附近的 item),是 RN 大列表性能的核心。windowSize 参数控制渲染窗口大小,默认 21 意味着前后各 10 屏。
TTI(Time to Interactive)
首次交互时间,从应用启动到用户可以操作的时间间隔。Hermes 的 AOT 编译主要优化这一指标,避免运行时 JS 解析开销。减少 JS Bundle 大小和首屏渲染组件数量同样可降低 TTI。
InteractionManager
React Native 提供的工具,允许在动画/过渡完成后执行耗时操作。用 InteractionManager.runAfterInteractions 将非紧急任务(如大量数据初始化)推迟到页面过渡动画结束后执行。

三线程模型详解

React Native 三线程模型(新架构) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌─────────────────────────────────────────────────┐ │ JS Thread │ │ • React 渲染(reconciliation) │ │ • 业务逻辑、网络请求处理 │ │ • 事件处理器(onPress、onChange 等) │ │ • 状态管理(Zustand/React Query) │ │ │ │ ⚠ 这里慢 = 动画卡顿 + UI 无响应 │ └──────────────┬──────────────────────────────────┘ │ JSI(C++ 直接引用,无 JSON 序列化) ▼ ┌─────────────────────────────────────────────────┐ │ Fabric Renderer(C++ 渲染层) │ │ • 并发渲染(React 18 Concurrent Mode) │ │ • 同步和异步更新模式 │ │ • 布局计算(Yoga 引擎内嵌) │ └──────────────┬──────────────────────────────────┘ │(提交原生更新) ▼ ┌─────────────────────────────────────────────────┐ │ UI Thread │ │ • 绘制原生组件(UIView / android.view.View) │ │ • 处理触摸/手势事件 │ │ • Reanimated worklet 动画 │ │ • useNativeDriver 动画 │ │ │ │ ✓ 这里始终流畅(操作系统保证) │ └─────────────────────────────────────────────────┘

性能分析工具:React Native DevTools

RN 0.76+ 引入了全新的 React Native DevTools,取代了旧版 Flipper 的主要调试功能。在开发模式下,摇晃设备或按 Cmd+D (iOS) / Cmd+M (Android) 打开开发菜单,选择 "Open DevTools"。

Profiler(性能分析器)
录制应用交互期间的 React 渲染时间线,显示每个组件的渲染耗时、重渲染原因(props/state/context 变化)、渲染次数。是定位不必要重渲染的最有效工具。
Components(组件树)
实时查看组件树结构、props 和 state。可以直接修改 state 观察 UI 变化,调试组件渲染问题。
Network(网络请求)
查看所有网络请求的 URL、状态码、响应时间、请求/响应体。调试 API 问题无需使用外部代理工具。
# 启动 React Native DevTools(RN 0.76+)
npx react-native start

# 在独立窗口打开 DevTools
# 开发服务器运行时,浏览器访问:
# http://localhost:8081/debugger-ui

# 或使用 Expo DevTools(Expo 项目)
npx expo start
# 按 j 打开 JavaScript 调试器
# 按 shift+i 打开 React Native DevTools

FlatList 深度优化

FlatList 的性能优化是 React Native 中最常见的性能课题。以下示例包含所有重要优化手段及其原理解释:

import { FlatList, View, Text, StyleSheet } from 'react-native';
import { memo, useCallback, useMemo } from 'react';

interface Item { id: string; text: string; height: number; }

// 优化 1:renderItem 组件用 memo 包裹
// 原理:FlatList 父组件 state 变化(如刷新状态)时,
// 不加 memo 的 ListItem 会全部重渲,加了 memo 只有 props
// 真正变化的 item 才重渲
const ListItem = memo(({ item, onPress }: { item: Item; onPress: (id: string) => void }) => (
  <View style={[styles.item, { height: item.height }]}>
    <Text>{item.text}</Text>
  </View>
));

export function OptimizedList({ data }: { data: Item[] }) {
  // 优化 2:useCallback 稳定 renderItem 函数引用
  // 原理:每次父组件渲染都会创建新函数,新引用 !== 旧引用,
  // 导致即使数据没变,ListItem 的 memo 也失效(props 变了)
  const handlePress = useCallback((id: string) => {
    console.log('pressed', id);
  }, []);

  const renderItem = useCallback(
    ({ item }: { item: Item }) => <ListItem item={item} onPress={handlePress} />,
    [handlePress]
  );

  // 优化 3:getItemLayout 告知 FlatList 每个 item 的精确尺寸
  // 原理:FlatList 默认需要测量每个 item 的高度才能计算
  // 滚动位置,getItemLayout 跳过这个测量过程,
  // 对 100+ 条数据的列表性能提升显著
  // 注意:仅适用于固定高度的 item!高度不固定时不能使用
  const ITEM_HEIGHT = 80;
  const getItemLayout = useCallback(
    (_: any, index: number) => ({
      length: ITEM_HEIGHT,          // item 高度(px)
      offset: ITEM_HEIGHT * index,  // 距列表顶部的偏移量
      index,                          // item 索引
    }),
    []
  );

  // 优化 4:稳定的 keyExtractor
  // 必须是唯一且稳定的值(不能用 index!)
  // 原理:React Diff 算法用 key 判断元素是否被复用还是重建
  const keyExtractor = useCallback((item: Item) => item.id, []);

  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      initialNumToRender={10}           // 首屏渲染数量(不要设太大,影响启动时间)
      windowSize={5}                   // 渲染窗口(默认21,缩小节省内存)
      maxToRenderPerBatch={5}          // 每批次渲染数量(滚动时分批渲染)
      updateCellsBatchingPeriod={30}   // 批次更新间隔(ms)
      removeClippedSubviews={true}    // Android:从渲染树中移除屏幕外视图
    />
  );
}

const styles = StyleSheet.create({
  item: {
    paddingHorizontal: 16,
    justifyContent: 'center',
    borderBottomWidth: 1,
    borderBottomColor: '#1e3a5f',
  },
});
removeClippedSubviews 的风险 removeClippedSubviews={true} 在 Android 上效果好,但在 iOS 上有已知 Bug:当与 FlatList 内部包含 Modal 或 overlay 组件时,可能导致内容消失。iOS 项目建议先不设置此属性,确认没有问题后再启用。

Hermes 引擎与启动优化原理

Hermes 是 Meta 专为 React Native 设计的 JavaScript 引擎。理解其工作原理才能有效利用其性能优势。

传统引擎 vs Hermes 启动流程对比 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 传统 V8 / JavaScriptCore 启动流程: ① 加载 JS Bundle(读取文件) → ~100ms ② 解析 JS 文本(词法分析/语法分析) → ~300ms(低端设备) ③ 编译为字节码(JIT 热点函数) → ~200ms ④ 执行代码(初始化模块) 总 TTI:600ms+ Hermes AOT 编译启动流程: 构建时:JS 源码 → Hermes 编译器 → 字节码文件(.hbc) 运行时: ① 加载字节码(读取已编译的 .hbc) → ~50ms ② 直接执行字节码(无需解析和编译) 总 TTI:200-300ms(减少约 40-50%) Bundle 大小变化: 源码 JS:1MB → 字节码:约 1.5MB(字节码更大但更快)

启动性能优化实践

// 优化 1:使用 InteractionManager 延迟非关键初始化
// 原理:App 启动时,首屏渲染和导航动画占用 JS 线程
// 将非关键任务延迟到所有交互完成后执行
import { InteractionManager } from 'react-native';
import { useEffect } from 'react';

function HomeScreen() {
  useEffect(() => {
    // 等待所有动画和交互完成后再初始化推荐引擎、预加载数据等
    const task = InteractionManager.runAfterInteractions(() => {
      initRecommendationEngine();  // 耗时初始化推迟执行
      prefetchNextPageData();      // 预加载非首屏数据
    });
    return () => task.cancel();   // 组件卸载时取消任务
  }, []);

  return <View>{/* 首屏内容 */}</View>;
}

// 优化 2:条件性加载重型库(动态 import)
// 原理:减小首屏 JS Bundle 执行量,将不常用模块推迟加载
const loadQRScanner = async () => {
  // 只在用户需要时才加载二维码扫描库
  const { BarCodeScanner } = await import('expo-barcode-scanner');
  return BarCodeScanner;
};

// 优化 3:避免在模块顶层执行昂贵操作
// 错误示例:模块加载时就执行昂贵计算
const data = processLargeDataset(rawData); // ← 坏!启动时阻塞

// 正确示例:延迟到需要时计算
let cachedData: any | null = null;
const getData = () => {
  if (!cachedData) {
    cachedData = processLargeDataset(rawData); // 首次调用时才计算
  }
  return cachedData;
};

渲染优化:识别和消除不必要重渲染

不必要的重渲染是 React Native 应用最常见的性能问题。以下展示常见的重渲染场景及其解决方案:

// 反模式:Context 导致大范围重渲染
// 问题:每次 AuthContext 中任何值变化(包括 token),
// 所有订阅此 Context 的组件都会重渲染
const AuthContext = createContext({
  user: null,
  token: null,
  isLoggedIn: false,
  login: (u, t) => {},
  logout: () => {},
});

// 正确:拆分 Context,按关注点分离
// 只关心 user 的组件不会因 token 变化而重渲
const UserContext = createContext<User | null>(null);  // 用户信息
const AuthActionsContext = createContext<AuthActions>({ login: () => {}, logout: () => {} });

// ─────────────────────────────────────────────

// 反模式:inline 对象/数组导致子组件无意义重渲染
// 问题:{ style: { color: 'red' } } 每次渲染都是新对象引用
// 即使 memo 也无效,因为 props.style !== prevProps.style
function BadParent() {
  return <MemoizedChild style={{ color: 'red' }} />; // ← 每次渲染创建新对象
}

// 正确:将 style 移到组件外部(StyleSheet 或常量)
const childStyle = { color: 'red' } as const; // 模块级常量,引用稳定
function GoodParent() {
  return <MemoizedChild style={childStyle} />; // 引用不变,memo 有效
}

图片性能优化

图片是移动应用最常见的性能问题来源。不当的图片处理会导致内存溢出、滚动卡顿和网络浪费。

// 推荐:使用 expo-image 替代 React Native 内置 Image
// expo-image 优势:内存+磁盘双缓存、blurhash 占位、渐进加载、WebP 支持
import { Image } from 'expo-image';

// blurhash:图片加载前的极低分辨率占位符
// 由服务端在上传时生成(约 30 个字符),前端存储并展示
// 视觉效果:图片轮廓的模糊预览,比灰色占位符更美观
const blurhash = '|rF?hV%2WCj[ayj[a|j[az_NaeWBj@ayfRayfQfQM{M|azj[azf6fQfQIpWXofj[ayj[j[fQayWCoeoeaya}j[ayfQa{oLj?j[WVj[ayayj[fQoff7azayj[ayj[j[ayofayayayj[';

export function PostImage({ uri }: { uri: string }) {
  return (
    <Image
      source={{ uri }}
      style={{ width: '100%', height: 240 }}
      placeholder={{ blurhash }}        // 加载前显示模糊占位
      contentFit="cover"               // 类似 CSS object-fit: cover
      transition={300}                 // 加载完成后 300ms 淡入
      cachePolicy="memory-disk"       // 内存缓存(快)+ 磁盘缓存(持久)
      recyclingKey={uri}               // 在 FlatList 中复用,避免闪烁
    />
  );
}

// 大图预加载(在详情页打开前预加载图片)
import { Image } from 'expo-image';

async function prefetchImages(uris: string[]) {
  await Image.prefetch(uris); // 预加载多张图片到磁盘缓存
}
图片尺寸陷阱 不要给 FlatList 中的缩略图请求原图!如果用户头像原图是 1024x1024 px,但展示尺寸只有 40x40 dp,每张图实际解码内存是显示所需的 650 倍((1024/40)² ≈ 650)。应该在服务端提供多尺寸版本,或使用 CDN 图片处理(如 Cloudflare 的 ?width=80&format=webp 参数),按展示尺寸请求对应大小的图片。

使用 useDeferredValue 处理搜索框

// 场景:搜索框输入时实时过滤大量数据
// 问题:每次键入都重渲 1000 条列表,导致输入卡顿
import { useState, useDeferredValue, useMemo } from 'react';

function SearchableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');

  // useDeferredValue 原理:React 优先渲染 query 状态(保证输入响应),
  // 将耗时的列表过滤渲染"延迟"到空闲时间执行
  // 用户键入时会看到旧的搜索结果,直到 React 有时间处理新查询
  const deferredQuery = useDeferredValue(query);

  // 昂贵的过滤计算:只在 deferredQuery 变化时执行
  const filteredItems = useMemo(
    () => items.filter(item =>
      item.title.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [items, deferredQuery]
  );

  // 根据 query !== deferredQuery 判断是否在"追赶"状态
  const isPending = query !== deferredQuery;

  return (
    <View>
      <TextInput
        value={query}
        onChangeText={setQuery}     // 立即响应输入
        placeholder="搜索..."
      />
      <View style={{ opacity: isPending ? 0.7 : 1 }}>  {/* 过渡期降低不透明度 */}
        <FlatList
          data={filteredItems}
          renderItem={({ item }) => <ItemRow item={item} />}
          keyExtractor={item => item.id}
        />
      </View>
    </View>
  );
}
优化的优先级:先测量,再优化 不要过早优化。正确的流程是:① 发现性能问题(用户反馈或实际掉帧)→ ② 用 React Native DevTools Profiler 定位具体瓶颈 → ③ 针对性优化。盲目添加 memo、useCallback 不仅没用,反而会因为额外的比较开销降低性能。特别是简单组件(只有 1-2 个 props),memo 的浅比较开销可能大于重渲染本身。

内存泄漏排查

// 常见内存泄漏场景 1:组件卸载后仍执行 setState
// 症状:Warning: Can't perform a React state update on an unmounted component
function BadComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(result => {
      setData(result); // ← 组件卸载后仍调用,内存泄漏
    });
  }, []);
}

// 正确:用 AbortController 取消请求
function GoodComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch('/api/data', { signal: controller.signal })
      .then(r => r.json())
      .then(result => setData(result))
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => controller.abort(); // 组件卸载时取消请求
  }, []);
}

// 常见内存泄漏场景 2:事件监听未清除
function AppStateMonitor() {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', handleChange);
    return () => subscription.remove(); // 必须在 cleanup 中移除监听!
  }, []);
}
本章小结 React Native 性能优化的核心原则:① 将动画逻辑移到 UI 线程(Reanimated worklet);② 用 memo + useCallback 减少不必要重渲染,但不要过度使用;③ FlatList 配置 getItemLayout(固定高度)+ windowSize 缩小渲染窗口;④ 使用 expo-image 的 blurhash 占位和多级缓存;⑤ 用 InteractionManager 推迟非关键初始化;⑥ 所有优化都要先用 DevTools Profiler 验证效果。Hermes 引擎的 AOT 编译在构建时已默认生效,无需额外配置。