1. 从一次搜索卡顿说起:RN on HarmonyOS的性能短板
在 React Native 鸿蒙项目里做搜索页,是我第一次真正意识到“缓存”不是一个后端概念,而是前端交互流畅度的命根子。当时在真机上输入一个关键词,列表过滤加上重新渲染,整帧直接掉到 20fps 以下,键盘都跟着掉帧。把 React Native for OpenHarmony 的 JSI 桥接、组件渲染开销和搜索过滤计算叠在一起,问题就会在鸿蒙设备上被放大:同样的逻辑在 Android 上还能忍,在鸿蒙的低端机上几乎不可用。后来我把搜索结果的过滤计算用 useMemo 缓存起来,同时给网络请求结果加了一层带 TTL 的内存缓存,才把性能拉回到可用水平。
这篇文章不绕弯子,直接讲我在这条路上验证过的方案:useMemo 在 React Native 鸿蒙里的正确用法、搜索结果缓存怎么设计、失效策略怎么定、以及我自己踩过的几个坑。适合正在做 React Native 鸿蒙搜索页、列表页的开发者,也适合那些想搞清楚 useMemo 到底能在“搜索结果缓存”里承担什么角色的人。
1.1 为什么 HarmonyOS 上的 RN 和 Android/iOS 不一样
React Native 在鸿蒙上的运行路径并不是 Android 的简单移植。React Native for OpenHarmony 社区版有自己的 JS 引擎调度、NAPI 转换层、组件映射和打包工具链,很多在 Android 上可以被系统原生能力隐藏的损耗,在鸿蒙上会直接暴露在 JS 线程里。比如同一个 FlatList,Android 有成熟的 RecyclerView 后端做回收,鸿蒙端如果还用最朴素的 ScrollView + map 渲染,那渲染耗时会被指数级放大。
更要命的是开发调试阶段的“假流畅”。很多团队在预览器、模拟器上测不出问题,一到真机就发现输入框每敲一个字都要白屏一下。这就是因为模拟器上 JS 引擎和原生渲染都在同一台高性能电脑上跑,压力被机器性能掩盖了;到了鸿蒙真机上,CPU 调度、内存带宽、原生侧组件复用效率的问题全部显形。
所以做 RN 鸿蒙性能优化,不能沿用“先写完再说”的思路,必须在架构阶段就把“哪些计算是重复的”“哪些渲染是多余的”“哪些内存是白占的” 问清楚。搜索结果缓存就是这三个问题的交集。
1.2 搜索卡顿的根因:过滤计算与状态更新叠加
搜索卡顿看起来是渲染问题,其实大部分是“重复计算 + 重复渲染”的叠加。一个典型的搜索页长这样:输入框的 onChangeText 直接 setKeyword,列表组件在 render 里对全量数据做一次 filter,再把过滤结果 map 成行组件。每一次按键,状态更新、组件重新执行、过滤计算全量重跑。
在鸿蒙端,这个链路还会被放大。React Native 的更新过程要经过 JS 线程计算 diff,再通过 bridge/NAPI 同步到原生侧;如果 JS 线程被大数据量的 filter 占住,diff 计算就只能排队。输入框每敲一个字,键盘回弹和文本更新都要和过滤任务抢 JS 线程,掉帧就来了。
用 useMemo 做搜索过滤缓存,本质上是把“过滤计算”的重复开销减掉:只有 keyword、dataSource、filters 这些真正影响结果的依赖变化了,才重新执行 filter。否则直接拿上一次的计算结果,JS 线程只要做一次引用比较,代价几乎为零。
但这里要提醒一句:useMemo 不是用来缓存“网络搜索结果”的。它缓存的是“根据输入计算出来的值”,也就是同步的派生数据。真正的网络请求结果缓存,要靠 Map、AsyncStorage 或者专门的请求层缓存组件来管理。很多人在热搜词里搜“useMemo 搜索结果缓存”,其实是把两个不同的“缓存”混在一起了。后面我会分开讲。
1.3 先把性能问题分层:网络缓存、计算缓存、渲染缓存
动手写代码前,我会把“缓存”拆成三层:
- 网络层缓存:相同关键词的搜索结果,在 TTL 内不重复请求,直接复用上次响应。
- 计算层缓存:状态或输入没变时,不重新执行
filter、sort、groupBy等纯函数逻辑。 - 渲染层缓存:列表项 props 没有变化时,不重新 render 子组件。
useMemo 管的是第二层;memo 管第三层;第一层需要自己搭一个缓存 Store。很多项目只做了第三层(比如给列表项加 memo),但忽略了第二层——结果每次按键还是要把全量数据过滤一遍,只是子组件减少了重复渲染,JS 线程的过滤耗时依然存在。等三项全做齐,搜索页在鸿蒙上的表现才会有质变。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. useMemo 为什么能救搜索页:缓存的计算与时机取舍
useMemo 不是新东西,但在 React Native 鸿蒙上,很多人对它的理解停留在“性能优化 Hook”这个标签上。实际上它解决的问题非常具体:在函数组件每次执行时,如果某个计算的依赖没有变化,React 就直接返回上次缓存的值,而不是重新算一遍。
2.1 useMemo 在 RN 鸿蒙中的工作方式
先看最典型的用法:
tsx复制const filteredList = useMemo(() => {
if (!keyword.trim()) return allData;
return allData.filter(item => item.title.includes(keyword.trim()));
}, [keyword, allData]);
当 keyword 或 allData 的引用没有变化时,React 会跳过这个箭头函数的执行,直接返回上一次的 filteredList。注意,这里比较依赖用的是 Object.is。如果 allData 是每次渲染都新建的数组(比如 getAllData() 写在组件体里),那依赖永远在变,缓存就会一直失效,等于白写。
在鸿蒙端,这个机制还有一层额外价值:RN 的 JS 线程本身就比原生侧更拥挤,减少一次全量过滤,等于给 React 的 diff 和原生编排腾出了宝贵的线程时间。实测在 2000 条数据下,filter 耗时大约 8-15ms 次;如果每次按键都算,一秒 10 次输入就是 100ms 以上的纯计算开销,足够把 JS 线程打满。
2.2 useMemo 缓存的是值,不是异步请求
这是最容易混淆的地方。useMemo 的工厂函数必须是同步的,不能在里面 await 一个接口,否则 React 也拿不到 Promise 的返回值。真正的“搜索结果缓存”如果来自远端接口,需要这样处理:
- 把“缓存容器”用
useMemo创建,保证生命周期内是同一个对象。
