主要思路:从 ScrollView 手动瀑布流到 FlashList Masonry
背景
在壁纸类应用中,列表页通常是“无限下拉 + 大量图片”的重灾区。最初页面使用 ScrollView 自己维护两列瀑布流(手动分列、手动计算高度、滚动到底触发分页)。当用户连续下拉几十页后,会出现明显卡顿:
视图树持续膨胀(
ScrollView会把已渲染的内容一直留在内存中)图片组件与布局计算越来越多,JS 与 UI 线程压力上升
触发分页越多,渲染与布局抖动越明显
同时,我们还增加了“全部 / 精华”筛选需求,并要求:
切换筛选必须高亮状态正确
即使筛选结果为空,筛选栏也不能消失
分页、点击图片弹层等交互需要保持不变
涉及页面:
app/(tabs)/mobile-wallpaper/[category].tsxapp/(tabs)/avatar-wallpaper.tsx
技术选型
1) 为什么从 ScrollView 换到虚拟列表
ScrollView 适合少量内容的滚动展示,但不适合“长列表 + 大量图片”。核心原因是:
ScrollView不会虚拟化,历史内容不会被回收数据越多,内存占用越高,渲染成本越大
因此需要换成具备虚拟化能力的列表:
FlatList:基础虚拟化,但性能在复杂场景(大图、瀑布流、高频更新)上不够理想@shopify/flash-list:更激进的性能优化,更适合图片流场景
最终选择:@shopify/flash-list。
2) Masonry(真瀑布流)怎么选
一开始用 FlashList + numColumns=2 能解决虚拟化问题,但它本质是“网格”,会按行对齐;当同一行两张图片高度差大时,会出现“空洞/大间隔”,视觉效果不好。
要实现“下面那张图能顶上来”的真瀑布流,需要 Masonry 布局。
在 @shopify/flash-list v2 中:
不再通过
MasonryFlashList组件(很多人会按旧文档/旧代码去 import,导致报错)正确方式是:在
FlashList上使用masonryprop
并且可以开启:
optimizeItemArrangement:更均衡地分配 item,减少列高差异
最终方案:FlashList + masonry + numColumns=2 + optimizeItemArrangement。
关键实现路径(分阶段演进)
阶段 A:ScrollView 手动瀑布流(问题版本)
典型实现特征:
自己把
records按 index 或高度分到 left/right 两列ScrollView内渲染两列View通过
onScroll或“接近底部”判断触发下一页
主要问题:
长列表卡顿越来越严重(历史内容不回收)
手动瀑布流维护成本高(分列、间距、错位、空态等容易出 bug)
阶段 B:FlashList 虚拟列表(先解决性能)
改造要点:
将
ScrollView替换为FlashList分页由
onEndReached负责顶部筛选栏放到
ListHeaderComponent,确保它不会因为空态/加载态被“整页替换”而消失空态与加载态放到
ListEmptyComponent底部加载更多提示放到
ListFooterComponent
收益:
虚拟化生效,滚动性能显著提升
列表渲染与分页逻辑更清晰
阶段 C:FlashList Masonry(解决真瀑布流与空洞)
在阶段 B 的基础上开启 Masonry:
masonrynumColumns={2}optimizeItemArrangement
效果:
item 会按列堆叠,避免网格行对齐造成的“空洞”
更接近“传统瀑布流”的视觉体验
让筛选栏在空态也可见(UI 稳定性)
出现过的问题:
当筛选结果为空(尤其是精华为空)时,如果用“提前 return 空页面”的方式渲染空态,会导致标题与筛选栏一起消失
正确做法:
页面结构始终渲染
FlashList筛选栏放
ListHeaderComponent空态与 loading 只在
ListEmptyComponent内切换
这样:
空态不会替换整页
筛选 UI 永远可见,且高亮状态保持一致
得到的优化提升
性能与内存
ScrollView-> 虚拟列表后,历史 item 会被回收,内存占用增长显著放缓长时间滚动依然保持流畅,卡顿明显减少
交互体验
筛选栏固定存在(空态也不消失)
分页逻辑更稳定(
onEndReached)Masonry 真瀑布流减少“空洞”,观感更高级
可维护性
删除手动分列与滚动监听相关逻辑
使用
ListHeaderComponent / ListEmptyComponent / ListFooterComponent把页面结构组织得更清晰
关键踩坑与解决
1) MasonryFlashList 导入报错
现象:
Module '"@shopify/flash-list"' has no exported member 'MasonryFlashList'
原因:
项目安装的是
@shopify/flash-listv2,masonry 的 API 与 v1 不同
解决:
使用
FlashList并开启masonryprop
2) 仅 numColumns=2 不是瀑布流
现象:
出现大间隔/空洞
原因:
网格布局按行对齐,高度不一致会留下空白
解决:
使用
masonry真瀑布流可选开启
optimizeItemArrangement