一、本课目标
学完这一课,你要建立七个认知:
- 性能优化的本质:减少不必要的函数重执行,减少不必要的 UI 描述对比。
- 渲染优化:纯渲染、稳定引用、细粒度更新------让函数只在必要时执行。
- 列表优化:Lazy 容器、稳定 key、避免内联------让列表只渲染可见项。
- 内存优化:清理副作用、避免泄漏、释放大对象------让内存只在必要时占用。
- 启动优化:减少首屏渲染、延迟加载、预加载------让首屏尽快可见。
- 性能监控与度量:量化指标、定位瓶颈、验证优化------用数据驱动优化。
- 实战能力:能定位性能瓶颈,能写出高性能的声明式 UI------从问题到方案的完整能力。
本课核心线索:
txt
视图 = 函数(状态)
↓
性能问题 = 函数执行太频繁 + UI 描述对比太昂贵
↓
渲染优化:减少函数重执行
↓
列表优化:Lazy 容器 + 稳定 key
↓
内存优化:清理副作用 + 避免泄漏
↓
启动优化:减少首屏渲染 + 延迟加载
↓
性能监控:量化指标 + 定位瓶颈
↓
实战:高性能的声明式 UI
贯穿全文的核心问题:
txt
如果 视图 = 函数(状态),
那么:
函数执行太频繁怎么办? → 第二节:渲染优化
UI 描述对比太昂贵怎么办? → 第三节:列表优化
内存占用太高怎么办? → 第四节:内存优化
首屏加载太慢怎么办? → 第五节:启动优化
怎么知道优化有没有效? → 第六节:性能监控
各框架怎么落地? → 第七节:跨框架对照
二、渲染优化的本质
2.1 一句话说透
渲染优化的本质:减少不必要的函数重执行,减少不必要的 UI 描述对比。
txt
视图 = 函数(状态)
↓
状态变化 → 函数重新执行 → 返回新 UI 描述 → 框架对比新旧描述
↓
性能问题:
1. 函数执行太频繁(不必要的重渲染)
2. UI 描述对比太昂贵(大列表、深层嵌套)
性能优化的两个方向:
txt
方向一:减少函数重执行
→ 状态放在需要它的最近公共祖先
→ 用 memo/equatable 跳过不必要的重渲染
→ 稳定引用(useCallback、useMemo)
方向二:减少 UI 描述对比
→ 减少嵌套深度
→ 用 Lazy 容器
→ 用 Canvas 替代大量视图
2.2 渲染的完整流程
txt
1. 状态变化
2. 框架标记需要重新渲染
3. 重新执行函数,读取状态 ← 瓶颈 1:函数执行太频繁
4. 得到新的 UI 描述 ← 瓶颈 2:UI 描述构建太昂贵
5. 框架对比新旧描述(diff) ← 瓶颈 3:diff 太昂贵
6. 如果有差异,通知底层更新
7. 底层完成界面更新 ← 瓶颈 4:底层更新太频繁
性能瓶颈可能出现在:
| 瓶颈 | 场景 | 优化方向 |
|---|---|---|
| 第 3 步 | 不必要的重渲染 | memo、细粒度状态 |
| 第 4 步 | 大列表、深层嵌套 | Lazy 容器、减少嵌套 |
| 第 5 步 | 大列表、深层嵌套 | 稳定 key、减少嵌套 |
| 第 6-7 步 | 频繁 DOM 操作 | 批量更新、Canvas |
2.3 渲染优化的通用原则
原则一:纯渲染。
tsx
// TSX (React Native)
// 错误:渲染中执行副作用
function Bad() {
fetch('/api/data').then(setData) // 每次渲染都发请求
return <Text>Loading</Text>
}
// 正确:副作用放 useEffect
function Good() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(setData)
}, [])
return <Text>{data}</Text>
}
原则二:稳定引用。
tsx
// TSX (React Native)
// 错误:每次渲染创建新对象/函数
<Child style={{ color: 'red' }} onPress={() => {}} />
// 正确:稳定引用
const styles = { color: 'red' }
const handlePress = useCallback(() => {}, [])
<Child style={styles} onPress={handlePress} />
原则三:细粒度更新。
tsx
// TSX (React Native)
// 错误:状态提升太高,全部重渲染
function App() {
const [input, setInput] = useState('')
return (
<View>
<Header /> {/* 不需要 input,但重渲染 */}
<Content /> {/* 不需要 input,但重渲染 */}
<InputBox value={input} onChange={setInput} />
</View>
)
}
// 正确:状态放在需要它的组件
function App() {
return (
<View>
<Header />
<Content />
<InputBox /> {/* 状态在内部 */}
</View>
)
}
2.4 各框架渲染优化对照
| 框架 | 跳过重渲染 | 稳定引用 | 细粒度更新 |
|---|---|---|---|
| React Native | React.memo |
useCallback、useMemo |
状态就近 |
| SwiftUI | EquatableView |
自动 | 状态就近 |
| ArkUI | @Reusable |
手动 | 状态就近 |
| Compose | 自动跳过 | remember |
状态就近 |
| Flutter | const |
手动 | 状态就近 |
三、渲染优化
3.1 React.memo 跳过重渲染
问题: 父组件重渲染时,子组件也跟着重渲染,即使 props 没变。
tsx
// TSX (React Native)
function Parent() {
const [count, setCount] = useState(0)
return (
<View>
<Text>Count: {count}</Text>
<Button title="+1" onPress={() => setCount(count + 1)} />
<Child /> {/* count 变化时,Child 也重渲染 */}
</View>
)
}
function Child() {
console.log('Child 渲染')
return <Text>Child</Text>
}
优化: 用 React.memo 包裹子组件。
tsx
// TSX (React Native)
const Child = React.memo(function Child() {
console.log('Child 渲染')
return <Text>Child</Text>
})
React.memo 的原理:
txt
React.memo 对比新旧 props:
→ props 没变 → 跳过重渲染
→ props 变了 → 正常重渲染
注意: React.memo 只对比 props,不对比 state 和 context。
3.2 useCallback 稳定函数引用
问题: 每次渲染创建新函数,导致 React.memo 失效。
tsx
// TSX (React Native)
function Parent() {
const [count, setCount] = useState(0)
return (
<View>
<Text>Count: {count}</Text>
{/* 每次渲染创建新函数,Child 的 props 变了 */}
<Child onPress={() => setCount(count + 1)} />
</View>
)
}
const Child = React.memo(function Child({ onPress }) {
return <Button title="+1" onPress={onPress} />
})
优化: 用 useCallback 稳定函数引用。
tsx
// TSX (React Native)
function Parent() {
const [count, setCount] = useState(0)
const handlePress = useCallback(() => {
setCount(c => c + 1)
}, []) // 空依赖数组,函数引用不变
return (
<View>
<Text>Count: {count}</Text>
<Child onPress={handlePress} />
</View>
)
}
useCallback 的原理:
txt
useCallback(fn, deps):
→ deps 不变 → 返回缓存的函数
→ deps 变化 → 返回新函数
空依赖数组:函数永远不变
3.3 useMemo 缓存计算结果
问题: 每次渲染都重新计算昂贵的派生数据。
tsx
// TSX (React Native)
function TodoList({ todos, filter }) {
// 每次渲染都重新过滤
const filteredTodos = todos.filter(todo => todo.category === filter)
return (
<View>
{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</View>
)
}
优化: 用 useMemo 缓存计算结果。
tsx
// TSX (React Native)
function TodoList({ todos, filter }) {
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.category === filter)
}, [todos, filter])
return (
<View>
{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</View>
)
}
useMemo 的使用原则:
txt
1. 简单计算不需要 useMemo
→ 字符串拼接、布尔判断、数组长度
2. 昂贵计算才需要 useMemo
→ 大数组过滤、复杂对象构造、递归计算
3. useMemo 本身有开销
→ 依赖比较、缓存管理
→ 不要过度使用
3.4 细粒度状态更新
问题: 状态提升太高,导致不必要的重渲染。
tsx
// TSX (React Native)
// 错误:整个 App 重渲染
function App() {
const [input, setInput] = useState('')
const [theme, setTheme] = useState('light')
return (
<View>
<Header theme={theme} /> {/* input 变化时重渲染 */}
<Content theme={theme} /> {/* input 变化时重渲染 */}
<InputBox value={input} onChange={setInput} />
</View>
)
}
优化: 把状态拆分为更细的粒度。
tsx
// TSX (React Native)
// 正确:input 状态放在 InputBox 内部
function App() {
const [theme, setTheme] = useState('light')
return (
<View>
<Header theme={theme} />
<Content theme={theme} />
<InputBox /> {/* input 状态在内部 */}
</View>
)
}
function InputBox() {
const [input, setInput] = useState('')
return <TextInput value={input} onChangeText={setInput} />
}
3.5 避免不必要的 Context 重渲染
问题: Context 变化触发所有消费者重渲染。
tsx
// TSX (React Native)
// 问题:theme 变化触发所有消费者重渲染
const AppContext = React.createContext({
theme: 'light',
user: null,
})
function App() {
const [theme, setTheme] = useState('light')
const [user, setUser] = useState(null)
// 每次渲染创建新对象
const value = { theme, user }
return (
<AppContext.Provider value={value}>
<Content />
</AppContext.Provider>
)
}
优化: 拆分 Context,用 useMemo 稳定引用。
tsx
// TSX (React Native)
const ThemeContext = React.createContext('light')
const UserContext = React.createContext(null)
function App() {
const [theme, setTheme] = useState('light')
const [user, setUser] = useState(null)
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<Content />
</UserContext.Provider>
</ThemeContext.Provider>
)
}
优化原则:
txt
1. 按变化频率拆分 Context
→ 频繁变化和稳定的分开
2. 按使用范围拆分 Context
→ 不同组件用的分开
3. 用 useMemo 稳定 Context value
→ 避免每次渲染创建新对象
3.6 各框架渲染优化对照
| 框架 | 跳过重渲染 | 稳定函数 | 缓存计算 | 细粒度 |
|---|---|---|---|---|
| React Native | React.memo |
useCallback |
useMemo |
状态就近 |
| SwiftUI | EquatableView |
自动 | 自动 | 状态就近 |
| ArkUI | @Reusable |
手动 | 手动 | 状态就近 |
| Compose | 自动 | remember |
remember |
状态就近 |
| Flutter | const |
手动 | 手动 | 状态就近 |
四、列表优化
4.1 使用 Lazy 容器
问题: 普通容器一次性渲染所有子视图。
tsx
// TSX (React Native)
// 错误:ScrollView 一次性渲染 1000 项
function BadList() {
return (
<ScrollView>
{Array.from({ length: 1000 }, (_, i) => (
<Text key={i}>Item {i}</Text>
))}
</ScrollView>
)
}
优化: 使用 FlatList(Lazy 容器)。
tsx
// TSX (React Native)
// 正确:FlatList 只渲染可见项
function GoodList() {
return (
<FlatList
data={Array.from({ length: 1000 }, (_, i) => ({ id: i, text: `Item ${i}` }))}
renderItem={({ item }) => <Text>{item.text}</Text>}
keyExtractor={item => item.id.toString()}
/>
)
}
Lazy 容器的原理:
txt
只渲染可见区域的子视图:
→ 初始渲染:只渲染可见的 ~10 项
→ 滚动时:回收离开屏幕的项,渲染新进入屏幕的项
→ 内存中始终只有少量项
各框架 Lazy 容器对照:
| 框架 | Lazy 容器 | 普通容器 |
|---|---|---|
| React Native | FlatList、SectionList |
ScrollView |
| SwiftUI | LazyVStack、LazyHStack、List |
VStack、HStack |
| ArkUI | LazyForEach、List |
ForEach、Column |
| Compose | LazyColumn、LazyRow |
Column、Row |
| Flutter | ListView.builder |
Column |
4.2 稳定 key
问题: 用 index 作为 key,列表重排时状态错乱。
tsx
// TSX (React Native)
// 错误:用 index 作为 key
<FlatList
data={todos}
renderItem={({ item, index }) => <TodoItem key={index} todo={item} />}
/>
优化: 用稳定 id 作为 key。
tsx
// TSX (React Native)
// 正确:用 id 作为 key
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={item => item.id.toString()}
/>
稳定 key 的性能收益:
txt
不稳定 key(index):
列表重排 → 框架认为所有元素都变了 → 重建所有元素
→ 状态丢失 + 性能浪费
稳定 key(id):
列表重排 → 框架知道哪些元素移动了 → 复用元素
→ 状态保留 + 性能优化
4.3 避免内联对象
问题: 每次渲染创建新对象,导致子组件不必要的重渲染。
tsx
// TSX (React Native)
// 错误:每次渲染创建新对象
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={item => item.id.toString()}
contentContainerStyle={{ padding: 16 }} // 新对象
ItemSeparatorComponent={() => <View style={{ height: 8 }} />} // 新组件
/>
优化: 提取到组件外部。
tsx
// TSX (React Native)
const styles = {
contentContainer: { padding: 16 }
}
const ItemSeparator = () => <View style={{ height: 8 }} />
const keyExtractor = (item: Todo) => item.id.toString()
function TodoListView({ todos }) {
return (
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={keyExtractor}
contentContainerStyle={styles.contentContainer}
ItemSeparatorComponent={ItemSeparator}
/>
)
}
4.4 避免内联函数
问题: 每次渲染创建新函数,导致子组件不必要的重渲染。
tsx
// TSX (React Native)
// 错误:每次渲染创建新函数
<FlatList
data={todos}
renderItem={({ item }) => <TodoItem todo={item} />}
keyExtractor={item => item.id.toString()} // 新函数
onEndReached={() => loadMore()} // 新函数
/>
优化: 用 useCallback 稳定函数引用。
tsx
// TSX (React Native)
const keyExtractor = useCallback((item: Todo) => item.id.toString(), [])
const renderItem = useCallback(({ item }) => <TodoItem todo={item} />, [])
const handleEndReached = useCallback(() => loadMore(), [loadMore])
function TodoListView({ todos }) {
return (
<FlatList
data={todos}
renderItem={renderItem}
keyExtractor={keyExtractor}
onEndReached={handleEndReached}
/>
)
}
4.5 列表项组件 memo
问题: 列表项组件在父组件重渲染时也重渲染。
tsx
// TSX (React Native)
function TodoItem({ todo }) {
return <Text>{todo.text}</Text>
}
优化: 用 React.memo 包裹列表项组件。
tsx
// TSX (React Native)
const TodoItem = React.memo(function TodoItem({ todo }) {
return <Text>{todo.text}</Text>
})
4.6 列表性能检查清单
txt
列表性能检查清单:
1. 是否使用了 Lazy 容器?
→ FlatList、LazyVStack、LazyColumn、ListView.builder
2. 是否使用了稳定 key?
→ 用 id,不用 index
3. 是否避免了内联对象?
→ 样式对象提取到组件外部
4. 是否避免了内联函数?
→ 用 useCallback 稳定函数引用
5. 列表项组件是否使用了 memo?
→ 用 React.memo 避免不必要的重渲染
6. 是否使用了 getItemLayout?
→ 固定高度的列表项可以优化滚动性能
7. 是否使用了 windowSize?
→ 控制渲染窗口大小
4.7 各框架列表优化对照
| 框架 | Lazy 容器 | key 优化 | 组件 memo | 其他 |
|---|---|---|---|---|
| React Native | FlatList |
keyExtractor |
React.memo |
getItemLayout |
| SwiftUI | LazyVStack、List |
Identifiable |
EquatableView |
--- |
| ArkUI | LazyForEach、List |
keyGenerator |
@Reusable |
--- |
| Compose | LazyColumn |
key() |
自动跳过 | contentType |
| Flutter | ListView.builder |
ValueKey |
const |
itemExtent |
五、内存优化
5.1 清理副作用
问题: 组件卸载后,副作用还在运行,导致内存泄漏。
tsx
// TSX (React Native)
// 错误:定时器没有清理
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
// 没有清理
}, [])
return <Text>{count}</Text>
}
优化: 清理副作用。
tsx
// TSX (React Native)
// 正确:定时器有清理
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(timer) // 清理
}, [])
return <Text>{count}</Text>
}
常见副作用清理:
txt
1. 定时器:clearInterval / clearTimeout
2. 订阅:unsubscribe
3. 请求:AbortController.abort()
4. 事件监听:removeEventListener
5. WebSocket:close()
6. 动画:cancelAnimationFrame
5.2 避免内存泄漏
问题: 闭包引用导致对象无法释放。
tsx
// TSX (React Native)
// 错误:闭包引用大对象
function Component() {
const bigData = useMemo(() => new Array(1000000).fill(0), [])
useEffect(() => {
const handler = () => {
console.log(bigData.length) // 闭包引用 bigData
}
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [bigData])
return <Text>Component</Text>
}
优化: 避免闭包引用大对象。
tsx
// TSX (React Native)
function Component() {
useEffect(() => {
const handler = () => {
// 不引用大对象
}
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
return <Text>Component</Text>
}
5.3 释放大对象
问题: 大对象一直存在内存中,无法释放。
tsx
// TSX (React Native)
// 错误:图片一直存在内存中
function ImageList({ images }) {
return (
<View>
{images.map(img => (
<Image key={img.id} source={{ uri: img.url }} />
))}
</View>
)
}
优化: 使用 Lazy 容器,只加载可见图片。
tsx
// TSX (React Native)
function ImageList({ images }) {
return (
<FlatList
data={images}
renderItem={({ item }) => (
<Image source={{ uri: item.url }} />
)}
keyExtractor={item => item.id.toString()}
/>
)
}
5.4 避免频繁创建大对象
问题: 每次渲染创建大对象,导致内存抖动。
tsx
// TSX (React Native)
// 错误:每次渲染创建大数组
function Bad() {
const bigArray = new Array(10000).fill(0)
return <Text>{bigArray.length}</Text>
}
优化: 用 useMemo 缓存大对象。
tsx
// TSX (React Native)
function Good() {
const bigArray = useMemo(() => new Array(10000).fill(0), [])
return <Text>{bigArray.length}</Text>
}
5.5 内存优化检查清单
txt
内存优化检查清单:
1. 是否清理了所有副作用?
→ 定时器、订阅、请求、事件监听
2. 是否避免了闭包引用大对象?
→ 闭包只引用必要的变量
3. 是否释放了大对象?
→ 用 Lazy 容器,只加载可见项
4. 是否避免了频繁创建大对象?
→ 用 useMemo 缓存
5. 是否有内存泄漏?
→ 用内存分析工具检查
6. 图片是否压缩?
→ 压缩图片,减少内存占用
7. 缓存是否有上限?
→ 缓存设置大小限制
5.6 各框架内存优化对照
| 框架 | 副作用清理 | 大对象释放 | 内存监控 |
|---|---|---|---|
| React Native | useEffect 清理 |
Lazy 容器 | Flipper、Xcode Instruments |
| SwiftUI | onDisappear |
LazyVStack |
Xcode Instruments |
| ArkUI | aboutToDisappear |
LazyForEach |
DevEco Studio |
| Compose | DisposableEffect |
LazyColumn |
Android Profiler |
| Flutter | dispose |
ListView.builder |
DevTools |
六、启动优化
6.1 减少首屏渲染
问题: 首屏渲染太多内容,启动慢。
tsx
// TSX (React Native)
// 错误:首屏渲染所有内容
function App() {
return (
<ScrollView>
<Header />
<Banner />
<FeaturedSection />
<CategorySection />
<ProductList />
<Footer />
</ScrollView>
)
}
优化: 首屏只渲染可见内容,其余延迟加载。
tsx
// TSX (React Native)
function App() {
const [showMore, setShowMore] = useState(false)
return (
<ScrollView>
<Header />
<Banner />
<FeaturedSection />
{showMore && (
<>
<CategorySection />
<ProductList />
<Footer />
</>
)}
{!showMore && (
<Button title="加载更多" onPress={() => setShowMore(true)} />
)}
</ScrollView>
)
}
6.2 延迟加载
问题: 所有代码打包在一起,启动时加载全部。
tsx
// TSX (React Native)
// 错误:一次性导入所有组件
import HeavyComponent from './HeavyComponent'
import AnotherHeavyComponent from './AnotherHeavyComponent'
优化: 用 React.lazy 延迟加载。
tsx
// TSX (React Native)
import React, { Suspense, lazy } from 'react'
const HeavyComponent = lazy(() => import('./HeavyComponent'))
const AnotherHeavyComponent = lazy(() => import('./AnotherHeavyComponent'))
function App() {
return (
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>
)
}
6.3 预加载
问题: 用户点击后才加载,体验差。
tsx
// TSX (React Native)
// 用户点击后才加载
function App() {
const [showDetail, setShowDetail] = useState(false)
return (
<View>
<Button title="查看详情" onPress={() => setShowDetail(true)} />
{showDetail && <DetailScreen />}
</View>
)
}
优化: 提前预加载。
tsx
// TSX (React Native)
function App() {
const [showDetail, setShowDetail] = useState(false)
// 预加载:组件挂载时就加载 DetailScreen 的代码
useEffect(() => {
import('./DetailScreen')
}, [])
return (
<View>
<Button title="查看详情" onPress={() => setShowDetail(true)} />
{showDetail && <DetailScreen />}
</View>
)
}
6.4 启动优化检查清单
txt
启动优化检查清单:
1. 首屏是否只渲染可见内容?
→ 延迟加载非首屏内容
2. 是否使用了代码分割?
→ React.lazy、动态 import
3. 是否预加载了关键资源?
→ 预加载下一个页面的代码
4. 是否优化了图片?
→ 压缩图片、懒加载图片
5. 是否减少了首屏请求?
→ 合并请求、缓存请求
6. 是否使用了启动屏?
→ 启动屏 + 后台加载
7. 是否分析了启动时间?
→ 用性能分析工具定位瓶颈
6.5 各框架启动优化对照
| 框架 | 代码分割 | 延迟加载 | 启动分析 |
|---|---|---|---|
| React Native | 动态 import | React.lazy |
Flipper |
| SwiftUI | --- | @ViewBuilder |
Xcode Instruments |
| ArkUI | 动态 import | @Builder |
DevEco Studio |
| Compose | --- | @Composable |
Android Profiler |
| Flutter | Deferred | FutureBuilder |
DevTools |
七、性能监控与度量
7.1 性能指标
txt
核心指标:
1. 帧率(FPS):60fps = 每帧 16ms
2. 渲染时间:组件渲染耗时
3. 布局时间:布局计算耗时
4. 内存占用:应用内存使用量
5. 启动时间:应用启动到首屏渲染
关键阈值:
→ 帧率低于 60fps 会卡顿
→ 渲染时间超过 16ms 会掉帧
→ 内存占用超过阈值会被系统杀死
→ 启动时间超过 2s 用户会感知
7.2 性能分析工具
React Native:
txt
1. Flipper
→ 查看组件树、性能、网络
2. React DevTools
→ 查看组件渲染次数
3. Xcode Instruments
→ 查看 CPU、内存、GPU
4. Android Profiler
→ 查看 CPU、内存、网络
SwiftUI:
txt
1. Xcode Instruments
→ 查看 CPU、内存、GPU
2. SwiftUI Inspector
→ 查看视图树
3. os_signpost
→ 自定义性能标记
ArkUI:
txt
1. DevEco Studio
→ 查看 CPU、内存、GPU
2. ArkUI Inspector
→ 查看组件树
3. HiTrace
→ 分布式追踪
Compose:
txt
1. Android Profiler
→ 查看 CPU、内存、网络
2. Layout Inspector
→ 查看组件树
3. Compose Compiler Metrics
→ 查看重组次数
Flutter:
txt
1. DevTools
→ 查看 CPU、内存、网络
2. Flutter Inspector
→ 查看组件树
3. Performance Overlay
→ 查看帧率
7.3 性能优化流程
txt
1. 度量
→ 用工具测量性能指标
→ 确定瓶颈在哪里
2. 定位
→ 找到具体的性能问题
→ 是渲染太频繁?还是 diff 太昂贵?
3. 优化
→ 根据问题选择优化策略
→ 减少重渲染、用 Lazy 容器、清理副作用
4. 验证
→ 重新测量性能指标
→ 确认优化有效
5. 监控
→ 持续监控性能
→ 防止性能退化
7.4 性能优化的优先级
txt
优先级从高到低:
1. 消除明显的性能问题
→ 渲染中发请求、无限循环
→ 内存泄漏、大对象未释放
2. 列表优化
→ Lazy 容器、稳定 key
→ 避免内联对象和函数
3. 渲染优化
→ memo、useCallback、useMemo
→ 细粒度状态更新
4. 启动优化
→ 代码分割、延迟加载
→ 预加载关键资源
5. 微优化
→ 减少对象创建
→ 优化算法复杂度
7.5 性能监控检查清单
txt
性能监控检查清单:
1. 是否定期测量性能指标?
→ 帧率、渲染时间、内存占用
2. 是否使用了性能分析工具?
→ Flipper、Xcode Instruments、DevTools
3. 是否监控线上性能?
→ 上报性能数据、异常监控
4. 是否设置了性能预算?
→ 帧率不低于 55fps
→ 启动时间不超过 2s
5. 是否定期做性能回归测试?
→ 每次发版前测试性能
6. 是否有性能优化文档?
→ 记录优化策略和注意事项
7.6 各框架性能监控对照
| 框架 | 性能分析工具 | 线上监控 |
|---|---|---|
| React Native | Flipper、React DevTools | Firebase Performance |
| SwiftUI | Xcode Instruments | MetricKit |
| ArkUI | DevEco Studio、HiTrace | HiAppEvent |
| Compose | Android Profiler | Firebase Performance |
| Flutter | DevTools、Performance Overlay | Firebase Performance |
八、常见误区
8.1 过早优化
tsx
// TSX (React Native)
// 错误:过早优化,代码复杂但收益小
const MemoizedItem = React.memo(function Item({ item }) {
const style = useMemo(() => ({ color: 'red' }), [])
const handlePress = useCallback(() => {}, [])
return <Text style={style} onPress={handlePress}>{item.text}</Text>
})
// 正确:先测量,确定瓶颈后再优化
function Item({ item }) {
return <Text>{item.text}</Text>
}
原则: 先测量,再优化。不要凭感觉优化。
8.2 过度使用 useMemo
tsx
// TSX (React Native)
// 错误:简单计算不需要 useMemo
const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName])
// 正确:直接算
const fullName = firstName + ' ' + lastName
原则: 只有昂贵的计算才用 useMemo。
8.3 过度使用 useCallback
tsx
// TSX (React Native)
// 错误:不需要稳定的函数用了 useCallback
const handlePress = useCallback(() => {
console.log('pressed')
}, [])
// 正确:简单函数直接定义
const handlePress = () => {
console.log('pressed')
}
原则: 只有传递给 memo 组件的函数才需要 useCallback。
8.4 忽略列表 key
tsx
// TSX (React Native)
// 错误:用 index 作为 key
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
// 正确:用稳定 id
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
8.5 不清理副作用
tsx
// TSX (React Native)
// 错误:没有清理定时器
useEffect(() => {
const timer = setInterval(() => {}, 1000)
}, [])
// 正确:有清理
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer)
}, [])
8.6 首屏渲染太多
tsx
// TSX (React Native)
// 错误:首屏渲染所有内容
function App() {
return (
<ScrollView>
<Header />
<Banner />
{/* ... 很多内容 */}
</ScrollView>
)
}
// 正确:首屏只渲染可见内容
function App() {
const [showMore, setShowMore] = useState(false)
return (
<ScrollView>
<Header />
<Banner />
{showMore && <MoreContent />}
</ScrollView>
)
}
8.7 忽略性能监控
txt
错误:不测量性能,凭感觉优化
正确:定期测量性能,定位瓶颈,针对性优化
8.8 性能优化反模式检查清单
txt
性能优化反模式检查清单:
1. 是否过早优化?
→ 先测量,再优化
2. 是否过度使用 useMemo?
→ 只有昂贵计算才用
3. 是否过度使用 useCallback?
→ 只有传给 memo 组件的函数才用
4. 是否忽略了列表 key?
→ 用稳定 id
5. 是否不清理副作用?
→ 定时器、订阅、请求都要清理
6. 是否首屏渲染太多?
→ 首屏只渲染可见内容
7. 是否忽略性能监控?
→ 定期测量,持续监控
8. 是否只优化不验证?
→ 优化后重新测量
九、习题(15 道)
题目 1(选择题)
渲染优化的本质是什么?
A. 减少代码量
B. 减少不必要的函数重执行,减少不必要的 UI 描述对比
C. 增加缓存
D. 减少组件数量
参考答案:B
解读: 渲染优化的本质是减少不必要的函数重执行和 UI 描述对比。视图 = 函数(状态),性能问题来自函数执行太频繁和 UI 描述对比太昂贵。
题目 2(判断题)
React.memo 可以跳过所有不必要的重渲染。
参考答案:错误
解读: React.memo 只对比 props。如果 props 没变,跳过重渲染。但如果 props 中有每次渲染创建的新对象/函数,React.memo 会失效。需要配合 useCallback 和 useMemo 使用。
题目 3(填空题)
渲染优化的三个通用原则是:、、______。
参考答案:纯渲染;稳定引用;细粒度更新
解读: 纯渲染:渲染函数不做副作用;稳定引用:避免每次渲染创建新对象/函数;细粒度更新:状态放在需要它的最近公共祖先。
题目 4(简答题)
什么是 Lazy 容器?为什么 Lazy 容器能提升列表性能?
参考答案: Lazy 容器是只渲染可见区域子视图的列表容器。
Lazy 容器提升性能的原因:
- 普通容器一次性渲染所有子视图,1000 项就渲染 1000 个
- Lazy 容器只渲染可见的 ~10 项
- 滚动时回收离开屏幕的项,渲染新进入屏幕的项
- 内存中始终只有少量项
各框架的 Lazy 容器:FlatList(React Native)、LazyVStack(SwiftUI)、LazyForEach(ArkUI)、LazyColumn(Compose)、ListView.builder(Flutter)。
题目 5(代码分析题)
下面代码有什么问题?如何优化?
tsx
// TSX (React Native)
function Parent() {
const [count, setCount] = useState(0)
return (
<View>
<Text>Count: {count}</Text>
<Child onPress={() => setCount(count + 1)} />
</View>
)
}
const Child = React.memo(function Child({ onPress }) {
return <Button title="+1" onPress={onPress} />
})
参考答案: 每次渲染创建新函数 () => setCount(count + 1),导致 Child 的 props 变化,React.memo 失效。
优化: 用 useCallback 稳定函数引用。
tsx
// TSX (React Native)
function Parent() {
const [count, setCount] = useState(0)
const handlePress = useCallback(() => {
setCount(c => c + 1)
}, [])
return (
<View>
<Text>Count: {count}</Text>
<Child onPress={handlePress} />
</View>
)
}
题目 6(代码改错题)
下面代码有什么问题?如何修正?
tsx
// TSX (React Native)
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
}, [])
return <Text>{count}</Text>
}
参考答案: 没有清理定时器,组件卸载后定时器继续运行,导致内存泄漏。
修正: 加清理函数。
tsx
// TSX (React Native)
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
return <Text>{count}</Text>
}
题目 7(跨框架对比题)
写出 React Native、SwiftUI、Compose 中列表优化的核心写法。
参考答案:
React Native:
tsx
// TSX (React Native)
<FlatList
data={items}
renderItem={({ item }) => <Item item={item} />}
keyExtractor={item => item.id.toString()}
/>
SwiftUI:
swift
// Swift (SwiftUI)
LazyVStack {
ForEach(items) { item in
ItemView(item: item)
}
}
Compose:
kotlin
// Kotlin (Jetpack Compose)
LazyColumn {
items(items, key = { it.id }) { item ->
ItemView(item)
}
}
解读: 三种框架的列表优化本质相同------用 Lazy 容器只渲染可见项,用稳定 key 标识元素身份。
题目 8(选择题)
以下哪种情况适合用 useMemo?
A. 字符串拼接
B. 布尔判断
C. 大数组过滤
D. 数组长度
参考答案:C
解读: useMemo 适合昂贵的计算,如大数组过滤、复杂对象构造。A、B、D 都是简单计算,直接算即可,不需要 useMemo。
题目 9(判断题)
useCallback 和 useMemo 可以随意使用,不会影响性能。
参考答案:错误
解读: useCallback 和 useMemo 本身有开销(依赖比较、缓存管理)。过度使用会增加性能开销,而不是优化性能。只有需要稳定引用或昂贵计算时才使用。
题目 10(填空题)
React Native 中跳过重渲染用 ______,稳定函数引用用 ______,缓存计算用 ______。
参考答案:React.memo;useCallback;useMemo
解读: React.memo 跳过 props 没变的组件重渲染;useCallback 稳定函数引用,配合 React.memo 使用;useMemo 缓存昂贵的计算结果。
题目 11(简答题)
什么是内存泄漏?如何避免内存泄漏?
参考答案: 内存泄漏是组件卸载后,副作用还在运行,导致内存无法释放。
常见内存泄漏:
- 定时器没有清理
- 订阅没有取消
- 请求没有取消
- 事件监听没有移除
- WebSocket 没有关闭
避免内存泄漏的方法:
- 所有副作用都要清理
- 定时器:
clearInterval/clearTimeout - 订阅:
unsubscribe - 请求:
AbortController.abort() - 事件监听:
removeEventListener - WebSocket:
close()
题目 12(代码分析题)
下面代码有什么问题?如何优化?
tsx
// TSX (React Native)
function App() {
const [input, setInput] = useState('')
const [theme, setTheme] = useState('light')
return (
<View>
<Header theme={theme} />
<Content theme={theme} />
<InputBox value={input} onChange={setInput} />
</View>
)
}
参考答案: input 状态放在 App,导致 Header 和 Content 在 input 变化时也重渲染,即使它们不需要 input。
优化: 把 input 状态放在 InputBox 内部。
tsx
// TSX (React Native)
function App() {
const [theme, setTheme] = useState('light')
return (
<View>
<Header theme={theme} />
<Content theme={theme} />
<InputBox />
</View>
)
}
function InputBox() {
const [input, setInput] = useState('')
return <TextInput value={input} onChangeText={setInput} />
}
题目 13(设计题)
设计一个高性能的聊天消息列表:
- 消息按时间排序
- 新消息插入到顶部
- 消息数量可能很大(1000+)
- 每条消息有头像、文本、时间
写出列表优化、渲染优化、内存优化。
参考答案:
tsx
// TSX (React Native)
// 1. 列表优化:FlatList
// 2. 渲染优化:memo + useCallback
// 3. 内存优化:Lazy 容器 + 清理副作用
const MessageItem = React.memo(function MessageItem({ message }) {
return (
<View>
<Image source={{ uri: message.avatar }} />
<Text>{message.text}</Text>
<Text>{message.time}</Text>
</View>
)
})
const keyExtractor = (item: Message) => item.id
function ChatList({ messages }) {
const renderItem = useCallback(({ item }) => (
<MessageItem message={item} />
), [])
return (
<FlatList
data={messages}
renderItem={renderItem}
keyExtractor={keyExtractor}
inverted // 新消息在底部
initialNumToRender={20}
maxToRenderPerBatch={10}
windowSize={10}
/>
)
}
优化点:
FlatList:Lazy 容器,只渲染可见项React.memo:消息项组件跳过不必要的重渲染useCallback:稳定renderItem引用keyExtractor:稳定 keyinverted:新消息在底部,避免顶部插入导致的列表重排initialNumToRender、maxToRenderPerBatch、windowSize:控制渲染窗口
题目 14(设计题)
设计一个启动优化的方案:
- 首屏只渲染可见内容
- 非首屏内容延迟加载
- 关键资源预加载
- 启动时间不超过 2s
写出首屏优化、代码分割、预加载策略。
参考答案:
tsx
// TSX (React Native)
// 1. 首屏优化:只渲染可见内容
function App() {
return (
<Suspense fallback={<SplashScreen />}>
<HomeScreen />
</Suspense>
)
}
function HomeScreen() {
const [showMore, setShowMore] = useState(false)
return (
<ScrollView>
<Header />
<Banner />
{showMore && (
<Suspense fallback={<Spinner />}>
<MoreContent />
</Suspense>
)}
</ScrollView>
)
}
// 2. 代码分割:React.lazy
const MoreContent = lazy(() => import('./MoreContent'))
const DetailScreen = lazy(() => import('./DetailScreen'))
// 3. 预加载:提前加载下一个页面
function HomeScreen() {
useEffect(() => {
// 预加载详情页代码
import('./DetailScreen')
}, [])
return <View>...</View>
}
优化点:
React.lazy:代码分割,按需加载Suspense:加载时显示 fallback- 首屏只渲染可见内容
useEffect预加载下一个页面
题目 15(思考题)
为什么说"性能优化就是减少不必要的函数重执行"?请从 视图 = 函数(状态) 的角度分析。
参考答案: 视图 = 函数(状态) 的核心是"状态变化触发函数重新执行"。
从渲染管线角度:
txt
1. 状态变化
2. 框架标记需要重新渲染
3. 重新执行函数,读取状态 ← 性能瓶颈 1
4. 得到新的 UI 描述 ← 性能瓶颈 2
5. 框架对比新旧描述(diff) ← 性能瓶颈 3
6. 如果有差异,通知底层更新
7. 底层完成界面更新
性能优化就是减少第 3、4、5 步的开销:
减少函数重执行(第 3 步):
- 状态放在需要它的最近公共祖先
React.memo跳过 props 没变的组件useCallback、useMemo稳定引用- 细粒度状态更新
减少 UI 描述构建(第 4 步):
- 减少嵌套深度
- 用 Lazy 容器
- 用 Canvas 替代大量视图
减少 diff 开销(第 5 步):
- 稳定 key
- 减少列表长度
- 减少嵌套深度
终极结论: 性能优化就是减少不必要的函数重执行和 UI 描述对比。具体来说:
- 能跳过的重渲染就跳过(memo、细粒度)
- 能懒加载的就懒加载(Lazy 容器、代码分割)
- 能复用的就复用(稳定 key、useMemo)
- 能清理的就清理(副作用、大对象)
这就是 视图 = 函数(状态) 在性能优化上的体现------让函数只在必要时执行,让 UI 描述只在必要时对比。
十、下节预告
第 10 课:测试与调试------单元测试、组件测试、端到端测试、调试技巧。