js 自带 GC(垃圾回收)机制,因此绝大多数 web 开发人员不会在日常开发中考虑内存情况(包括本人),在多数业务场景中,这可能没有问题,但在一些核心web应用场景下(比如某个页面投放在一级tab下这种 WebView 基本不会销毁的场景,或者像小程序、PhoneGap / Electron 这种目前以 WebView 渲染为主的应用),会造成一些白屏崩溃这种意想不到的bug,影响用户体验。
(*注:文中大多三方链接需要访问)
对于用户来说,一般内存泄漏场景根本感知不到,但是一旦内存泄漏比较严重,用户的直观感觉就是页面/电脑开始操作卡顿、直到某一时刻彻底卡死或者页面/应用崩溃闪退,这种情况下会让用户对你的产品丧失信任感。
相比于 PC 端,移动端硬件条件往往较为落后,并且 WebView 环境和内存限制也更为严格。并且移动端由于存在系统限制,不像PC端能方便进行浏览器更新/切换,因此更难以通过环境改善进行WebView管理优化,因此移动端的内存问题会更为严重,需要得到足够的重视。
通常 PC 端通过代码或引导浏览器更新/切换来减少内存问题,PC 应用也可以更新内核来优化环境,总体成本较小。移动端主流的优化手段就是优化内核/内存管理,有能力/有切实需求的往往会自研内核以减少内存问题,像很多安卓应用会基于 QQX5 进行改造,这些成本也往往较大并且还有宿主环境的限制要求。关于内存优化及管理本文就不做具体说明了。
在 SPA 页面中,内存并不会在每次导航切换时自动清除,相比于 MPA 更容易引发内存泄漏。因此SPA中事件监听、DOM操作、网络请求、定时器等都需要更加关注。
真机内存检测
“任务管理器”
——查看整体情况入口为 Chrome 右上角设置 - 更多工具 - 任务管理器
(Setting - More Tools - Task Manager
)。
并且我们可以通过右键菜单选择需要展示的字段:
其中字段说明:
Task
Profile
Memory Footprint
Network
Process ID
Image Cache
Script Cache
CSS Cache
GPU Memory
SQLite Memory
NaCI Debug Port
JavaScript Memory
Idle Wake Ups
File Descriptors
Process Priority
Keepalive Count
Performance
——js Heap
查看时间轴上的内存变化情况Performance 大家会用的相对多些,只要我们勾选了 Memory
便可以增加内存的变化统计。
Chrome 97开始支持(大约是在2021.10),可以作为 Performance 的 plus 版,增加了用户操作等相关的事件记录,以更好得定位具体操作场景:
官网使用介绍:《Chrome Developers——Record, replay and measure user flows》
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
唤醒搜索,根据关键字进行筛选
Performance monitor
——简易查看Performance monitor 是实时的,但是没办法看到细节信息。
Chrome排查内存的手段和场景还有很多,如
相比于PC,移动端真机调试一直是比较麻烦的,特别是内存分析难以像样式调试这种可以借助一些 socket 连接手段(Performance Memory兼容拉跨),所以要查看移动端页面的真实内存使用情况需要设备/环境帮助。
有线,需要有一台 iPhone 、Mac 和数据线。
设置
-> Safari
-> 高级
-> Web检查器
为打开状态);开发
-XXX 的 iPhone
,点击开始调试。注意如果要看时间线的内存变化,菜单选择为时间线
、左侧编程选中内存
模块,如:
特别提醒,要查看内存情况的话最好只选择
内存
一个指标模块,有多个指标选择的话容易 Safari 崩溃闪退。
*iOS 也能通过 ios-webkit-debug-proxy 然后使用 Chrome 进行调试,本身机制也是通过创建代理服务器与 Chrome 进行连接,可参考这篇文章:《How to debug remote iOS device using Chrome DevTools》
与iOS调试比较类似,有线,需要有一台 安卓手机 、Windows/Mac电脑 和数据线。
Chrome官方说明:《Chrome Developers——Remote debug Android devices》
1.确认手机设置(开发者模式
打开 -> USB调试
打开状态);
2.USB连接真机;
3.确认设备信任;
4.手机和电脑都打开 Chrome;
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》
一般通用的方案就是装 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;
}
最后还有一种方式就是利用Chrome和服务器搭建一套js VM 调试生态,如下小程序开发者工具也是这种模式,有兴趣可以看下Chrome DevTools Protocol: https://chromedevtools.github.io/devtools-protocol/
像微信/支付宝的小程序开发者工具,通常都较好得利用了Chrome 67起支持的js VM内存工具,因此可以非常方便得远程进行真机内存分析。
另外借助无头浏览器 Puppeteer,我们可以做到内存检测的自动化:
github地址:https://github.com/nolanlawson/fuite
Fuite是一个 js 写的 cli 工具,它基于 Puppeteer 分析页面是否存在内存泄漏,对SPA友好
注意:Fuite需要 nodejs v14.14.0及以上的环境,(目前 nodejs 稳定版在16+)
Fuite 比较简单,主要通过监控路由跳转来判断是否存在内存泄漏:
如果 Fuite 发现存在泄漏情况,它将在控制台或者 output文件中展示信息。
安装及测试
npx fuite https://blog.michealwayne.cn
使用:
fuite [options] <url>
参数:
url URL to load in the browser and analyze
其中Options:
-o, --output Write JSON output to a file
-i, --iterations Number of iterations (default: 7)
-s, --scenario Scenario file to run
-S, --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(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 的功能和使用场景,以更好地服务业务内存检测:
并非所有内存泄漏都是需要解决的问题,如 v8 的 JIT 也会导致内存增长,但作为 web 开发者,我们有义务通过工具方法找出业务中所有内存泄漏的场景。
*本人github:https://github.com/MichealWayne、博客地址:https://blog.michealwayne.cn/