as

Settings
Sign out
Notifications
Alexa
亚马逊应用商店
AWS
文档
Support
Contact Us
My Cases
新手入门
设计和开发
应用发布
参考
支持

应用性能最佳实践

应用性能最佳实践

由于适用于Vega的React Native应用刚好是React Native应用,因此有关React Native最佳实践的公开文档也适用。考虑在构建适用于Vega的React Native应用时的这些关键最佳实践,以及Vega与iOS或Android的主要区别。

视频概述

观看此视频,大致了解如何在Vega上提高应用性能。(附带中文字幕)

(附带中文字幕)

避免重新渲染和重新定义

memo

React的memo可以防止不必要地重新渲染功能组件,从而改善性能。默认情况下,每当组件的父组件重新渲染时,React都会重新渲染组件,即使其属性没有改变。React的memo通过浅显地比较之前属性和新属性来优化性能,如果它们相同,则跳过重新渲染。该优化对于接收不需要经常更新的稳定属性的组件非常有利,例如列表项、按钮或具有大量渲染逻辑的用户界面元素。当与useCallbackuseMemo结合使用时,memo还可以最大限度地减少浪费的渲染,从而提高React应用的效率。

默认情况下,memo使用Object.is静态方法对属性进行浅显比较,以确定组件是否应该重新渲染。在某些情况下,您可能需要提供自己的arePropsEqual函数来自定义此比较。memo可用于向函数组件传递行内样式或匿名定义属性值。在这些情况下,属性的值相等,但在内存中不相等,因此Object.is() 比较会将它们标记为不相等。在将复杂的对象或函数作为属性进行处理时,自定义比较很有用,在这种情况下,仅进行浅显比较是不够的。自定义比较功能可以更好地控制组件的更新时间,从而提高需要深入比较的场景或特定条件下的性能。

React Native有两个有用的工具来调查React组件的重新渲染: react-devtools分析器中的“Record why each component rendered while profiling”(在分析时记录渲染每个组件的原因)标记,以及社区程序包why-did-you-render (WDYR)。

最好将用户定义的函数组件包装在memo API中。但是,在任何地方都使用memouseMemo可能不可取。请参阅React的文章,应该在任何地方都添加memo吗?(仅提供英文版),了解何时以及如何使用memouseMemo

React编译器和eslint-plugin-react-compiler

在React 19候选版本 (RC) 中,React引入了静态React代码编译器,它使用memouseMemouseCallback自动应用记忆。编译器假定您的应用遵循React规则,是语义化的JavaScript或TypeScript,并在访问可能为空和可选的值和属性之前先进行测试。尽管React表示“编译器仍处于实验阶段,还存在许多不足之处”,但我们建议引入react编译器的ESLint插件eslint-plugin-react-compiler。这个插件不需要升级到React 19。要引入该插件,请参阅安装eslint-plugin-react-hooks(仅提供英文版)。

严格模式

<StrictMode>打包React Native应用是有益的,因为它有助于在开发过程中识别应用中的潜在问题。<StrictMode>会进行额外检查,并就已弃用的生命周期方法、不安全的副作用以及其他可能导致性能问题或错误的潜在问题发出警告。它还会双重调用某些函数,例如组件构造函数和useEffect回调,因此不会产生意想不到的副作用。这个调试工具可以帮助开发者尽早发现问题,从而开发出更稳定和高效的应用。<StrictMode>不会影响生产构建,但在开发中使用它是符合最佳实践的,并让应用对未来的React和React Native更新做好准备。

Suspense和lazy

React的lazy组件和<Suspense>边界可以显著改善启动时间和应用的整体性能。通过启用代码拆分,React的lazy允许组件仅在需要时加载,从而减小了初始Bundle的大小并加快了应用的初始渲染速度。React的<Suspense>lazy协同作用,具体方式是让应用“暂停”渲染,直到加载必要的组件,从而在不阻塞用户界面的情况下创造流畅的用户体验。这种方法不仅可以优化应用的启动时间,还可以提高Bundle加载效率,每个视图仅加载所需的代码。

