设想你是城市物流调度中心的总调度师。每天早晨,三千辆货车同时抵达配送园区,若让所有车辆一起涌上高架桥,整座桥必定瞬间瘫痪。聪明的调度师不会这么干——他只让桥上当前需要通行的三十辆车缓缓前行,其余车在园区内有序等待。这就是 React Native(RN)性能优化的核心哲学:永远只处理当前视野内需要的东西。RN 应用跑在手机这座“窄桥”上:JavaScript 线程负责逻辑,原生线程负责渲染,两者之间只有一条细细的桥接通道——“JS Bridge“。一旦我们试图一次性渲染千条列表、加载百张原图、频繁跨桥通信,桥就会拥堵,界面就会卡顿。而性能优化,正是为这座桥设计一套聪明的”交通管制方案“:用 FlatList 的虚拟化只渲染看得见的行,用图片缓存把常用图”存放在站点仓库“,用批量通信让货车”拼车过桥”,再加一套监控仪表盘让堵点无处遁形。

岗位关联

学会 RN 性能优化,你就能在面试和工作中像“总调度师”一样,用虚拟化、缓存、内存治理三板斧让 App 丝滑如飞,而不是在用户反馈里连夜救火。

目录
  1. 可视化演示
  2. 列表渲染优化
  3. 图片缓存策略
  4. 通信开销减负
  5. 内存管理治理
  6. 性能监控体系
  7. 持续优化机制
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示FlatList列表虚拟化、FastImage图片缓存、JS-Native Bridge通信优化及内存泄漏排查工具,提升RN应用流畅度,含粒子场背景和发光动画。

本节配有交互动效(进入视口后自动加载,约 3KB)。

列表渲染优化

物流园区里最壮观的景象,是那条长达一公里的智能分拣传送带。几千个包裹同时躺在传送带上,但分拣员只盯着眼前半米区域作业——传送带把远处的包裹藏进仓库,等快到面前才送出。这正是 FlatList 的“虚拟化”机制:它不会把 1000 条数据一次性渲染成 1000 个原生 View,而是维护一个“渲染窗口”,只创建用户当前能看到的那几行组件,离屏的行被回收销毁。

RN 的 FlatList 是长列表的默认武器。它基于 VirtualizedList 实现,核心思想是按需渲染。你需要关注三个参数:initialNumToRender 控制首次渲染的条数(好比传送带启动时先推进来 10 个包裹);windowSize 设置渲染窗口的高度(单位是屏幕高度的倍数,默认 21,即上下各 10 屏);maxToRenderPerBatch 限制每批最多渲染的条数,防止一次渲染太多导致 JS 线程卡死。另外,如果列表项高度固定,务必提供 getItemLayout 属性,它能直接算出每个 Item 的偏移量,让 FlatList 跳过“动态测量”这个耗时步骤,滚动定位变得精确无比。

回想传送带的那一幕:分拣员从不关心仓库深处还剩多少包裹,他只专注眼前的半米。FlatList 也一样——数据再多,只要窗口不扩大,渲染成本就稳如泰山。这就是长列表优化的第一性原理。

渲染成本=N窗口内的行数×单行渲染耗时\text{渲染成本} = N_{\text{窗口内的行数}} \times \text{单行渲染耗时}
虚拟化的威力在于:无论列表总长 N 是 100 还是 10000,窗口内行数几乎不变,所以渲染成本恒定。
  1. 窗口内行数表示当前屏幕内(含上下缓冲区)实际渲染的行数,通常等于 Math.ceil(屏幕高度 / 行高) + 2 × 缓冲区行数。
    N窗口内的行数=H窗口hˉ行\displaystyle N_{\text{窗口内的行数}} = \frac{H_{\text{窗口}}}{\bar{h}_{\text{行}}}
  2. 单行渲染耗时每渲染一条列表项所需的时间,包括 JS 创建组件、diff、原生布局三步。
    t单行=tJS+tdiff+tlayout\displaystyle t_{\text{单行}} = t_{\text{JS}} + t_{\text{diff}} + t_{\text{layout}}
  3. 总成本窗口内行数与单行耗时的乘积,代表每帧渲染的总开销。
    C=N窗口内的行数×t单行\displaystyle C = N_{\text{窗口内的行数}} \times t_{\text{单行}}

图片缓存策略

再回到物流园区。假设每个包裹里都有一张 4K 原图海报,如果每次贴海报都从千里之外的印刷厂现印一张,物流成本不堪设想。聪明的做法是在城市每个站点设立中转货架:第一次从总部取回原图后,就在货架上留一张复印件;下次再要,直接取货,不再出城。图片缓存就是这一套“中转货架”体系。

RN 开发中最常见的图片性能问题是什么?是同一个 URL 被反复请求、大图被无脑加载进内存。解决方案分三层:内存缓存(最靠近 UI,读取最快但容量小)、磁盘缓存(应用沙盒目录,重启后仍生效)、远端资源(网络加载)。在 RN 生态中,react-native-fast-image 是事实标准,它基于原生图片加载库实现三级缓存,并支持 priority(加载优先级)和 resizeMethod(降采样策略)。另外,Image 组件的 cache 属性可以控制 iOS 平台的缓存策略,配合 resizeMethod='scale-down' 让大图先在原生层缩小再进内存,避免一张 4000×3000 的图吃掉 40MB 内存。

中转货架的精髓在于“常用才存,不常用淘汰”。FastImage 的 LRU 缓存会自动淘汰最久未使用的图片,站点货架满了就清掉最不常用的海报,确保仓库永远给最热的货物留位。图片缓存的终极目标:让重复的请求不再发生,让大的图不再原样进内存。

内存占用=宽×高×4 字节\text{内存占用} = \text{宽} \times \text{高} \times 4 \text{ 字节}
一张 1000×1000 的图片在内存中约占 4MB(RGBA),缩小到 100×100 后仅占 0.04MB——降采样让内存占用下降 100 倍。
  1. 像素总量图片的宽度乘以高度,代表像素个数。
    P=Wimg×Himg\displaystyle P = W_{\text{img}} \times H_{\text{img}}
  2. 每像素字节数RGBA 格式每个像素占 4 字节。
    Bper=4\displaystyle B_{\text{per}} = 4
  3. 总占用像素总量乘以单像素字节数,得到内存占用。
    M=P×Bper\displaystyle M = P \times B_{\text{per}}

通信开销减负

物流园区连接着两座城:JS 城和原生城。两城之间的唯一通道是一座收费高架桥,每辆货车(每次 JS 调用原生方法)过桥都要交一笔“过桥费”(序列化、跨线程分发、执行开销)。如果业务代码里每个操作都单独派一辆车过桥——图片缓存读一次、存储读一次、Toast 弹一次——桥很快就堵死了。

优化思路第一条:批量派单。与其一个请求一辆车,不如把同批次的操作打包成一个大 JSON 一次传过去。在 RN 里,这意味着把多次 NativeModules 调用合并成一次批量调用,或使用 InteractionManager 把耗时操作推迟到滚动结束后的空闲期执行。第二条:减少派单本身。能用 JS 算的绝不丢给原生,例如简单的状态计算、字符串处理都留在 JS 线程。第三条:换一座更快的桥。新一代 TurboModule 和 JSI 允许 JS 直接持有原生对象的引用,不再走 JSON 序列化,调用开销从“过桥费”降为“直接握手”。

把货车装满再出发,永远比一趟一趟空车跑省油。编码时请牢记:每写一次 NativeModules.xxx 调用,就问自己——这趟车能捎上别的货吗? 批量、延迟、换高速路,三管齐下,桥就再也不堵了。

N过桥次数=N请求×N小包裹N_{\text{过桥次数}} = N_{\text{请求}} \times N_{\text{小包裹}}
如果不做批量,每次 setState 触发 10 个原生调用,就会产生 10 次过桥;合并为一次批量调用后,过桥次数降为 1,节省 90% 的通信开销。
  1. 请求数业务逻辑发起的高层请求数量,例如一次列表刷新请求。
    N请求=1\displaystyle N_{\text{请求}} = 1
  2. 小包裹数每个请求内部拆分出的原生方法调用次数。
    N小包裹=10\displaystyle N_{\text{小包裹}} = 10
  3. 总过桥次数两者相乘,代表通信总开销。
    N桥=N请求×N小包裹\displaystyle N_{\text{桥}} = N_{\text{请求}} \times N_{\text{小包裹}}

内存管理治理

物流仓库的货架是有限的。如果每天入库的包裹没人领,又不清理,仓库就会爆仓,新到的包裹只能堆在门口——这就是 App 的“内存泄漏”。RN 应用跑在 JS 引擎上,内存泄漏的根源往往是该释放的引用没释放。最常见的四个泄漏源:未清除的定时器、未移除的事件监听器、未销毁的动画实例、以及全局变量持有的大对象。

排查泄漏不能靠肉眼,要用工具“顺藤摸瓜”。LeakCanary 是 Android 原生层的内存泄漏检测利器,RN 项目中可以通过 LeakCanary 检测 Activity 泄漏,而 JS 层内存使用情况则用 react-native-performance 或 Hermes 引擎的采样器分析。内存治理三板斧:第一,useEffect 的 return 里清除定时器、移除监听器——这是 React Hooks 的黄金法则;第二,AppState、DeviceEventEmitter 等全局监听器在组件卸载时显式 remove();第三,大图、大数组用完及时置空,别让全局变量兜底持有。

仓库管理的铁律是“入库必有出库记录”。给每个定时器、监听器建一张“借出记录”,组件卸载就是还货日,逾期不还就是泄漏。持之以恒地做内存治理,App 才能像干净整洁的仓库一样运转流畅多年。

堆占用=∑i=1n存活对象i×对象大小i\text{堆占用} = \sum_{i=1}^{n} \text{存活对象}_i \times \text{对象大小}_i
对象越多、越大,堆占用越高。泄漏的本质是 存活对象 里混入了“本该死去”的引用,导致它们永远无法被 GC 回收。
  1. 存活对象当前仍被引用、无法被垃圾回收的对象集合。
    Objectalive={o∣reachable(o)}\displaystyle \text{Object}_{alive} = \{ o \mid \text{reachable}(o) \}
  2. 对象大小每个对象在堆中占用的字节数。
    ∣o∣=shallow(o)+∑c∈refs∣c∣\displaystyle |o| = \text{shallow}(o) + \sum_{c \in refs} |c|
  3. 总计所有存活对象大小的累加,即堆总占用。
    Heap=∑o∈Objectalive∣o∣\displaystyle \text{Heap} = \sum_{o \in \text{Object}_{alive}} |o|

性能监控体系

没有仪表盘的调度中心是盲人开车。RN 应用的性能监控,就是为这座物流园区装上一块实时仪表盘,让调度师看清:JS 线程每分钟处理多少任务?UI 线程每帧渲染是否超过 16.7 毫秒?哪些页面的“道路拥堵指数”(Jank)最高?

RN 性能监控有两大支柱。第一,帧率指标:FPS(每秒渲染帧数)和 Jank(卡顿次数)是最直观的健康度指标。在开发环境用 react-native-performance-monitor 插件或 PerformanceMonitor 组件,在屏幕上实时显示当前 FPS;Android 上还可以用 systrace 分析 UI 线程的每一帧耗时。第二,Flipper:这是 RN 官方推荐的调试神器,连接真机后可以查看 JS 堆内存、原生内存、网络请求瀑布图,还能实时录制 JS 线程和原生线程的 CPU 耗时。当页面滚动掉帧时,打开 Flipper 的 Performance 面板,就能看到是 JS 线程的 executeJavaScript 占满了时间,还是原生线程的 View 绘制过度。

就像调度中心的仪表盘能提前预警拥堵路段,Flipper 和 FPS 监控让性能问题在用户投诉之前就暴露出来。把监控做在优化之前——先画好仪表盘,再动手修路,才是工程化的正确顺序。

FPS=1000ms帧耗时FPS = \frac{1000 \text{ms}}{\text{帧耗时}}
一帧的标准预算是 16.7ms。帧耗时超出越多,FPS 越低,用户感知的卡顿越明显。
  1. 帧预算每帧渲染允许的最大时间,即 1000ms ÷ 60。
    B=100060≈16.7ms\displaystyle B = \frac{1000}{60} \approx 16.7 \text{ms}
  2. 实测帧耗时单帧内 JS 执行、原生布局、绘制三者的时间总和。
    T=TJS+Tlayout+Tdraw\displaystyle T = T_{\text{JS}} + T_{\text{layout}} + T_{\text{draw}}
  3. FPS用 1000ms 除以实测帧耗时,得到每秒可渲染的帧数。
    FPS=1000T\displaystyle FPS = \frac{1000}{T}

持续优化机制

物流调度中心每年都会总结一套《运营手册》:哪条线路高峰期易堵、哪个站点爆仓风险高、哪种货物需要专车。这份手册不是一次性的,而是每次运营后更新完善。RN 性能优化同样需要这样的“持续机制”,而不是上线前熬夜救火。

持续优化的四步循环:第一,定预算。给核心页面设性能预算,如“首页列表首屏渲染 ≤ 500ms、滚动 FPS ≥ 55”,超预算即视为回归。第二,上 CI 门禁。把性能预算写进 CI 流水线,每次提交代码自动跑一遍轻量性能用例,超标的提交直接被拦截,不让问题流到用户手里。第三,建走查清单。每个迭代版本发版前,按清单逐项检查:列表是否用 FlatList?图片是否走缓存?定时器是否清理?内存是否有增长?把清单变成团队的习惯动作。第四,灰度对比。新功能上线走灰度,用监控平台对比灰度组与对照组的核心指标,用数据说话,而不是“我感觉不卡”。

手册不更新就是废纸,机制不执行就是空谈。性能优化没有终点站,只有持续运转的循环——预算、门禁、清单、复盘,一圈一圈跑下去,App 才能始终保持在“丝滑区间”,让用户永远感受不到桥的存在。

ΔP=P新版本−P基线\Delta P = P_{\text{新版本}} - P_{\text{基线}}
性能回归 = 新版本指标 − 基线指标。当 ΔP 为正(如 FPS 下降、Jank 上升)时,CI 门禁直接红灯。
  1. 基线指标上一个稳定版本记录的性能数据,如 FPS = 55。
    P基线=55\displaystyle P_{\text{基线}} = 55
  2. 新版本指标当前待发布版本的实际性能数据。
    P新=48\displaystyle P_{\text{新}} = 48
  3. 差值两者相减,衡量性能是否退化。
    Δ=P新−P基线=48−55=−7\displaystyle \Delta = P_{\text{新}} - P_{\text{基线}} = 48 - 55 = -7

AI 应用场景

场景1:资讯类 App 信息流优化

阶段:数据处理与渲染优化

解决的问题:传统做法是 ScrollView + map 渲染几十条新闻卡片,用户滑动时频繁卡顿、内存飙升。AI 辅助分析用户阅读行为后,用 FlatList 虚拟化 + 图片三级缓存重写列表,只渲染可视窗口内的卡片,图片按屏幕尺寸预压缩。

具体案例:某新闻客户端首页 Feeds 流从 800 条数据降为同时渲染 20 条,FPS 从 32 提升至 58,内存峰值下降约 40%。AI 还能预测用户即将滑到的位置,触发 getItemLayout 提前布局,让滚动如丝般顺滑。

场景2:电商 App 商品图加载优化

阶段:缓存策略与资源调度

解决的问题:商品瀑布流的图片动辄 1080×1080,同一商品在不同页面反复加载导致带宽浪费和内存压力。AI 根据商品热度预测计算图片的访问频率,为高频图分配内存缓存、为低频图仅走磁盘缓存。

具体案例:某跨境电商 App 接入 fast-image 并引入 AI 热度模型后,图片平均加载耗时从 1.8s 降至 0.4s,重复下载率下降 70%,秒开率显著提升。

场景3:金融 App 性能监控告警

阶段:异常检测与持续优化

解决的问题:人工盯 FPS 报表效率低,问题发现滞后。AI 对 Flipper 上报的 JS 堆内存、FPS、Jank 序列做时序异常检测,自动识别性能拐点并在 CI 门禁中拦截回归。

具体案例:某银行 App 部署性能监控 AI 模型后,能提前 3 天预警 70% 的内存泄漏隐患,版本回退率下降 50%。

Python 演示

python74 行
# -*- coding: utf-8 -*-"""模拟 RN FlatList 虚拟化 + LRU 图片缓存"""from collections import OrderedDictimport random class VirtualizedList:    """虚拟化列表: 只渲染窗口内的条目"""    def __init__(self, total, view_items, buffer_items):        self.total = total        self.view_items = view_items        self.buffer_items = buffer_items        self.window = view_items + 2 * buffer_items     def visible_range(self, offset):        """根据滚动偏移量计算当前窗口内可见条目的索引范围"""        start = int(offset) - self.buffer_items        start = max(start, 0)        end = int(offset) + self.view_items + self.buffer_items        end = min(end, self.total)        return (start, end) class LRUImageCache:    """LRU 图片缓存: 内存中只保留最热门的图片"""    def __init__(self, capacity):        self.capacity = capacity        self._data = OrderedDict()     def _hash_key(self, width, height):        return (width, height, width * height)     def get(self, url):        if url not in self._data:            return None        self._data.move_to_end(url)        return self._data[url]     def put(self, url, img_bytes):        if url in self._data:            self._data.move_to_end(url)        elif len(self._data) >= self.capacity:            self._data.popitem(last=False)        self._data[url] = img_bytes def simulate_scroll(item_count=500, steps=300):    """模拟滚动过程, 统计平均渲染条目数"""    total_render = 0    for i in range(steps):        offset = (i / steps) * (item_count - 1)        start, end = virtual_list.visible_range(offset)        total_render += (end - start)    return total_render / steps def simulate_cache(total_requests=1000, capacity=50):    """模拟图片请求, 统计缓存命中率"""    img_pool = [f"https://img.example.com/{random.randint(1, 200)}.png" for _ in range(200)]    hit = 0    for _ in range(total_requests):        url = random.choice(img_pool)        if cache.get(url) is not None:            hit += 1        else:            cache.put(url, b"fake-image-data")    return hit / total_requests if __name__ == "__main__":    virtual_list = VirtualizedList(total=500, view_items=10, buffer_items=5)    cache = LRUImageCache(capacity=50)     avg_render = simulate_scroll()    print(f"[虚拟化] 500 条数据, 平均每帧仅渲染 {avg_render:.2f} 条")    print(f"[虚拟化] 若全部渲染, 每帧需渲染 500 条, 性能差距 {500 / avg_render:.1f} 倍")     hit_rate = simulate_cache(total_requests=2000, capacity=100)    print(f"[图片缓存] LRU 容量=100, 命中率 = {hit_rate * 100:.1f}%")
目的:一键展示 FlatList 虚拟化大幅减少渲染量、LRU 图片缓存显著提升命中率这两大优化手段的量化收益。
  1. visible_range(offset)根据滚动偏移量计算当前窗口(含缓冲区)的渲染区间,这是 FlatList 虚拟化的核心算法。
  2. view_items + 2 * buffer_items窗口大小 = 可视区 + 上下缓冲区,缓冲区用于提前渲染待滚动进入的条目。
  3. move_to_end(url)LRU 缓存把刚访问的图片移到队尾,表示“最近最活跃”。
  4. popitem(last=False)容量超限时淘汰队首(最久未使用)的图片,保证内存上限恒定。
  5. simulate_scroll()模拟从顶部滑到底部的完整过程,返回平均每帧渲染条数。
  6. simulate_cache()模拟 2000 次图片请求,统计缓存命中率,体现 LRU 策略的价值。

常见误区

误区“既然 FlatList 会回收屏幕外的行,那把 windowSize 调到 50,让所有数据都提前渲染好,滚动起来岂不是更顺滑?”

正解恰恰相反。windowSize 决定的是“渲染窗口”的高度(以屏幕高度为单位)。设置太大等于让大量离屏 Item 常驻内存和原生 View 层级,内存上涨、首屏变慢。就像传送带把 3000 个包裹一次性堆在分拣员身边,反而让人无从下脚。推荐默认的 21 或根据列表项高度微调,让窗口刚好覆盖“当前屏 + 缓冲若干屏”即可。

为何常见:初学者容易把“提前渲染”等同“性能好”,忽略了渲染本身有成本——离屏渲染并不免费,内存和 CPU 都在无声消耗。

误区“图片缓存就是把所有图片都存在内存里,肯定会撑爆内存,不如每次现加载现用。”

正解完整的三级缓存是“内存 — 磁盘 — 远端”,只有最热门的图片才留在内存,绝大部分缓存放磁盘。fast-image 的 LRU 策略会自动淘汰冷数据,加上 resizeMethod='scale-down' 让图先缩小再入内存,实际占用反而远小于“每次重新加载原图”的瞬时峰值。中转货架存的是标准件,不是把所有海报都挂墙上。

为何常见:只听说过“内存缓存”,不知道磁盘缓存的存在,把“缓存”简单等同于“占内存”。

误区“JS 与原生通信开销大,那我干脆少用 setState,甚至把状态都放到原生端管理,通信不就少了吗?”

正解setState 本身不是敌人,频繁的无意义 setState 才是。合理使用 setState + React 的 diff 机制,反而比“手动操作原生视图”更高效。真正的优化方向是:合并多个 setState(React 18 的自动批处理)、避免在滚动回调里 setState、用 InteractionManager 延迟非关键更新。拼车过桥的核心是“合并”,而不是“不上桥”。

为何常见:把通信开销的账算错了——少通信的收益,远低于逻辑复杂度上升和维护成本增加的代价。

误区“上线前拿 LeakCanary 扫一遍,没有泄漏报告,内存问题就永远不会发生了。”

正解内存泄漏往往依赖特定用户路径和时间点(如反复进出详情页 30 次后才出现),发版前的一次扫描只能覆盖测试路径。正确的做法是建立持续监控:线上接入堆内存采样,灰度期间对比前后版本的内存基线,配合性能监控面板长期观察。仓库管理不是盘点一次就结束,而是每天都有出入库记录。

为何常见:把内存治理当成“检查”而不是“机制”,忽略了 RN 应用运行时的动态性和用户行为的不可穷尽性。

面试题

Q1. 关于 FlatList 的虚拟化机制,下列说法正确的是?

单选题 · mid

  • A.FlatList 会一次性渲染所有数据,保证滚动完整
  • B.FlatList 只渲染当前窗口附近的 Item,离屏回收← 正确答案
  • C.FlatList 依赖 ScrollView 实现全量渲染
  • D.FlatList 在 Web 端不适用任何长列表场景

答案:B。FlatList 基于 VirtualizedList 实现,维护一个渲染窗口,只创建窗口内(含缓冲区)的 Item,窗口外行被回收。A 错在“一次性渲染”,这正是虚拟化要避免的;C 错在依赖 ScrollView 全量渲染;D 过于绝对,FlatList 在 RN Web 同样适用。

Q2. getItemLayout 属性的作用是什么?

单选题 · mid

  • A.让 FlatList 跳过动态测量,直接计算 Item 偏移量← 正确答案
  • B.让 FlatList 一次性渲染所有 Item 并布局
  • C.为每个 Item 设置唯一的 key 值
  • D.调整 Item 内的图片加载优先级

答案:A。getItemLayout 在 Item 高度固定时提供 (length, offset, index) 计算函数,FlatList 无需异步测量即可精确知道每个 Item 的位置。B 与虚拟化矛盾;C 是 keyExtractor 的职责;D 是图片缓存相关属性。

Q3. 以下哪些做法能有效减少 JS 与原生之间的通信开销?

多选题 · mid

  • A.将多次原生调用合并为一次批量请求← 正确答案
  • B.使用 TurboModule 替代传统 JS Bridge← 正确答案
  • C.在滚动回调中频繁调用原生方法
  • D.用 InteractionManager 延迟非关键任务← 正确答案

