如今网络速度越来越快、机器性能也越来越好,用户对于页面加载要求也随之增高,页面白屏过久、操作卡顿等问题对用户来说是无法忍受的,因此性能分析成了前端开发工作中的家常便饭。
常见的前端性能分析可以从时间和空间两个角度来进行:
要分析和查看这些时间和空间的数据,需要用到性能分析工具。现在大多数前端项目的性能问题都是通过 Chrome DevTools 进行定位和分析的,下面先来介绍 Chrome DevTools 工具。
Chrome DevTools 提供了特别丰富的开发者调试功能,这里主要介绍两个面板:
下面分别来认识这两个面板。
使用 Lighthouse 可以快速了解自己网站在加载过程中存在的一些性能问题,并进行优化解决,它的前身是 Chrome DevTools 面板中的 Audits。
Lighthouse 面板的优势在于自动化、成本低,它会收集网站加载时的一些性能数据(JavaScript/CSS 加载情况、HTTP 请求耗时、页面加载和渲染耗时等),并根据最佳实践来给每一项进行打分,同时针对低分项给出对应的优化方案。
我们先来看一下 Lighthouse 提供了怎样的一些功能。它的主要功能包括三个:
这份报告从 5 个方面来对页面进行分析,包括性能、辅助功能、最佳实践、搜索引擎优化和 PWA。通过这份报告,我们可以快速掌握网站加载的整体情况,并根据报告提供的优化方向进行优化。
想要知道 Lighthouse 是如何做到的,我们需要了解下 Lighthouse 内部的架构和工作原理。
Lighthouse 的组成部分主要包括四个:驱动(Driver)、收集器(Gatherers)、审查器(Audits)和报告(Report)。
Lighthouse 的具体工作过程为:
如果你希望短时间内对你的网站进行较全面的评估,可以使用 Lighthouse 来跑一下分数,确定大致的优化方向。但 Lighthouse 不能用于运行时的性能分析,也无法给到最佳实践以外更多的数据和建议。
而 Performance 面板则可以弥补 Lighthouse 的不足。
Performance 面板同样有个前身,叫 Timeline 面板,它常常在页面运行时使用,比如用户点击操作之后的逻辑执行、页面滚动时的页面渲染情况,等等。
我们先来看看 Performance 提供了什么功能。
Performance 面板功能特别多,一般来说我们需要先对页面运行过程进行录制。录制过程最好打开隐身模式,这样可确保 Chrome 不受扩展插件影响,从而以干净的状态运行。
录制完成后,DevTools 会进行数据处理,然后在 Performance 面板上显示结果。
一般来说,我们需要分析和使用到 Performance 面板上的这些结果。
我们还可以使用 Performance Monitor,来对页面运行过程中的一些性能指标进行监控,比如 CPU 占用率、JS 内存使用大小、内存中挂载的 DOM 节点个数、事件监听数,等等。
通过 Performance 面板,我们可以得到详细的运行时性能数据,包括函数的调用情况(调用耗时、调用堆栈)、各类事件(渲染事件、加载事件、脚本事件等)的顺序与耗时、CPU 占用情况,等等。
我们可根据这些数据定位到具体哪段脚本的执行导致的性能问题,从而得到解决方案。
使用 Performance 面板进行性能分析,具体的分析过程会比较烦琐,同时上手成本也不低,除了基本的页面加载耗时、网络耗时。如果你希望具体定位到哪一块代码的执行有问题,则需要结合前面介绍的 FPS、CPU、火焰图等一点点来进行更详细的分析。
同时,性能分析并不是一蹴而就的工作,往往在我们对前端应用完成性能优化之后,随着新功能的迭代、项目规模的扩大,很可能在网页运行一段时间之后又会变慢、出现卡顿。
那么,为了降低性能分析工作的成本,同时避免新功能的开发、代码重构等导致的性能下降未能及时发现的问题,我们可以考虑将性能分析自动化。
如果想要自动化地进行性能分析,意味着我们需要拿到浏览器的运行性能数据。
一般来说,我们可以通过集成测试、自动化测试等技术手段让项目的代码运行在浏览器中,并根据脚本来运行相应的功能。在此基础上,我们还需要获取网页加载和运行的性能数据。
结合前面提到的性能分析工具,我们可以考虑分别将 Lighthouse 和 Performance 功能进行自动化。
先来看 Lighthouse 自动化。
Lighthouse 自动化很简单,因为它提供了脚本的方式使用。因此,我们可以通过自动化任务跑脚本的方式,使用 Lighthouse 跑分析报告,通过对比以往的数据来进行功能变更、性能优化等场景的性能回归。
使用 Lighthouse 的优势在于开发成本低,只需要按照官方提供的配置来调整、获取自己需要的一些数据,就可以快速接入较全面的 Lighthouse 拥有的性能分析能力。
前面也说过,Lighthouse 是使用驱动(Driver)通过 Chrome DevTools Protocol 获取页面加载过程中的性能数据。其实通过 Chrome DevTools Protocol,我们还可以获取在 Performance 中的一些性能数据。
那么 Chrome DevTools Protocol 到底是什么呢?我们一起来看一下。
Chrome DevTools Protocol允许第三方对基于 Chrome 的网站进行检测、调试、分析等操作。
也就是说,我们可以自行开发工具,通过 Chrome DevTools Protocol 来获取 Chrome 中网站运行的性能数据。
那么,Chrome DevTools Protocol 到底是什么呢?我们可以从 Chrome DevTools 和浏览器内核的通信过程说起。
实际上,我们使用的 Chrome DevTools,也就是 Chrome 中的浏览器调试工具界面,其实是一个 Web 应用。
当我们打开 Chrome DevTools 的时候,浏览器内核 Chromium 本身会作为一个服务端,它会通过 WebSocket 与 Chrome DevTools 进行通信,过程如下:
那么,Chrome DevTools Protocol 这个协议它到底提供了什么内容呢?
Chrome DevTools Protocol 具有与浏览器的许多不同部分(例如页面、Service Worker 和扩展程序)进行交互的 API。该协议把不同的操作划分为了不同的域(domain),每个域负责不同的功能模块,比如DOM、Debugger、Network、Console和Performance等,可以理解为 DevTools 中的不同功能模块。
这里主要介绍几个和性能相关的域。
Performance域:可获取运行时性能指标,包括页面 DOM 节点数量、JavaScript 栈数量、页面布局耗时等。Tracing 域:可获取页面加载的 DevTools 性能跟踪,可以使用Tracing.start和Tracing.stop创建可在 Chrome DevTools 或时间轴查看器中打开的跟踪文件。Runtime域:通过远程评估和镜像对象暴露 JavaScript 的运行时,可以获取 JavaScript 栈的使用情况。Network域:可以分析网络相关的性能。window.performance、window.chrome.loadTimes()等。对于如何使用该协议,其实已有针对这个协议封装出不同语言的库,包括 Node.js、Python、Java 等,可以在awesome-chrome-devtools这个项目中找到。
除了直接使用 Chrome DevTools Protocol 之外,我们还可以使用Puppeteer的CDPSession,它们封装了 Chrome DevTools Protocol 能力,提供了更加便捷的使用方式,比如:
session.send() 方法调用 Chrome DevTools Protocol 协议方法;session.on() 方法订阅 Chrome DevTools Protocol 协议事件。
既然我们可以拿到这些性能数据,那么也可以根据业务的需要对这些数据进行自动化的分析,结合前端工程化在项目上线前进行自动化任务的执行、检测、生成对比报告等,实现自动化性能监控。现在我们已经知道,通过 Chrome DevTools Protocol,可以实现在一个模拟的浏览器环境中,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个审计报告。
上面提到的性能测试过程,属于前端性能监控中的合成监控(Synthetic Monitoring,SYN)方式。合成监控的使用场景不多,一般可能出现在开发和测试的过程中,例如结合流水线跑性能报告、定位性能问题时本地跑的一些简单任务分析等。
该方式的优点显而易见:
由于代码的运行环境是模拟环境,因此很多线上的运行问题难以发现。为了解决这个问题,我们还可以进行真实用户监控(Real User Monitoring,RUM)。
真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能指标。常见的一些性能监控包括加载耗时、DOM 渲染耗时、接口耗时统计等,本系列《前端监控体系》一篇已有所介绍。
真实用户监控往往需要结合业务本身的前后端架构设计来建设,其优点也比较容易理解:
对于新业务上线,应通过性能专项测试提前发现性能问题,避免上线后"救火"。专项测试在业务上线前,对首屏时间、白屏时间、页面加载时长等指标进行测试,确保达标。大致分四步:确定测试方案 → 设定测试标准 → 选择测试环境进行录屏及视频分帧 → 输出测试结果。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 性能 SDK | 线上/线下方案一致 | 业务未上线,内网访问量有限,无法形成大量分布数据 |
| 录制视频 | 可与 QA 基础设施结合 | 需后期计算 |
一般建议采用录制视频方式,可通过手机自带录屏或 adb 自动化录制:
adb shell screenrecord --time-limit 10 /sdcard/perf.mp4 # 录屏 10s
adb shell screenrecord --size 1280x720 /sdcard/perf.mp4 # 指定分辨率注意事项:高分辨率设备可能无法录制(可指定分辨率);模拟器替代不了真机(晃动、多点触摸、性能);录制过程不要旋转手机(会造成画面切断);可用 --verbose 显示调试日志。
分帧计算的标准:
白屏响应时间 = 白屏最后一帧时间 - 点击时的起始帧时间
首屏加载时间 = 内容完全加载那一帧的时间 - 点击时的起始帧时间卡顿标准:连续 5 帧超过 50ms 判定为卡顿,单帧渲染超过 250ms 判定为严重卡顿。
首屏时间标准:因缺乏海量数据无法使用秒开率标准,可退而采用不同网络环境下的性能标准(2G/3G/4G/WiFi 各自设定)。
测试环境指选择合适的网络环境(WIFI、2G/3G/4G、5G),在不同网络下测试以提前发现问题。此时不关注平均时间和秒开率,只要特定网络环境下首屏时间达标即可。
录屏后用 openCV + Python 做视频分帧计算,避免人工计算的不准确和低效:
"""视频分帧"""
import cv2
vc = cv2.VideoCapture('perf.mp4') # 读入视频文件
n = 1
if vc.isOpened():
rval, frame = vc.read()
else:
rval = False
timeF = 10 # 视频帧计数间隔频率
i = 0
while rval:
rval, frame = vc.read()
if (n % timeF == 0): # 每隔 timeF 帧存储
i += 1
cv2.imwrite(f'{i}.jpg', frame)
n = n + 1
vc.release()分帧图片按帧序号命名,结合帧率可将序号换算为当前播放时间(毫秒),首屏时长即首屏结束帧对应的时间,与标准对比即可判断是否达标。
进阶:用图像识别自动提取关键帧——认为图片从不稳定到稳定那一帧是首屏时间点。当两张图片变化值小于 5% 时趋于稳定,以 95 分位值为判定值,内容 95% 不变化则下一帧为视觉稳定时间点。
openCV 可通过 brew(Mac)或 Anaconda(Windows)安装。其他指标(白屏、卡顿)也可用同样四步进行专项测试。