useCallback

在React中使用useCallback时,可通过记忆函数实例来提高性能,防止在每次渲染时进行不必要的重新创建。当将函数作为属性传递给子组件时,useCallback很有用。当这些组件依赖引用相等性检查时,有助于避免不必要的重新渲染。例如,当被包装在React.memo中时,在函数是特效依赖项 (useEffect) 的情况下,useCallback有助于防止意外地重新执行这种不利情况。请谨慎使用useCallback,因为过度使用可能会增加内存使用量和复杂性。如果使用得当,useCallback可以优化React应用中的渲染效率并减少性能开销。

useCallback的依赖项数组中添加项目,这样记忆的函数就可以访问它引用的任何响应性依赖项的最新值。省略依赖项可能会导致过时的闭包,在这种情况下,函数继续使用先前渲染中的过时值,这可能会导致意外行为或错误。

useEffect()

尽管不是React Native开发特有的情况,但使用React的useEffect的方式可能会影响性能。建议阅读React文章您可能不需要特效(仅提供英文版)。与往常一样,确保您的useEffect依赖项数组包含所有响应性值(在组件主体中声明的属性、状态、变量和函数等)。使用eslint-plugin-react-compiler有助于您检测useEffect依赖项数组中缺少的依赖项。

useMemo

React的useMemo通过记忆消耗较高的计算结果来提升性能,每次渲染时防止进行不必要的重新计算。如果没有useMemo,执行复杂操作(例如筛选大型列表、执行数学计算或处理数据)的函数将在每次渲染时执行。即使它们的依赖项没有改变,这些函数也会执行。通过将此类计算打包在useMemo中,React仅在其依赖项更新时才重新运算它们,从而降低CPU使用率并提高响应能力。在组件由于无关状态变化而经常重新渲染的情况下,使用useMemo很有用。应当有选择地使用useMemo,因为过度使用会增加内存开销,且没有明显好处。

向依赖项数组中添加项目,以使useMemo在更改依赖项时重新计算备忘值。如果省略依赖项,则您的值可能会过时或不正确。不必要的依赖项会导致不必要的重新计算,从而降低性能。

useState和useTransition

尽量减少React的useState使用量可以通过减少不必要的重新渲染来提高性能。每次状态更新都会触发重新渲染,因此过度的状态管理会导致性能瓶颈,尤其是在复杂的组件中。您可以通过减少使用useState来提高组件的效率。您可以通过对不影响渲染的可变值使用引用 (useRef),或者通过派生状态而不是存储冗余值来减少useState的使用。避开useState可以提高组件的效率。useTransition挂钩通过优先考虑紧急状态更新(例如用户输入)来增强性能。同时,它会推迟不太关键的状态更新,例如搜索结果。使用useTransition也可以防止用户界面阻塞,即使在消耗较高的状态转换期间也能保持交互响应性,从而实现更流畅的体验。将集合状态调用包装在来自useTransitionstartTransition函数中时,允许React中断过时状态更新的渲染,并为最近的状态更新排定渲染优先级。

示例应用

在优化之前

已复制到剪贴板。


import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  Button,
  FlatList,
  TouchableOpacity
} from 'react-native';

// 不当做法: 未记忆的数据和函数
const BadPracticeApp = () => {
  const [count, setCount] = useState(0);
  const [selectedItem, setSelectedItem] = useState(null);

  // 每次渲染都会进行消耗较高的计算,BadPracticeApp级别重新渲染时会出现新的函数实例
  const sumOfSquares = (num) => {
    console.log('正在计算平方和...');
    return Array.from(
      { length: num },
      (_, i) => (i + 1) ** 2
    ).reduce((acc, val) => acc + val, 0);
  };

  // 每次BadPracticeApp重新渲染时都会重新创建数据
  const data = Array.from(
    { length: 50 },
    (_, i) => `项目${i + 1}`
  );

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>平方和{sumOfSquares(10)}</Text>
      <Button
        title="增加计数"
        onPress={() => setCount(count + 1)}
      />
      <FlatList
        data={data}
        keyExtractor={(item) => item} // 使用了匿名函数,因此每次重新渲染时都会创建新的函数实例
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => setSelectedItem(item)}>
            <Text
              style={{
                padding: 10,
                backgroundColor:
                  item === selectedItem ? 'lightblue' : 'white'
              }}>
              {item}
            </Text>
          </TouchableOpacity>
        )} // 使用匿名函数,因此不会记忆每次重新渲染和返回函数组件时创建的新函数实例
      />
    </View>
  );
};