答案:ABD。批量调用减少过桥次数,TurboModule 通过 JSI 直接调用降低序列化开销,InteractionManager 把任务推迟到空闲期避免阻塞。C 会在滚动高频期间大量过桥,是典型的性能反模式。

Q4. react-native-fast-image 相比内置 Image 组件的最大优势是?

单选题 · easy

  • A.支持 GIF 动图播放功能
  • B.内置三级缓存且支持降采样← 正确答案
  • C.可以显示 SVG 矢量图
  • D.完全不消耗任何磁盘空间

答案:B。fast-image 基于原生图片库实现内存/磁盘/网络三级缓存,并支持 resizeMethod 降采样避免大图撑爆内存。A、C 不是其核心卖点;D 错误,磁盘缓存必然占用磁盘空间。

Q5. 在 React Hooks 中,正确的定时器清理方式是什么?

单选题 · mid

  • A.组件卸载后浏览器自动回收定时器
  • B.在 useEffect 的 return 中调用 clearInterval← 正确答案
  • C.在 render 函数中清除上一次的定时器
  • D.使用全局变量保存定时器交给 GC 处理

答案:B。useEffect 的清理函数在组件卸载时执行,是清除定时器、移除监听器的标准位置。A 错,JS 引擎不会自动清除未清理的定时器;C、D 都会造成泄漏或多余副作用。

Q6. 关于 RN 内存泄漏的排查,以下说法正确的有?

多选题 · hard

  • A.LeakCanary 可以检测 Android 原生层的 Activity 泄漏← 正确答案
  • B.DeviceEventEmitter 添加的监听器在组件卸载后会自动移除
  • C.全局变量持有大对象会导致 JS 堆无法释放← 正确答案
  • D.Hermes 引擎的采样器可分析 JS 堆内存快照← 正确答案

答案:ACD。LeakCanary 确实检测 Activity 泄漏;全局变量持有对象会使该对象持续可达,无法被 GC;Hermes 提供内存采样能力。B 错误:DeviceEventEmitter 的监听器必须手动 remove,不会自动清理。

Q7. RN 中一帧渲染的推荐时间预算是多少毫秒?

单选题 · easy

  • A.100ms
  • B.50ms
  • C.16.7ms← 正确答案
  • D.5ms

答案:C。每秒 60 帧对应每帧 1000/60 ≈ 16.7ms 的预算。超出此预算即产生掉帧,用户感知为卡顿。这是性能监控 FPS 指标的理论基础。

Q8. 性能优化 CI 门禁中,“性能预算”的含义是?

单选题 · mid

  • A.每个功能允许编写的代码行数上限
  • B.核心指标的超标阈值,如首屏≤500ms← 正确答案
  • C.每名开发每周可提交的代码次数
  • D.发布前必须完成测试用例的百分比

答案:B。性能预算是为可量化指标(如首屏耗时、FPS、内存峰值)设定红线,CI 在每次提交或发版时自动校验是否达标。A、C 是代码管理概念,D 是测试覆盖率概念,均与性能预算无关。

Q9. 关于 Flipper 在 RN 性能监控中的作用,正确的是?

多选题 · hard

  • A.可查看 JS 堆内存与原生内存趋势← 正确答案
  • B.能录制 JS 线程与原生线程的 CPU 耗时← 正确答案
  • C.能直接修改 JS 业务代码并热重载
  • D.可分析网络请求瀑布图定位慢请求← 正确答案

答案:ABD。Flipper 提供内存、CPU、网络请求等丰富的调试面板,是 RN 官方推荐的真机调试工具。C 错:修改代码并热重载是 Metro 的职责,不是 Flipper 的核心能力。

Q10. 下列哪种场景最适合使用 FlatList 替代 ScrollView?

单选题 · easy

  • A.列表只有 3 个固定不动的卡片
  • B.包含 500 条动态数据的商品信息流← 正确答案
  • C.需要嵌套两个同方向的滚动列表
  • D.页面中仅有一个整屏展示的轮播图

答案:B。FlatList 针对长列表优化,数据量大且滚动频繁时优势明显。A、D 数据量小,ScrollView 完全胜任;C 属于嵌套滚动场景,应优先避免设计,而非用 FlatList 硬解。