应用性能最佳实践
由于适用于Vega的React Native应用刚好是React Native应用,因此有关React Native最佳实践的公开文档也适用。考虑在构建适用于Vega的React Native应用时的这些关键最佳实践,以及Vega与iOS或Android的主要区别。
视频概述
观看此视频,大致了解如何在Vega上提高应用性能。(附带中文字幕)
避免重新渲染和重新定义
memo
使用React的memo可以防止不必要地重新渲染功能组件,从而提高性能。默认情况下,每当组件的父组件重新渲染时,React都会重新渲染组件,即使其属性没有改变。React的memo通过浅显地比较之前属性和新属性来优化这一点,如果它们相同,则跳过重新渲染。这对于接收不需要经常更新的稳定属性的组件非常有利,例如列表项、按钮或具有大量渲染逻辑的用户界面元素。当与useCallback和useMemo结合使用时,memo还可以最大限度地减少浪费的渲染,从而提高React应用的效率。
默认情况下,memo使用Object.is静态方法对属性进行浅显比较,以确定组件是否应该重新渲染。在某些情况下,您可能需要提供自己的arePropsEqual函数来自定义此比较。一个常见的用例是将行内样式或匿名定义的属性值传递给函数组件。在这些情况下,属性的值相等,但在内存中不相等,因此Object.is() 比较会将它们标记为不相等。在将复杂的对象或函数作为属性进行处理时,自定义比较很有用,在这种情况下,仅进行浅显比较是不够的。自定义比较功能可以更好地控制组件的更新时间,从而提高需要深入比较的场景或特定条件下的性能。
用于调查React组件重新渲染的两个有用工具会包括react-devtools分析器中的“Record why each component rendered while profiling”(在分析时记录渲染每个组件的原因)标记,以及社区程序包why-did-you-render (WDYR)。
通常,最好将用户定义的函数组件包装在memo API中。但是,在任何地方都使用memo和useMemo可能不可取。请参阅React的文章,应该在任何地方都添加memo吗?(仅提供英文版),其中说明了何时以及如何使用memo和useMemo。
React编译器和eslint-plugin-react-compiler
在React 19候选版本 (RC) 中,React引入了静态React代码编译器,它使用memo、useMemo和useCallback自动应用记忆。编译器假定您的应用遵循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>与该组件协同作用,允许应用“暂停”渲染,直到加载必要的组件,从而在不阻塞用户界面的情况下创造流畅的用户体验。这种方法不仅可以优化应用的启动时间,还可以提高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的依赖项数组中添加项目,以确保无论何时其依赖项发生变化,都会重新计算记忆的值。省略依赖项会导致值过时或不正确,而不必要的依赖项会导致不必要的重新计算,从而降低性能优势。
useState和useTransition
尽量减少React的useState使用量可以通过减少不必要的重新渲染来提高性能。每次状态更新都会触发重新渲染,因此过度的状态管理会导致性能瓶颈,尤其是在复杂的组件中。通过最大限度地减少useState的使用,例如对不影响渲染的可变值使用引用 (useRef),或者派生状态而不是存储冗余值,可以提高组件的效率。useTransition挂钩通过优先考虑紧急状态更新(例如用户输入)来增强性能。同时,它会推迟不太关键的状态更新,例如搜索结果。这样可以防止用户界面阻塞,即使在消耗较高的状态转换期间也能保持交互响应性,从而确保更流畅的体验。将集合状态调用包装在来自useTransition的startTransition函数中时,允许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所需的data和renderItem属性并不能发挥使用FlatList组件的所有性能优势。在FlatList组件中设置其他属性,以改善应用的流畅度 (FPS)、CPU利用率和内存使用率。运用React Native官方文章优化FlatList配置中概述的所有最佳实践。在测试中,亚马逊发现getItemLayout、windowSize和initialNumToRender属性以及为子组件添加记忆对于流畅度和CPU利用率很重要。如果您的应用使用嵌套的FlatList来支持垂直和水平滚动,您需要记住嵌套的FlatList组件。
FlashList
Vega全面支持Shopify的FlashList程序包,该程序包为React Native的FlatList组件提供性能更高的替代方案。从FlatList切换到FlashList很简单,因为两者都有相同的组件属性。但是,一些特定于FlatList的属性不再适用于FlashList。这些属性包括windowSize、getItemLayout、initialNumToRender、maxToRenderPerBatch和updateCellsBatchingPeriod。您可以在这篇使用方法文章的末尾找到完整列表。设置一些关键属性,并遵循这些最佳实践,以实现FlashList的更好性能。
- 首先,请务必为您的
FlashList设置estimatedItemSize属性。在FlashList中使用estimatedItemSize属性时,可以让列表预渲染适当数量的项目,最大限度地减少空白空间和加载时间,同时通过避免不必要的重新渲染和大型渲染树来增强快速滚动期间的响应能力,从而帮助提高性能。有关更多详情,您可以阅读这篇文章:预计项目大小属性(仅提供英文版)。 - 在
FlashList中使用项目再循环时,可以重复使用屏幕外组件而不是销毁它们,从而防止不必要的重新渲染并减少内存使用量,进而提高性能。要对此进行优化,请避免对再循环组件的动态属性使用useState,因为来自先前项目的状态值可能会延续,从而导致效率低下。有关更多详情,请参阅这篇文章:再循环(仅提供英文版)。 - 从项目组件及其嵌套组件中移除key属性。有关更多详情,请参阅这篇文章:移除key属性(仅提供英文版)。
- 如果您有不同类型的单元组件,而且它们差异很大,可以考虑利用
getItemType属性。有关更多详情,请参阅这篇文章:getItemType(仅提供英文版)。
Image组件
React Native在iOS和Android上的Image组件不提供任何开箱即用的性能优化,例如缓存。React Native开发者通常使用诸如react-native-fast-image或expo-image的社区程序包来为其React Native应用中使用的图像进行记忆或磁盘级缓存。对于适用于Vega的React Native,在Image组件的原生实现中构建了缓存机制。它们的表现就和react-native-fast-image或expo-image中的Image相似。
我们建议您为自己的用例提供多种尺寸和分辨率的相同图像资产。例如,若在FlatList或ScrollView组件中渲染图像组件,最好使用裁剪版本或缩略图大小的资产。这将减少解码图像所花费的CPU周期,减少原始图像资产的内存使用量。
Animated库
React Native提供了一个Animated库,以实现React Native核心组件上的流畅动画。在React Native中,动画是消耗较高的操作。JS线程逐帧计算动画更新,通过桥将其发送到原生端以生成帧。使用动画还会影响同时运行的其他进程。这可能会让JS线程过载,因此它无法处理计时器、React挂钩的执行、组件更新或其他进程。要避免这种情况,请将动画上的useNativeDriver属性设置为true。在原生动画中,将useNativeDriver设置为true;在JS动画中,将useNativeDriver设置为false。有关这方面的更多信息,请参阅使用原生驱动程序(仅提供英文版)。
还建议您使用InteractionManager来计划动画。InteractionManager.runAfterInteractions() 采用回调函数或PromiseTask对象,其仅在当前动画或其他交互完成后执行。延迟执行新计划的动画可以降低JS线程过载几率,并有助于提高应用的整体响应能力和流畅度。
优化您的焦点管理用户界面
当组件获得焦点时,会调用其onFocus回调,当组件失去焦点时,会调用其onBlur回调。这是在用户浏览您的应用时驱动用户界面更改的主要方式。请尽可能简化您的onFocus和onBlur处理程序函数。有几种方法可以做到这一点。
如果您的组件仅在聚焦项目周围绘制边框,请使用条件样式。例如,styles={isFocused ? styles.focusedStyle : styles.blurredStyle}。确保onFocus和onBlur调用的每一次组合最多会导致一个React渲染周期。这样,JS线程执行的工作量会很少,用户导航仍能保持敏捷响应。
如果您的组件在获得焦点时需要更复杂的用户界面更新,请通过原生动画执行该工作,这样就不会在JS线程本身上完成该工作。此类用户界面更新的示例包括放大所选视图、文本或图像,或更改不透明度。要进一步优化,请添加一个防抖机制,只有当用户聚焦在项目上超过设定时间时,原生动画才会开始。虽然会导致稍后开始播放动画这种轻微影响,但这种延迟将防止用户在应用中快速滚动或导航时,每个聚焦项目都无法启动动画。
侦听器、事件订阅和计时器
一些Turbo模块 (TM),例如VegaAppState、DeviceInfo或Keyboard,让您可以注册特定事件的侦听器。这些侦听器通常是在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进行可视化。
从上图中我们可以看出,lodash为493.96KB (17.8%)。在将此导入语句切换为import debounce from 'lodash/debounce';后,bundle大小急剧减小。
现在,lodash只有13.19KB (0.6%)。更改导入语句后,将应用的bundle大小减少了480.77KB。
缩短应用的初始启动时间
利用Vega的Native SplashScreen API来高效地显示您的启动画面。此API使用捆绑在vpkg中的原始图像资产原生渲染启动画面,从而确保应用启动时立即可见。这种方法可以腾出应用的JavaScript线程来处理内容加载和网络调用等关键任务,而不是渲染基于JavaScript的启动画面。在应用的第一帧渲染后,原生启动画面会自动关闭。使用两种方法自定义启动画面关闭的时间。调用usePreventHideSplashScreen(),它会覆盖启动画面的自动关闭,另外调用useHideSplashScreenCallback(),它会隐藏启动画面。
相关主题
- 衡量应用KPI
- 使用Vega ESLint插件发现性能问题
- 如果您要开发WebView应用,请参阅Vega网页应用性能最佳实践
Last updated: 2025年10月1日



