在上一节中曾提到过两种性能监控:SYN 和 RUM,那么对应的也有两种分析:数据分析和实验室分析。
数据分析会通过采集上来的性能信息来剖析和定位可能存在的各种问题。
实验室分析会通过某个线上或本地的测试工具对页面进行单点测试,得出性能分析报告。
本文会对前者介绍一些分析实践,后者会介绍一些比较有名的性能测试工具。
数据分析的前端代码已上传至 shin-admin,后端代码上传至 shin-server。
一、数据分析
在将数据采集到后就需要立刻存储,并且按百分位数计算后,需要定期计算和清理。
各类图表的辅助可以更好的定位到发生的性能问题。
1)存储
在将性能数据采集到后,就需要将它们存储到数据库中,例如 MySQL、MongoDB 等。
2023-04-13 如果将数据存储在 MongoDB 中,那么就可以将 JSON 中的属性作为条件来查询,例如查询 measure 对象中的 TTFB 大于 10 的记录。
而 MySQL 中的 JSON 都是字符串序列化后再存储到数据库中的,所以如果要对某个属性做查询,会比较困难。
为了避免拖垮服务器,在服务端接收时会通过队列来异步新增。
以我当前公司的实践为例,将性能数据存储到 web_performance 表中,表结构如下。
CREATE TABLE `web_performance` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `load` int(11) NOT NULL DEFAULT '0' COMMENT '页面加载总时间', `ready` int(11) NOT NULL DEFAULT '0' COMMENT '用户可操作时间', `paint` int(11) NOT NULL DEFAULT '0' COMMENT '白屏时间', `screen` int(11) NOT NULL DEFAULT '0' COMMENT '首屏时间', `network` int(11) NOT NULL DEFAULT '0' COMMENT '网络时间,TTFB + responseDocumentTime', `measure` varchar(1000) COLLATE utf8mb4_bin NOT NULL COMMENT '其它测量参数,用JSON格式保存', `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `day` int(11) NOT NULL COMMENT '格式化的天(冗余字段),用于排序,20210322', `hour` tinyint(2) NOT NULL COMMENT '格式化的小时(冗余字段),用于分组,11', `minute` tinyint(2) DEFAULT NULL COMMENT '格式化的分钟(冗余字段),用于分组,20', `identity` varchar(30) COLLATE utf8mb4_bin NOT NULL COMMENT '身份', `project` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '项目关键字,关联 web_performance_project 表中的key', `ua` varchar(600) COLLATE utf8mb4_bin NOT NULL COMMENT '代理信息', `referer` varchar(300) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '来源地址', `referer_path` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '来源地址中的路径', `timing` text COLLATE utf8mb4_bin COMMENT '浏览器读取到的性能参数,用于排查', `resource` text COLLATE utf8mb4_bin COMMENT '静态资源信息', `os_name` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作系统名称', `os_version` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作系统版本', `app_version` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '客户端版本', `ip` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '客户端IP', `country` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '国家', `province` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '省份', `city` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '城市', `isp` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '网络运营商', PRIMARY KEY (`id`), KEY `idx_project_day` (`project`,`day`), KEY `idx_project_day_hour` (`project`,`day`,`hour`), KEY `idx_ctime` (`ctime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能监控'
referer_path 字段,用于分析指定页面的性能。
2023-05-24 将表中的 referer 字段长度从 200 增加到 300。
2023-05-24 在表中新增 os_name、os_version、app_version 和 ip 四个字段,用于做性能日志的分析。
2023-06-04 在表中新增 network 网络时间字段,其值是 TTFB 和 responseDocumentTime 相加得到的和。
2023-06-07 在表中新增 country、province、city 和 isp 四个字段,存储 IP 解析后的信息,包括国家、省份、城市和网络运营商。
IP 解析可以选择付费服务,得到的结果比较准确,并且还能保持更新。或者选择开源的离线 IP 库,虽然精度不高,但是免费。
表中的 project 字段会关联 web_performance_project 表(结构如下所示)中的 key。
CREATE TABLE `web_performance_project` ( `id` int(11) NOT NULL AUTO_INCREMENT, `key` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '唯一值', `name` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '项目名称', `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:正常 0:删除', PRIMARY KEY (`id`), UNIQUE KEY `name_UNIQUE` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能监控项目';
性能项目就是要监控的页面,与之前不同,性能的监控粒度会更细,因此需要有个后台专门管理这类数据。
key 值是通过名称得到 16 位 MD5 字符串,需要引入 Node.js 的 cryto 库,如下所示。
const crypto = require('crypto');
const key = crypto.createHash('md5').update(name).digest('hex').substring(0, 16);
可以对长期维护的网页创建单独的性能项目,而对于那些临时活动可以共用一个项目。
2)百分位数
均值会受极值的影响,从而让它不够准确,无法真实的反映出用户的性能情况。
故而选择了百分位数来解决极值问题,例如前 95% 用户的首屏时间在 2s 内,
这种写法也叫 TP95,表示 95 分位数,TP 是 Top Percentile 的缩写。
95 分位数是比较高的统计指标,意味着大多数的用户都能享受到更好的性能体验。
为了能看到变化趋势,可以采用图表的方式,例如折线图,如下所示,横坐标可按天或小时。
3)定时任务
每天可以选一个时间(例如凌晨三点),来统计昨天的日志信息。
例如将计算得到的统计信息以 JSON 格式(如下所示)存储到数据库表的一个字段中。
这个字段可以是 TEXT 类型或更大的 MEDIUMTEXT,一天只插入一条记录。
{ hour: { x: [11, 14], load: ["158", "162"], ready: ["157", "162"], paint: ["158", "162"], screen: ["157", "162"] }, minute: { 11: { x: [11, 18, 30], load: ["157", "159", "160"], ready: ["156", "159", "160"], paint: ["157", "159", "160"], screen: ["156", "159", "160"] }, 14: { x: [9, 16, 17, 18], load: ["161", "163", "164", "165"], ready: ["161", "163", "164", "165"], paint: ["161", "163", "164", "165"], screen: ["161", "163", "164", "165"] } } }
还可以选一个时间来做数据清理,因为没有必要一直将这么多的数据保留着。
4)资源瀑布图
通过资源瀑布图可以查看当时的资源加载情况。
在上报性能参数时,将静态资源的耗时通过 getEntriesByType() 方法得到(如下所示),然后一起打包给服务器。
// 静态资源列表 const resources = performance.getEntriesByType("resource"); const newResources: TypeSendResource[] = []; resources && resources.forEach((value: PerformanceResourceTiming): void => { // 过滤 fetch 请求 if (value.initiatorType === "fetch") return; // 只存储 1 分钟内的资源 if (value.startTime > 60000) return; newResources.push({ name: value.name, duration: rounded(value.duration), startTime: rounded(value.startTime) }); }); obj.resource = newResources;
由于我本地业务请求使用的是 XMLHTTPRequest,因此在代码中会过滤掉 fetch 请求,只在上报监控数据时采用了 fetch() 函数。
这可以根据实际情况来处理。在搜集资源时,1 分钟以外的都会舍弃,并且只记录了资源名称、耗时和开始时间。
最终的效果如下图所示,包含一个横向的柱状图,并且在图中会标注白屏、首屏、load 和 DOMContentLoaded 的时间点。
这样能对资源的加载做更直观的比较,便于定位性能问题。
5)堆叠柱状图
先将所有的性能记录统计出来,然后分别统计白屏和首屏 1 秒内的数量、1-2 秒内、2-3 秒内、3-4 秒内、4+秒的数量,白屏的 SQL 如下所示。
SELECT COUNT(*) FROM `web_performance` WHERE `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` <= 1000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 1000 and `paint` <= 2000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 2000 and `paint` <= 3000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 3000 and `paint` <= 4000 and `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00'; SELECT COUNT(*) FROM `web_performance` WHERE `paint` > 4000 `ctime` >= '2022-06-12 00:00' and `ctime` < '2022-06-13 00:00';
算出后,分母为总数,分子为上述五个值,组成一张堆叠柱状图,类似于下面这样,每种颜色代码一个占比。
这样就能直观的看到优化后的性能变化了,可更快的反馈优化结果。
6)阶段时序图
在将统计的参数全部计算出来后,为了能更直观的发现性能瓶颈,设计了一张阶段时序图。
描绘出 TTFB、responseDocumentTime、initDomTreeTime、parseDomTime 和 loadEventTime 所占用的时间,如下所示。
橙色竖线表示白屏时间,黑色竖线表示首屏时间。移动到 id 或来源地址,就会提示各类参数。
2023-05-24 对阶段时序图进行了一次比较大的优化。
在阶段时序图中,增加几个过滤条件,并且点击 id,可以从屏幕右侧边缘滑出浮层面板(采用抽屉式的交互)。
在弹层中,基本信息包括 IP、客户端版本、解析的 UA 信息等。
机型提供了跳转地址,点击后就能自动到搜索引擎页面。
性能数据包括 Web Core Vitals 中的 LCP、FP 等,还有经过计算的性能数据,以及浏览器提供的原始性能参数。
资源瀑布图就是之前那功能,只是搬到了弹层中,就能通过当前日志自动绘制出来,而不必再手动搜索了。
当日性能覆盖会统计处于相同性能参数范围的白屏和首屏数据,例如当前是1-2秒内,则只统计此区间段内的数据。
其中操作系统会加上版本号,例如 Android 13、iOS 15.3.1。客户端会附带 APP 的版本号。区间内操作系统的数量,也会有所统计。
最后会展示行为轨迹,性能日志中有个 identity 字段,可以关联前端监控日志,这样就能了解当前性能是否真的在影响用户的使用体验。
二、实验室分析
成熟的性能优化工具不仅能给出网络、渲染等信息,还能给出各种经过实践的优化建议,让我们的优化工作事半功倍。
1)Chrome DevTools
Chrome 的 DevTools 是一款内置的开发者工具,可用于调试页面、查看网络请求、打印日志等常规功能。
还提供了 Performance 面板,专门用于性能分析,可查看性能参数的时间点、各阶段的耗时、内存变化等。
限于篇幅原因,本文只会重点讲解 Network 和 Performance 两块面板。
在 Network 中会呈现页面中所有的网络请求(可指定类型),并且会给出状态码、协议、请求瀑布图等信息。
蓝线是 DOMContentLoaded(DCL) 事件触发的时间点,红线是 Load 事件触发的时间点。
No throttling 用于模拟网速,模拟 4G、3G 或 Offline 离线等网络,在实际开发中很常用。
Performance 需要点击录制按钮后,才会开始分析,如下图所示,内容还是比较多的。
在 Frames 中可以查看各个阶段的页面截图,Timings 中可以查看到 FP、LCP 等性能参数的时间点。
Main 指的是当前页面,火焰图中描绘了 JavaScript 的性能。
例如 Parse HTML、Evaluate Script(加载 JavaScript)、Compile Script(执行 JavaScript)等。
在 Summary 的环形图中,可以看到火焰图中各种颜色对应的操作,例如:
- 蓝色 Loading 表示加载中,对 HTML、CSS 等资源进行解析工作。
- 黄色 Scripting 表示执行脚本,例如执行函数、触发事件等。
- 紫色 Rendering 表示渲染,包括 HTML 和 CSS 的变化,例如重绘或重排。
- 绿色 Painting 表示绘制,将合成的图层绘制到屏幕中。
Performance 中的 Network 更注重时序和优先级,可在此查看资源加载是否符合预期。
内存视图是一个用不同颜色标注的折线图(如下所示),包括 JavaScript 堆、DOM 节点数量、事件监听器数量等信息。
此处只是蜻蜓点水般介绍了下 Performance 的功能,详细内容还可以去参考官方英语文档。
2)WebPageTest
WebPageTest 是一款线上性能分析工具,通过布置一些特定的场景进行测试,例如不同的网速、浏览器、位置等。
测试完成后,会给出一份性能报告,包括优化等级、性能参数、请求瀑布图、网页幻灯片快照、视频等。
WebPageTest 的原理是将配置参数发送到后台,然后通过浏览器相关的代理程序,启动 Chrome、Firefox 或 IE。
执行完毕后将数据回传给后台,后台再将数据保存起来,最后通过各种形式(统计图、表格等),将分析过的数据呈现给用户。
如果是新手,官方还提供了一份快速入门指南作为参考。多年前曾对 WebPageTest 做过分析,有些内容仍然具有参考价值。
在选择完浏览器和地区后(如下图所示),点击 Start Test 就开始测试了。
性能报告的第一部分是优化建议和各种指标,包括 LCP、FCP、CLS 等,如下所示。
Speed Index 表示速度指数,衡量页面内容填充的速度(越低越好),适合页面优化前后的对比。
在 Requests Details 中,呈现了请求信息,视图包括资源瀑布图、连接时序图、请求耗时表、各条请求的头信息。
在 WebPageTest 的幻灯片视图(Filmstrip View)中,在滑动滚动条时,下面会有根红线对应这个时刻的资源载入情况。
3)Lighthouse
Lighthouse 会对测试的网站进行打分,包括性能、可访问性、最佳实践、SEO 和 PWA 五个部分,并且会提供这五个部分的优化建议。
测量的指标有 6 个,FCP、SI、LCP、CLS、TTI 和 TBT,如下所示。
Lighthouse 的使用方法有多种,第一种是在 Chrome 的 DevTools 中选择 Lighthouse 面板,不过要使用的话,得安装代理。
另一种使用方法是将 Lighthouse 下载到本地,安装后使用命令来执行测试,如下所示。
lighthouse https://www.pwstrick.com --output html --output-path ./report/report.html
Lighthouse 给出一些切实可行的优化建议,如下图所示,在每条建议中,还会给出 Learn More 的链接,了解更多优化细节。
虽然是英文的,但用翻译软件或自己阅读都比较容易理解其含义。
例如修改图像格式、压缩图像、预加载影响 LCP 的图像、延迟加载屏幕外的图像、减少未使用的脚本、剔除阻塞渲染的资源等。
总结
数据分析和实验室分析是性能优化的两块重要组成部分。
数据分析在采集到性能信息后,会先进行日志存储,然后按指定的百分位数进行数据整理,最后还会定期进行删除。
在管理界面提供了几种视图来更好的分析性能瓶颈,包括资源瀑布图、堆叠柱状图和阶段时序图。
在实验室分析中,主要介绍了 3 款性能测试工具,包括 DevTools Performance、WebPageTest 和 Lighthouse。
3 款软件都非常优秀,可以帮助开发人员更快、更准的进行优化工作。