你的网页Lighthouse跑出了100分,却在用户手里撑不过一个下午。

这不是夸张。实验室里的完美分数,测的是单次加载的干净状态——首屏渲染、交互响应、视觉稳定。它不关心6小时后会发生什么:上百次路由跳转、几千轮轮询请求、React Query缓存里早该清理却残留的数据。

打开网易新闻 查看精彩图片

真实世界的崩溃没有报错。Chrome的渲染进程直接死掉,用户只看到"Aw, Snap!"。这不是异常,是OOM——内存耗尽。

为什么实验室测不到

Lighthouse在隔离环境中跑单次加载,用的是全新浏览器实例。它算不出堆内存的增长曲线,测不出3GB安卓机上的标签页存活概率。OOM不会抛出JavaScript错误,进程直接终止,没有任何堆栈可追踪。

Chrome用户体验报告(CrUX)倒是采集了真实用户数据,但只记录加载体验,没有"4小时后被系统杀进程"这种指标。这个数据根本不存在于任何公开聚合报告中。

盲区就在这里:你能拿着满分上线,实际交付的软件却在全天使用的场景下逐渐劣化、最终崩溃。

OOM到底是什么

每个Chrome标签页运行在独立的渲染进程里,有内存上限。堆内存突破上限,进程就被杀掉。

上限不是固定数字。Chrome动态计算:

16GB内存的桌面机,单标签可用2-4GB;3GB内存的安卓机,Chrome可能主动杀掉后台标签——甚至不是当前活跃的那个。

performance.memory API(仅Chrome,非标准)能暴露部分限制:

totalJSHeapSize:V8已分配的堆内存
usedJSHeapSize:JS对象实际占用
jsHeapSizeLimit:硬上限,堆无法突破

典型桌面Chrome的jsHeapSizeLimit约4GB;3GB安卓设备上通常只有512MB-1GB。当usedJSHeapSize逼近这条线,崩溃随时发生。

长会话应用的三类内存陷阱

1. 路由状态堆积

SPA切换路由时,旧组件的引用如果没被正确释放,DOM节点、事件监听器、定时器全部留在内存里。用户点了100次导航,等于同时挂着100个页面的残骸。

2. 无界缓存

React Query、SWR这类数据获取库默认缓存所有响应。配置不当的话,几小时积累下来的查询结果全留在内存,没人清理。

3. 订阅泄漏

WebSocket、EventSource、RxJS订阅在组件卸载时没取消,连接和回调持续占用内存。轮询定时器同理——setInterval开了没关,每秒都在产生新数据。

生产环境怎么监控

实验室测不到,只能上真机采数据。

定期采样performance.memory,上报usedJSHeapSize的增长趋势。不是看绝对值,是看斜率——每小时增长50MB和增长500MB,完全是两种事故。

结合用户会话时长做分群:用满8小时的用户,内存曲线是不是线性攀升?特定操作后有没有异常跳变?

崩溃本身很难直接捕获,但可以通过心跳机制间接探测:页面定期上报存活状态,服务端发现某用户突然失联,结合设备信息推断OOM概率。

让应用扛住8小时

路由层面:切出页面时强制清理非必要状态,限制历史栈深度。

缓存层面:给所有查询库设TTL和上限,陈旧数据主动驱逐。不要依赖"用户会刷新页面"这种假设。

订阅层面:组件卸载生命周期里取消所有外部连接,用WeakRef管理可选缓存,让垃圾回收有机会介入。

内存预算:根据目标设备的jsHeapSizeLimit倒推安全水位,复杂应用预留50%缓冲。

Lighthouse 100分值得庆祝,但别让它成为唯一的成绩单。真正的高性能,是用户从早用到晚,标签页还活着。