web内存分析及工具

js 自带 GC(垃圾回收)机制,因此绝大多数 web 开发人员不会在日常开发中考虑内存情况(包括我自己),在多数业务场景中,这可能没有问题,但在一些核心web应用场景下(比如某个页面投放在一级tab下这种 WebView 基本不会销毁的场景,或者像 PhoneGap / Electron 这种以 WebView 渲染为主的应用),会造成一些白屏崩溃这种意想不到的bug,影响用户体验。

*文中大多三方链接需要翻墙访问

内存泄漏的影响

对于用户来说,一般内存泄漏场景根本感知不到,但是一旦内存泄漏比较严重,用户的直观感觉就是页面/电脑开始操作卡顿、直到某一时刻彻底卡死或者页面/应用崩溃闪退,这种情况下会让用户对你的产品丧失信任感。

  • PC 浏览器:浏览器卡顿、甚至崩溃
  • iOS:早期 UIWebView 由于与 app 共享内存空间, 会容易导致app/浏览器卡顿、白屏、甚至崩溃,甚至 input 传个大图/文件如果处理不当就直接崩;之后 WKWebView 虽然优化了 WebView 内存及管理,但仍存在很多bug,加上 iPhone 本身内存空间较小,综合起来还是容易出现卡顿、白屏、甚至崩溃问题;
  • 安卓:相比 iPhone,安卓设备通常内存空间较大,且 WebView 的内存分配更足,这为 WebView 的稳定性提供了一些帮助,但是安卓由于生态混乱,加上引擎等对 js 支持的问题,会导致很多奇葩问题,更容易出现内存泄漏。容易出现卡顿、白屏、甚至崩溃问题;

相比于 PC 端,移动端硬件条件往往较为落后,并且 WebView 环境和内存限制也更为严格。并且移动端由于存在系统限制,不像PC端能方便进行浏览器更新/切换,因此更难以通过环境改善进行WebView管理优化,因此移动端的内存问题会更为严重,需要得到足够的重视。

通常 PC 端通过代码或引导浏览器更新/切换来减少内存问题,PC 应用也可以更新内核来优化环境,总体成本较小。移动端主流的优化手段就是优化内核/内存管理,有能力/有切实需求的往往会自研内核以减少内存问题,像很多安卓应用会基于 QQX5 进行改造,这些成本也往往较大并且还有宿主环境的限制要求。关于内存优化及管理本文就不做具体说明了。

SPA 页面需要更加关注

在 SPA 页面中,内存并不会在每次导航切换时自动清除,相比于 MPA 更容易引发内存泄漏。因此SPA中事件监听、DOM操作、网络请求、定时器等都需要更加关注。


真机内存检测

最通用的检测手段——Chrome DevTool

1.“任务管理器”——查看整体情况

入口为 Chrome 右上角设置 - 更多工具 - 任务管理器Setting - More Tools - Task Manager)。

p-chrome-1

并且我们可以通过右键菜单选择需要展示的字段:

p-chrome-2

其中字段说明:

  • 任务:Task
  • 个人资料:Profile
  • 内存占用空间:Memory Footprint
  • CPU
  • 网络:Network
  • 进程ID:Process ID
  • 图片缓存:Image Cache
  • 脚本缓存:Script Cache
  • CSS缓存:CSS Cache
  • GPU缓存:GPU Memory
  • SQLite使用的内存:SQLite Memory
  • NaCI调试端口:NaCI Debug Port
  • JavaScript 使用的内存:JavaScript Memory
  • 闲置状态唤醒:Idle Wake Ups
  • 文件描述符:File Descriptors
  • 进程优先级:Process Priority
  • 正在使用相应拓展程序的活动数:Keepalive Count

2.Performance——js Heap 查看时间轴上的内存变化情况

Performance 大家会用的相对多些,只要我们勾选了 Memory 便可以增加内存的变化统计。

p-chrome-3

*Recorder,结合用户事件进行记录

Chrome 97开始支持(大约是在2021.10),可以作为 Performance 的 plus 版,增加了用户操作等相关的事件记录,以更好得定位具体操作场景:

p-chrome-7

p-chrome-8

官网使用介绍:《Chrome Developers——Record, replay and measure user flows》

3.Memory ——查看某段/一时刻内存具体快照信息

真要定位内存问题,这是必不可少的工具,它的使用也比较简单。

p-chrome-4

选择模式:

  • Heap snapshot:堆快照,用以打印堆快照,堆快照文件显示页面的 js 对象和相关 DOM 节点之间的内存分配;
  • Allocation instrumentation on timeline: 在时间轴上记录内存信息,随着时间变化记录内存信息;
  • Allocation sampling: 内存信息采样,使用采样的方法记录内存分配。此配置文件类型具有最小的性能开销,可用于长时间运行的操作。它提供了由 js 执行堆栈细分的良好近似值分配。

筛选

选择模式进行快照后,可通过右上选择模块进行筛选:

  • 快照查看方式:默认Summary
    • Summary: 可以显示按构造函数名称分组的对象。使用此视图可以根据按构造函数名称分组的类型深入了解对象(及其内存使用),适用于跟踪 DOM 泄漏。
    • Comparison: 可以显示两个快照之间的不同。使用此视图可以比较两个(或多个)内存快照在某个操作前后的差异。检查已释放内存的变化和参考计数,可以确认是否存在内存泄漏及其原因。
    • Containment: 此视图提供了一种对象结构视图来分析内存使用,由顶级对象作为入口。
    • Statistic:内存使用饼状的统计图。
  • 对象归类的筛选:对Constructor的筛选
  • 对象选择:默认All objects

展示字段

表中展示字段的说明:

  • Contructor:表示使用此构造函数创建的所有对象
  • Distance:显示使用节点最短简单路径时距根节点的距离
  • Shallow Size: 显示通过特定构造函数创建的所有对象浅层大小的总和。浅层大小是指对象自身占用的内存大小(一般来说,数组和字符串的浅层大小比较大)
  • Retained Size: 显示同一组对象中最大的保留大小。某个对象删除后(其依赖项不再可到达)可以释放的内存大小称为保留大小。
  • New:(Comparison 特有)新增项
  • Deleted:(Comparison 特有)删除项
  • Delta:(Comparison 特有)增量
  • Alloc. Size:(Comparison 特有)内存分配大小
  • Freed Size:(Comparison 特有)释放大小
  • Size Delta:(Comparison 特有)内存增量

官网术语解释:《Chrome Developers——Memory terminology》

搜索

ctrl/command + F 唤醒搜索,根据关键字进行筛选

p-chrome-5

4.Performance monitor——简易查看

p-chrome-6

Performance monitor 是实时的,但是没办法看到细节信息。

Chrome排查内存的手段和场景还有很多,如


移动端——Chrome/Safari

相比于PC,移动端真机调试一直是比较麻烦的,特别是内存分析难以像样式调试这种可以借助一些 socket 连接手段(Performance Memory兼容拉跨),所以要查看移动端页面的真实内存使用情况需要设备/环境帮助。

iOS——Safari

有线,需要有一台 iPhone 、Mac 和数据线。

步骤

  • 1.确认手机设置(设置 -> Safari -> 高级 -> Web检查器为打开状态);
  • 2.USB连接真机;
  • 3.确认设备信任;
  • 4.手机和电脑都打开Safari;
  • 5.Safari菜单中开发-XXX 的 iPhone,点击开始调试。

注意如果要看时间线的内存变化,菜单选择为时间线、左侧编程选中内存模块,如:

p-safari-1.jpg

特别提醒,要查看内存情况的话最好只选择内存一个指标模块,有多个指标选择的话容易 Safari 崩溃闪退。

*iOS 也能通过 ios-webkit-debug-proxy 然后使用 Chrome 进行调试,本身机制也是通过创建代理服务器与 Chrome 进行连接,可参考这篇文章:《How to debug remote iOS device using Chrome DevTools》

安卓/鸿蒙——Chrome/Android Studio

与iOS调试比较类似,有线,需要有一台 安卓手机 、Windows/Mac电脑 和数据线。

Chrome官方说明:《Chrome Developers——Remote debug Android devices》

步骤

  • 1.确认手机设置(开发者模式打开 -> USB调试打开状态);
  • 2.USB连接真机;
  • 3.确认设备信任;
  • 4.手机和电脑都打开 Chrome;
  • 5.PC Chrome访问chrome://inspect:确认连接状态
    p-dchrome-1.jpg
  • 6.选择对应页面的’inspect’进行访问
    p-dchrome-2.jpg

Android Studio 的调试模式与 Chrome 类似,也依赖 Chrome,操作可以参考《How to debug Android Chrome from Windows, Linux, or Mac——Install Android Debug Bridge (ADB)》

特别提醒:如果通过'inspect'进行访问时,发现调试控制台始终空白或者404,大概率为控制台涉及的js文件加载失败,大部分js文件需要翻墙访问,这时候需要翻一下。

有没有不用插线的远程方法

有,但是需要设备在一个网段。在开发电脑上建立个 Web 服务器并托管一个站点,然后从 Android 设备访问内容。具体可以查看文档说明:《Chrome Developers——Access local servers》

app内页面

一般通用的方案就是装 debug app,然后可以通过 IDE debug 或者再借助 Safari/Chrome。这种方式的主要问题就是有 debug 包及环境的要求;
要么就是用客户端开发的模拟器进行排查。这种方式的主要问题就是因为是模拟环境,与真实环境有一定区别;
要么就是客户端提供控制台,将内存信息放到控制台中展示,如滴滴的DoraemonKit,但要注意,iOS 现在 App 基本会用 WKWebView,这种情况下客户端是拿不到页面(WebView)的内存信息的(因为系统共享 WebView 虚拟内存),因此像 DoraemonKit 的内存模块也是无法观察页面内存情况,这时候的方案就是获取整个设备的内存信息,通过观察设备内存变化来进行判断,缺点就是难以保证其他应用及系统的影响;

iOS 开发获取内存的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取当前app消耗的内存,注意捕获不了WebView的内存消耗
+ (NSUInteger)useMemoryForApp {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (kernelReturn == KERN_SUCCESS) {
int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
return memoryUsageInByte / 1024 / 1024;
} else {
return -1;
}
}

// 获取整个设备的内存情况
+ (NSUInteger)totalMemoryForDevice {
return [NSProcessInfo processInfo].physicalMemory / 1024 / 1024;
}

*js VM

最后还有一种方式就是利用Chrome和服务器搭建一套js VM 调试生态,如下小程序开发者工具也是这种模式,有兴趣可以看下Chrome DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/

小程序——开发者工具

像微信/支付宝的小程序开发者工具,通常都较好得利用了Chrome 67起支持的js VM内存工具,因此可以非常方便得远程进行真机内存分析。

p-weapp-1.jpg


借助 Puppeteer,我们可以做到内存检测的自动化

快速手段——FuiteJs

github地址:https://github.com/nolanlawson/fuite

Fuite是一个 js 写的 cli 工具,它基于 Puppeteer 分析页面是否存在内存泄漏,对SPA友好

注意:Fuite需要 nodejs v14.14.0及以上的环境,(目前 nodejs 稳定版在16+)

原理机制

Fuite 比较简单,主要通过监控路由跳转来判断是否存在内存泄漏:

  • 1.使用 Puppeteer 打开对于参数的页面;
  • 2.找到页面中所有路由页面并打开;
  • 3.模拟后退(路由返回);
  • 4.重复(默认为7次)以确认是否存在内存泄漏。整个库的主要逻辑处理就是这块。判断依据:
    • Objects:对象(Chrome heap snapshots)
    • Event listeners:事件监听
    • DOM节点
    • Arrays, Maps, Sets、普通Object

如果 Fuite 发现存在泄漏情况,它将在控制台或者 output文件中展示信息。

使用

命令式

安装及测试

1
npx fuite https://blog.michealwayne.cn

使用:

1
fuite [options] <url>

参数:

1
url                        URL to load in the browser and analyze

其中Options:

1
2
3
4
5
6
7
8
9
10
-o, --output <file>        Write JSON output to a file
-i, --iterations <number> Number of iterations (default: 7)
-s, --scenario <scenario> Scenario file to run
-S, --setup <setup> Setup function to run
-H, --heapsnapshot Save heapsnapshot files
-d, --debug Run in debug mode
-p, --progress Show progress spinner (use --no-progress to disable)
-b, --browser-arg <arg> Arg(s) to pass when launching the browser
-V, --version output the version number
-h, --help display help for command

引用式

import { findLeaks } from 'fuite';

const myScenario = {
  async setup(page) { /* ... */ },            // 默认无
  async createTests(page) { /* ... */ },    // 默认拿href
  async iteration(page, data) { /* ... */ }    // 默认为页面后退的往返
};

for await (const result of findLeaks('https://blog.michealwayne.cn', {
  scenario: myScenario,        // scenario参数可选,默认为defaultScenario
})) {
  console.log(result);
}

有意思的是,Fuite 的作者用 Fuite 对10个前端主流框架的主页进行了测试,发现都存在泄漏问题(作者在统计中隐藏了具体名称,有兴趣可以试一下):

p-author-page-1.jpg

延伸

我们可以扩展延伸 Fuite 的功能和使用场景,以更好地服务业务内存检测:

  • 1.服务化:我们可以将 Fuite 放在服务器,通过配置化进行定时检查,以及对应的异常报警等,将内存检测服务化、平台化;
  • 2.集群处理:使用 Fuite 通常耗时会比较久,如果有多个地址需要检测的话建议起多进程集群进行分开检测;
  • 3.改造:Fuite 逻辑是获取页面中所有路由进行检测,我们可以调整筛选控制,并且对 MPA 的处理进行业务优化,以提升检测效率和覆盖率。

最后

并非所有内存泄漏都是需要解决的问题,如 v8 的 JIT 也会导致内存增长,但作为 web 开发者,我们有义务通过工具方法找出业务中所有内存泄漏的场景。


相关链接