export default BadPracticeApp;

优化后

已复制到剪贴板。


import React, {
  memo,
  StrictMode,
  useState,
  useMemo,
  useCallback,
  useTransition
} from 'react';
import {
  View,
  Text,
  Button,
  FlatList,
  TouchableOpacity,
  ActivityIndicator
} from 'react-native';

// 用于防止不必要重新渲染的记忆的项目组件
const Item = memo(({ item, isSelected, onPress }) => {
  return (
    <TouchableOpacity onPress={() => onPress(item)}>
      <Text
        style={{
          padding: 10,
          backgroundColor: isSelected ? 'lightblue' : 'white'
        }}>
        {item}
      </Text>
    </TouchableOpacity>
  );
});

const BestPracticeApp = () => {
  const [count, setCount] = useState(0);
  const [selectedItem, setSelectedItem] = useState(null);
  const [isPending, startTransition] = useTransition(); // For handling non-urgent updates

  // 记住消耗较高的计算
  const sumOfSquares = useMemo(() => {
    console.log('正在计算平方和...');
    return Array.from(
      { length: 10 },
      (_, i) => (i + 1) ** 2
    ).reduce((acc, val) => acc + val, 0);
  }, []); // 仅当依赖项发生变化时(本例中没有)才重新计算

  // 记忆数据以避免不必要的数组重新创建
  const data = useMemo(
    () => Array.from({ length: 50 }, (_, i) => `项目${i + 1}`),
    []
  );

  // 记忆renderItem函数以避免不必要的重新创建
  const renderItem = useCallback(
    ({ item }) => (
      <Item
        item={item}
        isSelected={item === selectedItem}
        onPress={handleSelectItem}
      />
    ),
    [selectedItem] // 仅在selectedItem更改时重新创建
  );

  // 模拟选择项目时的延迟过程(例如,获取其他数据)
  const handleSelectItem = (item) => {
    startTransition(() => {
      setSelectedItem(item); // 标记所选项目
    });
  };

  return (
    <StrictMode>
      <View style={{ flex: 1, padding: 20 }}>
        <Text>平方和{sumOfSquares}</Text>
        <Button
          title="增加计数"
          onPress={() => setCount(count + 1)}
        />
        <Text>计数{count}</Text>

        <FlatList
          data={data}
          keyExtractor={(item) => item}
          renderItem={renderItem}
          getItemLayout={(data, index) => ({
            length: 50,
            offset: 50 * index,
            index
          })}
        />

        {isPending && (
          <ActivityIndicator size="large" color="#0000ff" />
        )}
      </View>
    </StrictMode>
  );
};

export default BestPracticeApp;

如何处理大型列表

React Native提供了许多不同的列表组件。

Scrollview

出于性能考虑,通常最好使用FlatList组件而不是ScrollView组件,因为FlatList虚拟化会延迟渲染子组件。ScrollView组件一次渲染其所有React子组件。因此,为ScrollView组件使用许多子组件会影响性能。渲染速度变慢,内存使用量增加。但是,对于数据集较小的列表,使用ScrollView是完全可以接受的。记得相应地记忆ScrollView及其子组件。

FlatList

设置FlatList所需的datarenderItem属性并不能发挥使用FlatList组件的所有性能优势。在FlatList组件中设置其他属性,以改善应用的流畅度 (FPS)、CPU利用率和内存使用率。运用React Native官方文章优化FlatList配置中概述的最佳实践(仅提供英文版)。在测试中,亚马逊发现getItemLayoutwindowSizeinitialNumToRender属性以及为子组件添加记忆对于流畅度和CPU利用率很重要。如果您的应用使用嵌套的FlatList来支持垂直和水平滚动,您需要记住嵌套的FlatList组件。

