做股票行情类 App 绕不开一个场景:一屏列表里几十个品种,每个品种的买价、卖价、涨跌幅都在通过 WebSocket 长连接每秒推好几次,价格一变还要闪一下涨跌色。功能写出来不难,难的是用户停在行情页两分钟后开始抱怨:手机背面发烫、列表滑动一卡一卡、内存监控里数字只涨不回落。
我接手的那个项目最严重时,iPhone 16 Pro Max 的 Release 包在行情页主线程 51%、JS 线程 36% 双线饱和,FPS 在稳态期周期性掉到 0~10,虽然没触发系统热降频,但持续发烫已经成了投诉点。这篇文章把整个排查和优化过程完整复盘,所有代码已脱敏,核心是四刀解法——它们对任何"高频数据 + 长列表"的场景(行情、弹幕、IM、实时大屏)都通用。
在本篇文章中,我们将从浅入深,一起搞定以下内容:
- 高频行情渲染为什么会同时引发发热、掉帧、内存三个问题
- 用真机
trace定位四个"永不停"的CPU黑洞 - 第一刀:按需订阅,只为可视区品种建立
WebSocket订阅 - 第二刀:per-symbol 精准订阅,切断"心跳让全表
rerender" - 第三刀:
FlashList渲染稳定性,让滑动复用链不断 - 第四刀:心跳降频 + 边沿侦测 +
AppState启停,杀掉后台空转 - 给
App内置实时入站帧率、断连原因、GC监控面板 - 用量化阈值表验收"优化到底有没有生效"
# 一、问题现场为什么三个症状一起来
发热、掉帧、内存暴涨看起来是三个问题,根子其实是同一个:CPU 在被持续无意义地烧。
WebSocket 把行情帧推到 JS 线程,每帧都要 JSON.parse、写状态、触发 React 重渲染、再走 Fabric 的 ShadowTree commit 和原生 mount transaction。这条链路里任何一环只要频率失控,就会:
- 发热:
CPU长期高占用,芯片功耗上去了,热量自然来——GC频繁、mount transaction每秒几十次都是元凶。 - 掉帧:主线程被
mount transaction和clipping递归占满,渲染管线挤不进16ms一帧的预算,FPS就塌了。 - 内存暴涨:高频
JSON.parse产生海量临时对象,Hermes堆反复扩张;如果订阅没按需回收,可见区外的几百个品种状态还挂在内存里只涨不回落。
所以优化的总目标只有一句话:让 CPU 只在"真的有价格变化、且这个品种用户真的看得到"的时候才干活,其余时间一律闲着。 四刀解法全是围绕这句话展开的。
# 二、定位四个永不停的 CPU 黑洞
光说"CPU 高"没用,得拿真机 Release 包的 Instruments trace 把热点钉死。交叉审计 trace 关键字和源码后,我定位到四组"持续运行、永不停"的隐性黑洞:
黑洞一:全局心跳定时器永不停。 一个 80ms(12.5Hz)的全局 setInterval,模块加载就起、息屏后台都不停,每次都写一个 SharedValue,触发每个挂载过的行情文本组件的 useDerivedValue worklet 重算"价格新鲜度"。trace 里 reanimated+worklet 关键字 inclusive 累计 207s,是 18.67s 采样的绝对头部。
黑洞二:Fabric mount transaction 每秒 66 次。 每次 commit 都递归遍历整棵子视图树做 clipping(updateClippedSubviewsWithClipRect 主线程占 17.2%,递归 30+ 层)。源头多半是黑洞一频繁写动画样式属性触发的 ShadowTree commit。
黑洞三:把超大业务 store 当订阅源。 行情单元格订阅了一个杂烩 store(订单、持仓、余额、设置几十个字段都在里面),结果任意业务字段写入(下单、切 tab、HTTP 刷新)都把全部可见单元格的 selector 同步跑一遍。trace 里 Hermes 解释器 34s inclusive。
黑洞四:单元格视图层级太深 + 多层圆角裁剪。 Pressable → View → View → ... → Icon 嵌套 8+ 层,每层 rounded-xl 都触发圆角图片绘制和 clipping 递归向更深扫描,把前三条的 CPU 成本进一步放大。
四条同时存在、叠加放大,导致单独优化任何一条都看不出效果,必须一次性收口。下面按 ROI 从高到低逐刀拆解,整体解法和收口效果先看这张全景图:

# 三、第一刀按需订阅只为可视区建立订阅
最大的浪费是:列表里有几百个品种,但用户一屏只看得到十几个,却给全部品种都开了 WebSocket 订阅、全部都在接收推送、全部都在 parse 和写状态。
解法是「可视区订阅」:只为当前屏幕可见的品种 + 上下各 5 个缓冲建立订阅,滑出去的退订。FlashList 的 onViewableItemsChanged 给我们可见区索引,debounce 合并滚动过程中的高频回调:
import type { ViewToken, ListRenderItemInfo } from '@shopify/flash-list'
import { useCallback, useRef } from 'react'
import { setVisibleSymbols, VISIBLE_BUFFER_RADIUS, VISIBLE_DEBOUNCE_MS } from '@/lib/ws'
function useVisibleSubscription(scopeKey: