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
)。
并且我们可以通过右键菜单选择需要展示的字段:
其中字段说明:
- 任务:
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
便可以增加内存的变化统计。
*Recorder,结合用户事件进行记录
Chrome 97开始支持(大约是在2021.10),可以作为 Performance 的 plus 版,增加了用户操作等相关的事件记录,以更好得定位具体操作场景:
官网使用介绍:《Chrome Developers——Record, replay and measure user flows》
3.Memory
——查看某段/一时刻内存具体快照信息
真要定位内存问题,这是必不可少的工具,它的使用也比较简单。
选择模式:
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
唤醒搜索,根据关键字进行筛选
4.Performance monitor
——简易查看
Performance monitor 是实时的,但是没办法看到细节信息。
Chrome排查内存的手段和场景还有很多,如
- 检查ArrayBuffer《Chrome Developers——Inspect JavaScript ArrayBuffer with the Memory inspector》
移动端——Chrome/Safari
相比于PC,移动端真机调试一直是比较麻烦的,特别是内存分析难以像样式调试这种可以借助一些 socket 连接手段(Performance Memory兼容拉跨),所以要查看移动端页面的真实内存使用情况需要设备/环境帮助。
iOS——Safari
有线,需要有一台 iPhone 、Mac 和数据线。
步骤
- 1.确认手机设置(
设置
->Safari
->高级
->Web检查器
为打开状态); - 2.USB连接真机;
- 3.确认设备信任;
- 4.手机和电脑都打开Safari;
- 5.Safari菜单中
开发
-XXX 的 iPhone
,点击开始调试。
注意如果要看时间线的内存变化,菜单选择为时间线
、左侧编程选中内存
模块,如:
特别提醒,要查看内存情况的话最好只选择
内存
一个指标模块,有多个指标选择的话容易 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
:确认连接状态
-
6.选择对应页面的’inspect’进行访问
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 开发获取内存的相关代码:
// 获取当前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内存工具,因此可以非常方便得远程进行真机内存分析。
另外借助无头浏览器 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文件中展示信息。
使用
命令式
安装及测试
npx fuite https://blog.michealwayne.cn
使用:
fuite [options] <url>
参数:
url URL to load in the browser and analyze
其中Options:
-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个前端主流框架的主页进行了测试,发现都存在泄漏问题(作者在统计中隐藏了具体名称,有兴趣可以试一下):
延伸
我们可以扩展延伸 Fuite 的功能和使用场景,以更好地服务业务内存检测:
- 1.服务化:我们可以将 Fuite 放在服务器,通过配置化进行定时检查,以及对应的异常报警等,将内存检测服务化、平台化;
- 2.集群处理:使用 Fuite 通常耗时会比较久,如果有多个地址需要检测的话建议起多进程集群进行分开检测;
- 3.改造:Fuite 逻辑是获取页面中所有路由进行检测,我们可以调整筛选控制,并且对 MPA 的处理进行业务优化,以提升检测效率和覆盖率。
最后
并非所有内存泄漏都是需要解决的问题,如 v8 的 JIT 也会导致内存增长,但作为 web 开发者,我们有义务通过工具方法找出业务中所有内存泄漏的场景。
*本人github:https://github.com/MichealWayne、博客地址:https://blog.michealwayne.cn/
相关链接
- https://developer.chrome.com/docs/devtools/open/
- https://nolanlawson.com/2021/12/17/introducing-fuite-a-tool-for-finding-memory-leaks-in-web-apps/
- https://developer.chrome.com/docs/devtools/memory-problems/
- https://github.com/nolanlawson/fuite
- https://developer.chrome.com/blog/new-in-devtools-67/
- https://gilfink.medium.com/help-my-memory-is-leaking-bf5dcaf83fc6
- https://chromedevtools.github.io/
- https://chromedevtools.github.io/devtools-protocol/
- https://github.com/google/ios-webkit-debug-proxy
- https://medium.com/@nikoloza/how-to-debug-remote-ios-device-using-chrome-devtools-f44d697003a7
- https://raygun.com/blog/debug-android-chrome/