FlashList

Vega全面支持Shopify的FlashList程序包,该程序包为React Native的FlatList组件提供性能更高的替代方案。从FlatList切换到FlashList很简单,因为两者都有相同的组件属性。但是,一些特定于FlatList的属性不再适用于FlashList。这些属性包括windowSizegetItemLayoutinitialNumToRendermaxToRenderPerBatchupdateCellsBatchingPeriod。您可以在这篇使用方法文章的末尾找到完整列表。设置一些关键属性,并遵循这些最佳实践,以实现FlashList的更好性能。

  • 首先,为您的FlashList设置estimatedItemSize属性。在FlashList中使用estimatedItemSize属性可以让列表预渲染适当数量的项目,从而最大限度地减少空白空间和加载时间,从而提高性能。它还通过避免不必要的重新渲染和大型渲染树来提高快速滚动时的响应能力。有关更多详情,您可以阅读这篇文章:预计项目大小属性(仅提供英文版)。
  • FlashList中的项目再循环可以重复使用屏幕外组件而不是销毁它们,从而防止不必要的重新渲染并减少内存使用量,进而提高性能。要对此再循环进行优化,请避免对再循环组件的动态属性使用useState,因为来自先前项目的状态值可能会延续,从而导致效率低下。有关更多详细信息,请参阅再循环(仅提供英文版)。
  • 从项目组件及其嵌套组件中移除key属性。有关更多详情,请参阅这篇文章:移除key属性(仅提供英文版)。
  • 如果您有不同类型的单元组件,而且它们差异很大,可以考虑利用getItemType属性。有关更多详情,请参阅这篇文章:getItemType(仅提供英文版)。

Image组件

React Native在iOS和Android上的Image组件不提供任何开箱即用的性能优化,例如缓存。React Native开发者通常使用诸如react-native-fast-imageexpo-image的社区程序包来为其React Native应用中使用的图像进行记忆或磁盘级缓存。对于适用于Vega的React Native,在Image组件的原生实现中构建了缓存机制。它们的表现就和react-native-fast-imageexpo-image中的Image相似。

我们建议您为自己的用例提供多种尺寸和分辨率的相同图像资产。例如,若在FlatListScrollView组件中渲染图像组件,最好使用裁剪版本或缩略图大小的资产。裁剪或缩略图大小的资源在解码图像时花费的CPU周期更少,而用于原始图像资源的内存也更少。

Animated库

React Native提供了一个Animated库,以实现React Native核心组件上的流畅动画。在React Native中,动画是消耗较高的操作。JS线程逐帧计算动画更新,通过桥将其发送到原生端以生成帧。使用动画还会影响同时运行的其他进程。这可能会让JS线程过载,因此它无法处理计时器、React挂钩的执行、组件更新或其他进程。要避免JS线程过载,请将动画上的useNativeDriver属性设置为true。在原生动画中,将useNativeDriver设置为true;在JS动画中,将useNativeDriver设置为false。有关更多信息,请参阅使用原生驱动程序(只提供英文版)。

我们建议您使用InteractionManager来计划动画。InteractionManager.runAfterInteractions() 采用回调函数或PromiseTask对象,其仅在当前动画或其他交互完成后执行。延迟执行新计划的动画可以降低JS线程过载几率,并提高应用的响应能力和流畅度。

优化您的焦点管理用户界面

当组件获得焦点时,它会调用onFocus回调。当组件失去焦点时,它会调用onBlur回调。这是在用户浏览您的应用时驱动用户界面更改的方式。请尽可能简化您的onFocusonBlur处理程序函数。有几种方法可以简化这些处理程序函数。

如果您的组件在聚焦项目周围绘制边框,请使用条件样式。例如,styles={isFocused ? styles.focusedStyle : styles.blurredStyle}。确保onFocusonBlur调用的每一次组合最多会导致一个React渲染周期。这样,JS线程执行的工作量会很少,用户导航仍能保持敏捷响应。

