主要思路:从 ScrollView 手动瀑布流到 FlashList Masonry

背景

在壁纸类应用中,列表页通常是“无限下拉 + 大量图片”的重灾区。最初页面使用 ScrollView 自己维护两列瀑布流(手动分列、手动计算高度、滚动到底触发分页)。当用户连续下拉几十页后,会出现明显卡顿:

  • 视图树持续膨胀(ScrollView 会把已渲染的内容一直留在内存中)

  • 图片组件与布局计算越来越多,JS 与 UI 线程压力上升

  • 触发分页越多,渲染与布局抖动越明显

同时,我们还增加了“全部 / 精华”筛选需求,并要求:

  • 切换筛选必须高亮状态正确

  • 即使筛选结果为空,筛选栏也不能消失

  • 分页、点击图片弹层等交互需要保持不变

涉及页面:

  • app/(tabs)/mobile-wallpaper/[category].tsx

  • app/(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 上使用 masonry prop

并且可以开启:

  • 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:

  • masonry

  • numColumns={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-list v2,masonry 的 API 与 v1 不同

解决:

  • 使用 FlashList 并开启 masonry prop

2) 仅 numColumns=2 不是瀑布流

现象:

  • 出现大间隔/空洞

原因:

  • 网格布局按行对齐,高度不一致会留下空白

解决:

  • 使用 masonry 真瀑布流

  • 可选开启 optimizeItemArrangement