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