如果您的组件在获得组件焦点时需要更复杂的用户界面更新,请使用原生动画,这样JS线程就无法完成所有工作。此类用户界面更新的示例包括放大所选视图、文本或图像,或更改不透明度。要进一步优化,请添加一个防抖机制,只有当用户聚焦在项目上超过设定时间时,原生动画才会开始。您的动画会稍微延迟一点,但是使用防抖机制可以防止当用户快速浏览您的应用时,每个聚焦项目的动画都无法启动。

侦听器、事件订阅和计时器

一些Turbo模块 (TM),例如VegaAppStateDeviceInfoKeyboard,有助于您注册特定事件的侦听器。这些侦听器可以在useEffect挂钩中创建。为了避免任何悬空内存,请在useEffect挂钩的返回函数中清理侦听器。

已复制到剪贴板。

useEffect(() => {
  const keboardShowListenerHandler = Keyboard.addListener(
    'keyboardDidShow',
    handleKeyboardDidShow
  );
  return () => {
    keboardShowListenerHandler.remove();
  };
}, []);

减少过度绘制

适用于Vega的React Native应用就像一块画布。当您有不同背景颜色的嵌套视图占据同一个空间时,就相当于是在一遍又一遍地在同一个区域上绘制。例如,如果全屏灰色视图覆盖全屏黑色视图,则黑色图层会变得“完全过度绘制”。 系统会对其进行处理,但它是不可见的。虽然有些过度绘制是可以接受的,尤其是局部过度绘制,但最大限度地减少这些冗余绘制操作可以提高性能。目标是尽可能减少每帧绘制每个像素的次数。您可以通过使用?SHOW_OVERDRAWN=true启动查询参数运行应用,检测过度绘制。

已复制到剪贴板。

vda shell vlcm launch-app "pkg://com.amazon.keplersampleapp.main?SHOW_OVERDRAWN=true"

画布上的用户界面元素现在具有半透明色调。生成的颜色表示画布被过度绘制的次数。

  • 真彩色: 没有过度绘制
  • 蓝色: 过度绘制1次。
  • 绿色: 过度绘制2次。
  • 粉色: 过度绘制3次。
  • 红色: 过度绘制4次或以上。

理想情况下,应用的任何部分都不应包含粉色或红色色调,这意味着不要过度绘制3个或更多图层。

减小bundle大小

对于任何React应用,最大限度地减少Bundle大小以优化内存使用和缩短应用启动时间是至关重要的。react-native-bundle-visualizer有助于识别消耗较高的导入语句或未使用的依赖项。安装完成后,您可以运行以下命令在浏览器中自动打开HTML文件,供您交互式查看。

已复制到剪贴板。

npx react-native-bundle-visualizer

下面是一个示例性的适用于Vega的React Native应用Bundle,其正在尝试通过lodash via import { debounce } from 'lodash';使用防抖。在这个导入语句之后,您可以像下面这样对Bundle进行可视化。

较大bundle大小(点击图像放大)

从上一张图像中,我们可以看到lodash是493.96KB (17.8%)。在将此导入语句切换为import debounce from 'lodash/debounce';后,bundle大小急剧减小。

较小bundle大小(点击图像放大)

现在,lodash只有13.19KB (0.6%)。更改导入语句后,将应用的bundle大小减少了480.77KB。

缩短应用的初始启动时间

利用Vega的Native SplashScreen API来高效地显示您的启动画面。此API使用捆绑在vpkg中的原始图像资产原生渲染启动画面,从而在应用启动时立即可见。这种方法可以腾出应用的JavaScript线程来处理内容加载和网络调用等关键任务,而不是渲染基于JavaScript的启动画面。在应用的第一帧渲染后,原生启动画面会自动关闭。您可以使用两种方法自定义何时关闭初始画面。调用usePreventHideSplashScreen(),它会覆盖启动画面的自动关闭,另外调用useHideSplashScreenCallback(),它会隐藏启动画面。


Last updated: 2026年1月27日