6.2 列表虚拟化与FlatList调优 本节摘要:长列表是移动端性能的第一主战场,解法只有一个词:虚拟化——只渲染看得见的,看不见的先不画。本节拆解 FlatList 的窗口工作原理,讲清初始渲染数量、预渲染距离、条目记忆化三组关键参数的调法,给出一份高频反模式清单,并用一个万人榜单的调优实录走完全流程。学完本节,列表「滑不动」「跳变」「白屏闪」三类顽疾都有对应药方。 只画看得见的:虚拟化的工作原理 原理一句话:即使数据源有一万条,列表组件也只在屏幕附近维护少量「活的」列表项——屏幕内的必画,屏幕外再预留一段缓冲区提前渲染,更远的条目根本不存在于视图树里;滚动时,进入缓冲区的条目被创建、远去的被回收。这就是虚拟化窗口。
本节摘要:长列表是移动端性能的第一主战场,解法只有一个词:虚拟化——只渲染看得见的,看不见的先不画。本节拆解 FlatList 的窗口工作原理,讲清初始渲染数量、预渲染距离、条目记忆化三组关键参数的调法,给出一份高频反模式清单,并用一个万人榜单的调优实录走完全流程。学完本节,列表「滑不动」「跳变」「白屏闪」三类顽疾都有对应药方。
原理一句话:即使数据源有一万条,列表组件也只在屏幕附近维护少量「活的」列表项——屏幕内的必画,屏幕外再预留一段缓冲区提前渲染,更远的条目根本不存在于视图树里;滚动时,进入缓冲区的条目被创建、远去的被回收。这就是虚拟化窗口。它解释了列表调优的全部关键参数:初始渲染多少条(决定进入页面时画多少)、缓冲区留多远(决定快滑时来不来得及造)、条目回收后缓存什么(决定回滚时闪不闪)。
先看标准写法,三个关键参数已就位:
import React from 'react'; import { FlatList, Text, View, StyleSheet } from 'react-native'; const RankItem = React.memo(function RankItem({ item }) { // 记忆化:只有本条数据变化才重渲染,滚动中父刷新与我无关 return ( <View style={styles.row}> <Text style={styles.rank}>{item.rank}</Text> <Text style={styles.name}>{item.name}</Text> <Text style={styles.score}>{item.score}</Text> </View> ); }); export function RankList({ data, onEndReached }) { return ( <FlatList data={data} renderItem={({ item }) => <RankItem item={item} />} keyExtractor={item => item.id} // 初始只渲染 10 条,进页面更快 initialNumToRender={10} // 缓冲区:视口前后各多准备 10 个屏高,快滑不白屏 windowSize={10} // 分批渲染步长:进入缓冲区的条目按 20 个一批造 maxToRenderPerBatch={20} onEndReached={onEndReached} onEndReachedThreshold={0.5} /> ); } const styles = StyleSheet.create({ row: { flexDirection: 'row', padding: 14, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#e4eaf0' }, rank: { width: 40, color: '#7a8a99' }, name: { flex: 1, color: '#2c3e50' }, score: { color: '#1a73e8' }, });

初始渲染数量(initialNumToRender):按「首屏能看见几条」加少量余量设定,缺省值偏保守的页面可下调以提速首屏。它的另一半意义在「跳转定位」:回滚到列表顶部靠的是这批初始条目,设得过小会让「回顶部」时出现补渲染闪动。
窗口与批次(windowSize 与每批渲染数量):窗口越大,快滑越不容易白屏,但同时活着的条目越多、内存与渲染压力越大;每批渲染数量决定进入缓冲区后造条目的「批粒度」,批太大一次吃掉长线程时间片(JS 帧尖峰),批太小频繁打断调度。经验起点:窗口从缺省开始,快滑白屏就加大、内存告警就减小;每批数量在十几到三十之间微调,配合 6.1 的帧率曲线验证。
条目记忆化:列表项包上记忆化、渲染回调里不写内联函数对象、取数逻辑下沉——这一组在 6.1 案例里已经预演过。这里补一条列表专属纪律:抽取函数返回的组件模板要与记忆化组件配合使用,模板每次执行生成新类型会让 React 以为整列都换了,记忆化形同虚设——这是列表「莫名全量重渲染」的常见暗坑。
其一,用 ScrollView 装长列表:ScrollView 没有虚拟化,一万个子组件全量挂载,数据量一上来直接卡死——长数据必须用列表组件。其二,条目高度不定且未声明:虚拟化依赖位置估算,不定高条目会带来跳变与定位漂移,能定高就定高,不能就给合格的估算值。其三,把复杂交互塞进条目:条目里嵌视频播放器、倒计时器、重型动画,每个条目都拖着一套定时任务,几十条就把线程拖垮——重交互条目要按需激活(进入视口才启动定时器)。其四,在渲染回调里做重计算:排序、过滤、分组应在数据层完成后传入,列表只负责展示。其五,无 key 或用下标当 key:插入删除时下标键让整列重新对账,用稳定业务标识。
榜单、通讯录这类带分组头的内容是列表的进阶形态,实现思路决定成败。朴素做法(分组头作为普通条目混排)胜在简单,但失去吸顶能力;吸顶做法(分组头悬浮在顶部、滚动时切换标题)体验最好,但要把「当前滚到哪个分组」的计算交给列表的可视项回调,在原生侧完成吸顶切换,避免每帧回 JS 计算。无论哪种做法,一条底线不变:吸顶头必须是窗口体系的一部分,独立悬浮在列表之外的组件会常驻渲染,等于给每个页面白送一个永不回收的负担——这与 7.2 讲的快照降级是同一条资源哲学。
背景:运营活动的排行榜冲到上万人参与,反馈集中为三点:进入榜单页慢、快速滑动白屏、下拉刷新后跳动。操作分三段:第一段治「进入慢」——初始渲染降到首屏量,条目组件瘦身(去掉条目内的相对时间倒计时,改为整点批量刷新一次);第二段治「白屏」——图片改用列表专用的预取与降采样方案,缓冲窗口适度加大,快滑时缓冲区提前造条目;第三段治「跳动」——为不定高的带图条目记录首次测量高度作为缓存估算值,并把下拉刷新改为保持滚动位置的就地更新。结果:中端 Android 真机进入时间明显缩短,快滑白屏率大幅下降,刷新不再跳顶。解读:三段手术分别对应本节的三组参数与反模式清单——列表调优没有神秘技巧,全是「量出来、对号入座、逐项验证」。变式:分区列表(带吸顶分组头)是本节的进阶版,把分组头作为特殊条目交给虚拟化统一管理,而非独立悬浮组件——后者会脱离窗口体系,成为滚动时常驻的渲染负担。
时间账修完,下一节补空间账:内存泄漏的四个惯犯,以及一套够用的调试工具链。