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创建,保证生命周期内是同一个对象。 - 请求发出前,先查缓存容器;命中就直接设置 state,不请求。
- 请求成功后,写入缓存容器,再设置 state。
- 缓存容器配合 TTL 和 LRU 策略,防止无限膨胀。
这样设计之后,useMemo 依然扮演了关键角色:它让缓存容器不随渲染重建。如果不加 useMemo,直接在组件体里 const cache = new Map(),那么每次渲染都会生成一个新的 Map,之前存的结果全部丢失,缓存形同虚设。
2.3 依赖数组的设计:从“值依赖”到“稳定依赖”
useMemo 的缓存命中,完全取决于依赖数组里的每一项是否“稳定”。在 React Native 鸿蒙开发里,我见过最经典的缓存失效问题是这样的:
tsx复制const searchParams = { category: selectedCategory, city: currentCity };
const result = useMemo(() => filterList(apiData, searchParams), [searchParams, apiData]);
searchParams 是每次 render 都新建的对象,虽然值相同,但引用不同,useMemo 的依赖比较会认为它变了,于是每次渲染都重新过滤。解决方案是用基础值做依赖:
tsx复制const result = useMemo(() => {
const searchParams = { category: selectedCategory, city: currentCity };
return filterList(apiData, searchParams);
}, [apiData, selectedCategory, currentCity]);
或者用 useMemo 先把 searchParams 稳定住:
tsx复制const searchParams = useMemo(
() => ({ category: selectedCategory, city: currentCity }),
[selectedCategory, currentCity]
);
第二种方式在项目里更常见,因为 searchParams 可能还会传给子组件或请求函数,一处稳定,处处受益。
3. 实操:搜索页接入 useMemo 和结果缓存的完整改法
接下来直接把我的搜索页改造过程拆给你看。这个过程不是“加一行 useMemo”就完事,而是围绕“搜索词、数据源、请求结果、过期策略”做一套组合拳。
3.1 一个最小可运行的搜索缓存示例
这里我先给一个同时包含“计算缓存”和“请求结果缓存”的示例。第一段代码是组件核心逻辑:
tsx复制import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { TextInput, View, FlatList, Text, ActivityIndicator } from 'react-native';
interface SearchResult {
id: string;
title: string;
}
const TTL = 5 * 60 * 1000; // 5分钟
const CACHE_LIMIT = 50;
class ResultCache {
private store = new Map<string, { data: SearchResult[]; expireAt: number }>();
get(key: string): SearchResult[] | undefined {
const item = this.store.get(key);
if (!item) return undefined;
if (Date.now() > item.expireAt) {
this.store.delete(key);
return undefined;
}
return item.data;
}
set(key: string, data: SearchResult[]) {
if (this.store.size >= CACHE_LIMIT) {
const firstKey = this.store.keys().next().value;
if (firstKey) this.store.delete(firstKey);
}
this.store.set(key, { data, expireAt: Date.now() + TTL });
}
clear() {
this.store.clear();
}
}
const SearchScreen = () => {
const [keyword, setKeyword] = useState('');
const [allResults, setAllResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [visibleList, setVisibleList] = useState<SearchResult[]>([]);
// 用 useMemo 保证缓存容器在组件生命周期内不会重建
const resultCache = useMemo(() => new ResultCache(), []);
// 同步过滤:如果输入框内容变化,就重新过滤本地数据
const filteredList = useMemo(() => {
const kw = keyword.trim().toLowerCase();
if (!kw) return allResults;
return allResults.filter(item => item.title.toLowerCase().includes(kw));
}, [keyword, allResults]);
// 模拟网络请求,优先走缓存
const fetchSearchResult = useCallback(
async (query: string) => {
const cached = resultCache.get(query);
if (cached) {
setVisibleList(cached);
return;
}
setLoading(true);
try {
const fakeData = await new Promise<SearchResult[]>((resolve) => {
setTimeout(() => {
resolve(
Array.from({ length: 50 }, (_, i) => ({
id: `${query}-${i}`,
title: `${query} 的结果 ${i + 1}`,
}))
);
}, 800);
});
resultCache.set(query, fakeData);
setVisibleList(fakeData);
} finally {
setLoading(false);
}
},
[resultCache]
);
useEffect(() => {
const trim = keyword.trim();
if (!trim) {
setVisibleList([]);
return;
}
const timer = setTimeout(() => {
fetchSearchResult(trim);
}, 300);
return () => clearTimeout(timer);
}, [keyword, fetchSearchResult]);
// 输入变化时,先展示本地过滤结果,带来即时反馈
useEffect(() => {
setVisibleList(filteredList);
}, [filteredList]);
return (
<View style={{ flex: 1 }}>
<TextInput
value={keyword}
onChangeText={setKeyword}
placeholder="输入搜索关键词"
style={{ height: 44, borderWidth: 1, margin: 12, paddingHorizontal: 8 }}
/>
{loading ? <ActivityIndicator style={{ marginTop: 20 }} /> : null}
<FlatList
data={visibleList}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text style={{ padding: 12 }}>{item.title}</Text>}
/>
</View>
);
};
export default SearchScreen;
在这里,useMemo 承担了两个职责:一个是缓存 filteredList 这个同步计算结果,另一个是缓存 resultCache 这个实例。第二个职责往往被忽略,但它才是“网络请求结果缓存”的基石,没有它,缓存容器每次渲染都会丢。
3.2 优化点:用 useDeferredValue 让输入框不卡输入
上面这个例子已经比“每次按键全量过滤 + 每次按键发请求”好了很多,但还有一个小问题:当输入很快时,keyword 同时驱动了“过滤本地数据”和“发起网络请求”,这两件事会抢占 JS 线程。在鸿蒙上,这可能导致输入框先短暂卡住,然后再统一更新。
React 18 开始(React Native 0.70+,RN for OpenHarmony 也跟随了这个版本基线),可以用 useDeferredValue 把低优先级的派生更新往后挪:
tsx复制const deferredKeyword = useDeferredValue(keyword);
const filteredList = useMemo(() => {
const kw = deferredKeyword.trim().toLowerCase();
if (!kw) return allResults;
return allResults.filter(item => item.title.toLowerCase().includes(kw));
}, [deferredKeyword, allResults]);
这样输入框会立刻响应用户输入,而过滤计算和请求在后台“慢半拍”执行。如果你在鸿蒙真机上测试,会发现第一个字符显示更快的感知很明显。需要注意,useDeferredValue 不是所有版本都支持,建议在 RN 鸿蒙项目里先查看当前的 React 版本,再决定是否引入。
3.3 命中缓存的关键:稳定 key 与防抖配合
搜索结果缓存的 key 决定命中率。我建议直接用“搜索词”作为 key,但要注意两个点:
- 搜索词需要标准化:去首尾空格、统一大小写,否则“React Native”和“react native”会存成两个 key,白白浪费缓存空间。
- 搜索词的组合顺序也要固定,尤其当你有筛选条件时,建议用
JSON.stringify({ keyword, category, city })作为 key,保证条件是同一组时才能命中。
防抖和缓存并不冲突。防抖解决的是“每敲一个字就发一个请求”的抖动问题,缓存解决的是“相同请求重复发”的问题。两者结合后的流程是:输入防抖 300ms 后,先查缓存,命中则直接渲染,未命中才发请求。我在多个鸿蒙设备上压测过,这个流程可以把重复请求率降低至少 40%,并且用户感知的搜索速度会明显提升,因为第二次搜索同一个词时,接口调用根本不会发生。
4. 缓存失效策略:TTL、LRU 和鸿蒙内存治理
搜索结果缓存不是存得越久越好。在鸿蒙上,内存资源和系统对进程的限制比想象中更严格,一个失控的缓存 Map 很容易让 App 在后台被杀,甚至在前台直接 OOM。所以缓存一定要有过期和淘汰机制。
4.1 为什么必须有失效策略
搜索结果本质上是有时效性的数据。新闻、商品、库存、价格,任何一个维度变化,旧结果都可能出错。如果用户搜“春节机票”之后一周再搜同一个词,还看到上一周的缓存结果,那功能就是有问题的。
另外,内存缓存还有一个数学问题:如果不限制条目数,缓存会无限增长。假设一条结果存 2KB,用户搜 1000 个关键词就是 2MB,看起来不多,但搜索结果往往包含图片 URL、摘要、状态字段,膨胀速度远超预期。在低端鸿蒙设备上,JS 堆内存本身有限,一个无上限的缓存就可能成为压垮进程的最后一根稻草。
4.2 TTL、LRU 和基于数量的清理
上面代码里 ResultCache 已经做了两件事:TTL 过期读时删除,以及数量超过 CACHE_LIMIT 时删除最老的 key。这是一个非常简单的 LRU 近似实现。如果你需要更精确的最近最少使用策略,可以给每条缓存加 lastAccessAt,每次 get 时更新时间,清理时按 lastAccessAt 排序删除。
但如果你的搜索场景比较复杂,比如多个页面共享同一个搜索缓存,最好直接用一个 useMemo 放在全局 Store 或 Context 里,而不是每个页面各自 new 一个 ResultCache。否则页面 A 查过的结果,页面 B 查不到,缓存的命中率会低很多。
下面给一个带 lastAccessAt 的升级版:
tsx复制class LRUCache {
private store = new Map<string, { data: unknown; expireAt: number; lastAccessAt: number }>();
private limit: number;
constructor(limit: number) {
this.limit = limit;
}
get(key: string) {
const item = this.store.get(key);
if (!item) return undefined;
if (Date.now() > item.expireAt) {
this.store.delete(key);
return undefined;
}
item.lastAccessAt = Date.now();
this.store.delete(key);
this.store.set(key, item);
return item.data;
}
set(key: string, data: unknown, ttl: number) {
if (this.store.has(key)) {
this.store.delete(key);
}
this.store.set(key, {
data,
expireAt: Date.now() + ttl,
lastAccessAt: Date.now(),
});
if (this.store.size > this.limit) {
const oldestKey = this.store.keys().next().value;
if (oldestKey) this.store.delete(oldestKey);
}
}
clear() {
this.store.clear();
}
}
这里用“读时删除”和“插入时淘汰”的组合策略,既能控制内存,又能保证最热的数据留在缓存里。
4.3 失效时机:搜索词变化、数据源变化、手动刷新
缓存失效不仅仅靠 TTL,还要在合适的业务时机主动清除:
- 用户切换城市/分类/账号:这些条件如果拼进了缓存 key,就不需要全部清;但如果你的 key 里没有这些条件,就必须统一清掉,否则串数据。
- 客户端收到“内容更新”推送或版本升级:应用启动后应该检查本地缓存版本号,发现不匹配就清空。
- 用户下拉刷新:只刷新当前搜索词的缓存即可,不清其它词条。
- 退出登录:涉及账号维度的缓存必须全部清空,否则下一个账号会看到上一个账号的搜索历史。
在鸿蒙开发里,你还可以监听 App 进入后台的事件,在后台时做一次缓存清理,避免进程被系统回收前仍然占据大量内存。这个细节对应用体验很有帮助,尤其是鸿蒙的多任务卡片界面下,用户很可能切出去再切回来,内存状态变化剧烈。
5. 在鸿蒙真机上压测后的几个坑:白屏、日志和缓存命中
代码层面做完了,不等于真机就稳了。这里我踩过几个和“缓存 + 鸿蒙”相关的坑,分享出来帮你避雷。
5.1 启动白屏:缓存读得太同步
热搜词里有“react native 启动白屏”,我把这个和缓存放在一起说。鸿蒙上 RN 启动白屏的常见原因有几个:Bundle 加载慢、首帧渲染时机晚、初始化流程里有大量同步任务。其中有一条和缓存直接相关:如果你在 App 启动时用 AsyncStorage.getItem 同步读取历史搜索缓存(老版本 AsyncStorage 的同步接口,或者自己封装成同步桥接),会直接卡住首屏渲染。
正确的做法是:
- 启动时只读取“非用不可”的配置,搜索缓存等首帧渲染完成后再异步加载。
- 如果缓存数据量大,可以用
InteractionManager.runAfterInteractions,等用户看到首屏后再计算和写入。 - 请求结果缓存优先用内存缓存,持久化缓存放到用户实际触发搜索时再读。
在鸿蒙上,首帧是用户对 App 的第一印象,任何阻塞它的操作都应该被优化掉。
5.2 真机调试时缓存“未命中”的误会
我遇到过非常诡异的现象:在鸿蒙模拟器上,同一个关键词第二次搜索时明明命中了缓存,日志也打出来了;到了真机,同一个词每次都要重新请求。排查之后发现,不是 useMemo 的问题,而是真机上打开了 React Native 的“开发模式”和“Fast Refresh”,每次修改代码后组件重新挂载,useMemo 缓存自然会被清空。
要确认缓存是否真正失效,建议在发布模式(Release)下测试。另外,开发模式下如果依赖了 __DEV__ 等全局变量,也会影响 useMemo 依赖的比较结果。真机调试看到缓存未命中,先想“组件是否重新挂载了”,而不是急着怀疑 useMemo。
5.3 实测数据:缓存前后的渲染耗时对比
我在一款使用 React Native for OpenHarmony 开发的测试 App 上,用 2000 条本地数据做搜索过滤压测。真机是一台 8GB 内存的中端鸿蒙设备,测试结果如下:
| 场景 | 操作 | 平均 JS 耗时 | 列表帧率 |
|---|---|---|---|
| 无缓存 | 输入一个关键字,全量过滤 | 约 25ms | 30-45fps,偶尔掉帧 |
useMemo 计算缓存 |
输入相同关键字,二次触发 | 约 0.5ms | 稳定 55fps+ |
useMemo + memo 子组件 |
输入相同关键字,二次触发 | 约 0.3ms | 稳定 60fps |
接口请求部分的对比更加明显:没有请求缓存时,同一个词快速重搜,可能同时发出 3 个相同的请求;加了 resultCache 后,第二次搜索直接命中缓存,请求时间为 0ms。这些都是肉眼可感知的提升。
5.4 另一个坑:FlatList 的 extraData 和缓存结果联动
如果你把 filteredList 作为 FlatList 的 data,但 filteredList 经过 useMemo 后引用变了,而 FlatList 还依赖旧的 extraData 去判断是否需要更新,就会导致列表内容更新了但界面没刷新。
建议在鸿蒙 RN 里,列表数据和渲染状态尽量用一个唯一的“刷新标记”串联起来。比如:
tsx复制const refreshToken = useMemo(() => `${filteredList.length}-${Date.now()}`, [filteredList]);
<FlatList
data={filteredList}
extraData={refreshToken}
...
/>
当然,这种做法是为了兼容某些老版本鸿蒙 RN 适配层的 bug。如果你用的版本已经正常,可以不加 Date.now(),否则每次 filteredList 长度没变但内容变了时,列表不会重绘。
6. 后续还可以做什么:请求层缓存、FlatList 优化和鸿蒙特性接入
到这里,你的搜索页已经从“输入卡顿”进化成“二次搜索秒开”了。但还有三件周围的事值得做,会让整体体验再上一个台阶。
6.1 useMemo + useCallback + memo 黄金组合
很多鸿蒙 RN 项目的列表性能问题,不是过滤计算,而是列表项渲染过多。给列表项组件套一层 memo,并让传给它的回调函数用 useCallback 稳定起来,可以大幅减少子组件的重复渲染:
tsx复制const handlePressItem = useCallback((id: string) => {
navigation.navigate('Detail', { id });
}, [navigation]);
const renderItem = useCallback(({ item }) => {
return <SearchListItem data={item} onPress={handlePressItem} />;
}, [handlePressItem]);
注意 renderItem 本身也要稳定,否则 FlatList 每次收到新的 renderItem 函数,还是会重新渲染。这套组合在鸿蒙上的效果比 Android 上更明显,因为鸿蒙原生列表组件对键控更新的依赖更强。
6.2 请求层缓存:AbortController、离线兜底、缓存更新
内存缓存只是起点。更好的做法是在请求层封装一个全局的搜索请求管理器,具备以下能力:
- 同一个 key 的并发请求合并:如果用户快速输入“React Native”,防抖期间触发了 3 次请求,只发一次,其它等待同一个 Promise。
- 过期缓存兜底:当接口请求失败时,如果缓存里还有上一次成功的数据(即使 TTL 已过),也可以先展示数据并打上一个“可能过期”的标记。
- 后台预取:用户在某条结果上停留超过 500ms,可以用空闲时间预取下一条相关搜索词的结果,用
InteractionManager.requestAnimationFrame安排。
这些在 RN 鸿蒙里都能用 JS 实现,不需要写原生代码。关键是不要让每个页面各写一套缓存逻辑,而是抽成一个 useSearchCache Hook,业务方只需要传 key 和请求函数。
6.3 鸿蒙侧的持久化缓存:AsyncStorage 与分布式 KV 的选择
如果你的搜索缓存需要跨启动保留(比如保存最近 10 条搜索历史和结果),可以考虑持久化。RN 鸿蒙里最直接的是 AsyncStorage,但要注意:
- AsyncStorage 的读写是异步的,不要在小流量场景下高频调用。
- 保存大数据结构时建议做压缩,例如只保存
id列表和摘要,详情等点击后再拉取。 - 鸿蒙原生侧还有分布式 KV Store 能力,可以通过自定义 NativeModule 接入,但除非你要做多端同步,否则不建议为了搜索缓存去引原生依赖。
我的经验是:结果缓存尽量留在内存里,持久化只做“用户搜索历史”这类轻量数据。因为结果数据往往有图片和富文本,持久化会占用太多 AsyncStorage 空间,而且清洗起来非常麻烦。
6.4 如果数据源来自远端,分页缓存怎么做
当搜索结果不是一次性返回全部,而是分页时,缓存就不能只存“一个关键词对应一个数组”了,而是要存“关键词 + 页码”对应的数据。此时 useMemo 依然适用,只是缓存 key 需要拼上页码:
tsx复制const pageCacheKey = useMemo(() => `${keyword}-${page}`, [keyword, page]);
const pageList = useMemo(() => {
return pageCache.get(pageCacheKey) ?? [];
}, [pageCacheKey, pageCache]);
注意分页缓存会带来“第一页更新了,第二页还是旧的”的问题。这时候要比对总条数和当前加载到的条数,发现不一致就清掉后几页的缓存,保证用户滑动加载时不会出现断裂。我通常的做法是:每次第一页成功返回,就把该关键词的后续页码缓存全部清空,避免分页错乱。
最后再分享两个小技巧
第一,搜索页的缓存容器不要只放在组件里,如果你有多个入口都能跳转到同一个搜索页,可以把缓存提升到模块级单例。比如 export const globalSearchCache = new LRUCache(50);,这样页面重新挂载也不会丢内存结果,但需要在退出登录时手动 clear()。
第二,在鸿蒙真机上测试性能,建议打开 RN 开发者菜单里的 Performance Monitor,重点看 JS 线程的帧率。不要在只有一条数据时测,一定用能代表线上真实分布的数据量压测。搜索这种场景,用户的数据量差异非常大,你必须在最坏情况下保证不掉帧,才算真正合格。
