教你如何衡量一个网页的性能

网页性能.jpg

写作背景:

拥有一个良好用户体验的网页对于前端开发同学来说是一件必须做,持续做的一件事情,其中首屏的渲染尤为重要,因为用户与网页产生的交互就是从首屏开始的。接下来将介绍网页性能的几个指标以及如何计算和度量这些指标,通过监控和优化这些性能数据来持续优化网页,以提高用户体验。

Navigation Timing API:

我们先来看一组 API,Navigation Timing API 提供了用来衡量一个网站性能的数据指标。相比于我们传统的基础手段使用这组 API 可以获取更有用、更精确的数据还可以做更多的数据统计。
Navigation Timing API 的兼容性也是相当的不错,兼容我们现在常用的浏览器,包括让人讨厌的 IE9+。完整的兼容性表见链接
image.png

资源加载过程:

当我们的一个资源发起访问(navigationStart)后到资源完成加载(loadEventEnd)经历如下过程:
①首先就是当资源需要重定向的时候会进入 redirect阶段,不需要重定向将会跳过这个阶段**;**
②接着就会检查资源是否有HTTP缓存的存在,当资源没有缓存的情况下回进入下一步;
③DNS 寻址的过程;
④TCP 协议握手建立通信;
⑤发起请求;
⑥响应资源;
⑦对响应的内容进行处理,如:对 DOM 资源加载并解析;
⑧最后一步完成资源加载。

one-resource-process.gif
下面这张图将是对这个过程的标准描述,也是一道前端经典面试题的标准答案,你懂得~
image.png


上图是W3C第一版的 Navigation Timing 的处理模型(Level 1),从当前浏览器窗口卸载旧页面到加载完成新页面,整个过程一共分为 9 个阶段:

  1. 提示卸载旧文档
  2. 重定向/卸载
  3. 应用缓存
  4. DNS 解析
  5. TCP 握手
  6. HTTP 请求处理
  7. HTTP 响应处理
  8. DOM 处理
  9. 文档装载完成

Level 1 的规范从2012 年底进入候选建议阶段使用已将近10年之久,也算完成了其历史使命,今年的3月20(2022年3月10日)正式进入Level2的规范( WorkingDraft ),Level2相比较Level1精度更高。

Level2处理模型如下:
w3c-level2.jpg

Level2将整个过程划分为了11个阶段,各阶段指标明细:

序号指标解释
1navigationStart表示从上一个文档卸载结束时的unix时间戳,如果没有上一个文
档,这个值将和fetchStart相等。
2unloadEventStart表示前一个网页(与当前页面同域)unload的时间戳,如果无前
一个网页unload或者前一个网页与当前页面不同域,则值为0。
3unloadEventEnd返回前一个页面unload时间绑定的回掉函数执行完毕的时间戳。
4redirectStart第一个HTTP重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为0。
5redirectEnd最后一个HTTP重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为0。
6fetchStart浏览器准备好使用HTTP请求抓取文档的时间,这发生在检查本地缓存之前。
7domainLookupStart
domainLookupEnd
DNS域名查询开始/结束的时间,如果使用了本地缓存(即无DNS查询)或持久连接,则与fetchStart值相等。
8connectStartHTTP(TCP)开始/重新建立连接的时间,如果是持久连接,则与fetchStart值相等
9connectEndHTTP(TCP)完成建立连接的时间(完成握手),如果是持久接,则与fetchStart值相等。
10secureConnectionStartHTTPS连接开始的时间,如果不是安全连接,则值为0。
11requestStartHTTP请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存。
12responseStartHTTP开始接收响应的时间(获取到第一个字节),包括从本地读取缓存。
13responseEndHTTP响应全部接收完成的时间(获取到最后一个字节),包括本地读取缓存。
14domLoading开始解析渲染DOM树的时间,此时Document…readyState变为loading,并将抛出readystatechange相关事件。
15domlnteractive完成解析DOM树的时间,Document…readyState变为interactive,并将抛出readystatechange相关事件,注意只是DOM树解析完成,这时候并没有开始加载网页内的资源。
16domContentLoadedEventStartDOM解析完成后,网页内资源加载开始的时间,在DOMContentLoaded事件抛出前发生。
17domContentLoadedEventEndDOM解析完成后,网页内资源加载完成的时间(如JS脚本加载执行完毕)
18domCompleteDOM树解析完成,且资源也准备就绪的时间,Document…readyState变为complete,并将抛出readystatechange相关事件。
19loadEventStartload事件发送给文档,也即load回调函数开始执行的时间。
20loadEventEndload事件的回调函数执行完毕的时间。

计算页面加载的总时长:

Navigation Timing API 为 window下挂的 Performance 对象增加了timing 和 navigation 属性,通过window.performance.timing可以得到 PerformanceTiming 接口,我们可以使用接口里面提供的loadEventEnd减去navigationStart得到页面加载总的时长。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>页面性能</title>
  </head>
  <body>
    <script>
      // 书签支持的 js 脚本格式:javascript:<code>
      javascript: (() => {
        const perfData = window.performance.timing;
        const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
        console.log("页面加载耗时:", pageLoadTime, "ms");
      })();
    </script>
  </body>
</html>
当你在自己的电脑中运行这段代码的时候会发现,输出的耗时居然是负数,那是因为本地服务的访问速度是很快了,所以整个页面的加载时间几乎为 0,我们可以将里面的 JavaScript 脚本保存到你的浏览器书签,在随便找一个网站等待加载完成后点击这个书签就可以在控制台输出正常的页面加载的完整时间了。<br />下图就是我们通过上述脚本获取到的页面耗时在 6798 毫秒,那么这个数据是否正确呢?其实浏览器的开发者工具也为我们统计了这个数据,可以接着看下面的第二张图的右下角。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1652369421894-e385f195-22be-4c7c-9d5b-5799cae0fde1.png#clientId=uc7a148d5-45b8-4&from=paste&height=229&id=yvEwR&name=image.png&originHeight=458&originWidth=2336&originalType=binary&ratio=1&rotation=0&showTitle=false&size=109747&status=done&style=none&taskId=u5e9415b8-1acc-4f73-8caf-a45a421973e&title=&width=1168)<br />下图右下角的时间是浏览器开发者工具为我们统计到的数据:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2373519/1652369350911-aba004f9-3cf7-4462-8c8d-f96a0dedb083.png#clientId=uc7a148d5-45b8-4&from=paste&height=764&id=MZcnY&name=image.png&originHeight=1528&originWidth=2746&originalType=binary&ratio=1&rotation=0&showTitle=false&size=616740&status=done&style=none&taskId=u86c37f1e-d7af-40ee-a73f-1b51e115c24&title=&width=1373)

根据前面的资源加载的流程图我们就可以获取到任意一段有意义的时间,比如:资源请求的耗时我们就可以使用 **responseEnd **减去 **requestStart **得到,DOM 加载的整个时间可以使用 **domComplete **减去 **domLoading **得到等。

如何得到更精确的数据:

前面我们使用的 API 所提供的数据通过查看均是毫秒级别的,也看得到 API 均被标注的弃用的标识。那么更高精度的数据我们就需要使用一个新的 API 来获取,最终得到一个 **PerformanceNavigationTiming **对象,这个对象提供了纳秒级别的数据统计,我们一起来执行下面的代码。

<script>
  window.onload = function () {
    // PerformanceEntryList
    const entries = window.performance.getEntries();
    console.log(entries);
  };
</script>

我们运行上述代码将在控制台得到下面的数组,其中数组的第一项的 typenavigate,这表示我们是通过在浏览器的地址栏输入 URL 打开的页面,也可以是通过点击链接、表单提交、脚本初始化,但不包括刷新页面,回退等,type 的更多描述见mdn
image.png
上图中的 name 属性在这里也需要说明一下,可以看到数据第一项的 name 表示这次资源请求的地址,其他的两个是特殊的标识,其内容取决于PerformanceEntry对象的subtypePerformanceEntry.entryType,所以特地摘录了 mdn 上的属性表格,

ValueSubtypeEntryType描述
URLPerformanceNavigationTimingframe, navigation文档的地址
URLPerformanceNavigationTimingresource请求资源所解析的URL
DOMStringPerformanceMarkmark执行performance.mark()创建标记时使用的名称
DOMStringPerformanceMeasuremeasure执行performance.measure()时使用的名称
DOMStringPerformancePaintTimingpaintfirst-paint
first-contentful-paint

我们也可以通过指定 **EntryType **来获取对应的信息,下面的代码我们将只会获取到 first-paint 和 **first-contentful-paint **两个元素:

<script>
  window.onload = function () {
    const entries = window.performance.getEntriesByType('paint');
    console.log(entries);
  };
</script>

image.png

常见的性能指标:

我们在前面输出PerformanceNavigationTiming对象时就已经看到了 name 非 first-paint 和 first-contentful-paint 的对象,这两个就是典型的前端性能指标,其次还有几个,我们用一个表格来看一下:

指标含义
FPFirst Paint,首次绘制,指浏览器从开始请求网站到屏幕渲染第一个像素点的时间。
FCPFirst Contentful Paint,首次内容绘制,指浏览器渲染出第一个内容的时间,这个内容可以是文本、图片、背景图、非空白的画布、SVG等,并不包括iframe元素。
LCPLargest Contentful Paint,最大内容绘制,指网页被展示在视口中的最大内容的显示时间。
FMPFirst Meaningful Paint,首次有效绘制,指网页渲染出第一个关键内容的时间。区别与 FCP,FMP 指第一块有意义的内容绘制。如:博客网站的关键内容指的是正文,视频网站的关键内容指的是视频播放器,电商网站的关键内容指的是商品列表或商品详情等。因其计算过于复杂,得到的结果并不是非常准确,在 Lighthouse6.0 中用 LCP 替换了 FMP。
DCLDOMContentLoaded,指网页中的 DOM 加载并完成解析后触发此事件, 但不包括样式、图像等。
Lload,指网页完成了所有的加载包括 DOM、样式、图像等内容后触发此事件。

下图是在控制台的性能页签抓取的数据:
image.png

实时获取性能指标:

上述的性能指标都存在一个问题,就是我们只能获取在 onload 执行前的性能数据,之后变化的数据将无法获取。所以在 HTML5 中为我们新增的 **PerformanceObserver **对象,可以使用观察者的模式来拿到每次发生变化后的数据,兼容性见下表:
image.png

获取 fp 和 fpc:

  1. 实例化 PerformanceObserver 对象;
  2. 在构造函数位置指定处理函数;
  3. 调用 observe 并指定 entryTypes 使其包含 paint
<script>
  const ob = new PerformanceObserver((list) => {
    list.getEntries().forEach((entrie) => {
      console.log("entrie:", entrie);
    });
  });
  ob.observe({ entryTypes: ["paint"] });
</script>

下图输出的信息就是对应的 fp 和 fcp 的信息了:
image.png

获取其他资源的性能数据:

  1. 调整 **observe **函数中 **entryTypes **使其包含 **resource **;
  2. 将 ob 的实例化提到其他脚本执行之前;
<head>
  <meta charset="UTF-8" />
  <title>页面性能</title>
  <script>
    const ob = new PerformanceObserver((list) => {
      list.getEntries().forEach((entrie) => {
        console.log("entrie:", entrie);
      });
    });
    ob.observe({ entryTypes: ["resource"] });
  </script>
  <script src="./index.js"></script>
</head>

下图输出的信息就包含了此脚本加载的性能信息:
image.png

获取脚本运行到特定位置的性能数据:

  1. 再次调整**observe **函数中 **entryTypes **使其包含 **resource **;
  2. 调用 window.performance.mark("own");进行打点,own 为自定义标记;
<head>
  <meta charset="UTF-8" />
  <title>页面性能</title>
  <script>
    const ob = new PerformanceObserver((list) => {
      list.getEntries().forEach((entrie) => {
        console.log("entrie:", entrie);
      });
    });
    ob.observe({ entryTypes: ["mark"] });
  </script>
  <script src="./index.js"></script>
  <script>
    console.log("↓↓↓↓↓↓↓↓↓↓↓");
    window.performance.mark("own");
    console.log("↑↑↑↑↑↑↑↑↑↑");
  </script>
</head>

下图输出的信息就包含了执行到此标记时的耗时:
image.png

计算 DOM 插入的时间:

  1. 编写一个 addDom 函数,内部实现一个 div 插入到 body 的完整过程;
  2. 在 addDom 函数的开始和结束位置分别使用 mark 进行打标记;
  3. 当我们触发页面中的 add dom 按钮的时候就会接收到包含我们自定义标记两个 **PerformanceMark **对象,其中包含的时间相减就可以得到DOM 插入的时间了。

image.png

内容扩展:

W3C标准只是推荐标准,并没有强制执行的能力。但在 Web 标准领域各浏览器厂商对推荐的标准都很重视,积极的响应,但并非所有的标准都能得到适配。那么作为一名Web前端的开发人员,我们在使用一些新的API的时候也要关注这些API目前处于什么样的阶段,兼容性如何,能否可靠的运行到生产环境。下图是一份W3C标准制定的流程示意图:

一份标准的制定从工作草案提出由公众和会员进行讨论形成最终工作草案后并再次进行讨论及修订,修订完毕的最终工作草案将列入候选推荐标准,此时的规范处于稳定状态,可以开展实验性的实施,在持续一段进行的验证和修复后就会进入到建议推荐标准的阶段,W3C会员将对这份标准进行审阅,如果审阅没有通过将打回到候选标准推荐,更严重的将直接打回到最初的工作草案阶段或彻底移除,审阅通过后W3C总监将宣布该规范为W3C推荐标准

写在最后:

在这一篇中我们介绍了几个在网页性能紧相关的几项指标,也介绍了从毫秒到纳秒的性能数据获取的 API 和使用的方式,期间我们也演示了通过浏览器提供的Performance功能,Chrome 浏览器还内置了 Lighthouse 功能来为我们的网页性能打分。我们通常也可以完全应用这两个工具,那么在需要定制一些私有的性能监控平台的时候 API 就可以发挥更大的功能了。

参考资料:

https://www.w3.org/TR/navigation-timing/
https://www.w3.org/TR/navigation-timing-2/
https://zhuanlan.zhihu.com/p/82981365

团队介绍

高灯科技交易合规前端团队(GFE), 隶属于高灯科技(北京)交易合规业务事业线研发部,是一个富有激情、充满创造力、坚持技术驱动全面成长的团队, 团队平均年龄27岁,有在各自领域深耕多年的大牛, 也有刚刚毕业的小牛, 我们在工程化、编码质量、性能监控、微服务、交互体验等方向积极进行探索, 追求技术驱动产品落地的宗旨,打造完善的前端技术体系。

  • 愿景: 成为最值得信任、最有影响力的前端团队
  • 使命: 坚持客户体验第一, 为业务创造更多可能性
  • 文化: 勇于承担、深入业务、群策群力、简单开放

Github:github.com/gfe-team
团队邮箱:gfe@goldentec.com

作者:GFE-小鑫同学
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
某网站性能测试用例 某网站提供会员模板下载、上传、购买、支付等功能,目前进入性能测试阶段,通过性能需求可以了解到主要有以下几个性能指标需要进行测试:   ● 产品页面刷新性能   ● 产品上传性能   ● 产品下载性能   目前给出的指标为:   延迟:   测试项 响应时间 抖动 备注   产品页面刷新 <5秒 <2秒   产品下载相应时间 <4秒 <2秒   吞吐量:   编号 项 吞吐量   Perf.T.1 所有登录用户在线状态更改频率 每10分钟1次   Perf.T.2 每日页面平均访问量 60000次   Perf.T.3 每日下载量 50000   Perf.T.4 平均每日新增会员数量 500   Perf.T.5 高峰同一模板下载量 100用户并发下载   Perf.T.6 高峰不同模板下载量 150用户并发下载   容量:   编号 项 容量   Perf.C.1 用户数 <=100万   Perf.C.2 活动用户数 10000   Perf.C.3 模板中心总用户数 <=25万   根据如上性能需求及数据我们该如何设计性能测试用例及场景呢?(可以说给出的性能需求很垃圾,没有丝毫价值,但没办法还是点做啊)   首先,我不去在乎它要求的性能是什么,我只需要去做在一定的测试环境下对系统进行压力测试,找到各个性能指标的临界点就好了,至于是否达到性能指标,在和性能需求对照编写测试报告即可。   所以,针对这几个需要进行性能测试的页面,我们做一下分析,如何设计场景才能尽可能准确地体现出系统的性能:   先说一下搜索页面   搜索页面根据对项目的了解,搜索后,将所有符合条件的结果遍历出来,显示在前台,每页的显示数量是一定的,超出的部分分页显示。根据上面的描述我们可以看出搜索结果是在将符合条件的所有结果集均发送到前台页面,对于页面显示对性能的消耗我们可以忽略不计,主要的压力来自数据的传输、sql的执行及应用服务器的处理过程,所以我可以从两个方面设计场景:   a、虚拟用户一定,不同数据库数量级的情况下,搜索的性能   如何确定虚拟用户的数量成为一个关键,我们可以让客户提供一个常规情况下每天访问用户数(如果没有实际数据可参考,可以根据产品方案中期望的用户数来代替),我们就用这个用户数来进行测试;再来分析一下不同的数据库数量级,如果系统运营1年的产品数据量是5万条,那么我们就根据这个值分别取1W条、3W 条、5W条、10W条、20W条数据量来进行测试(具体的分法可以根据实际情况而定),所以对于这个测试目标,我们可以设计5个场景进行:   虚拟用户数 数据库数量级 录制页面 并发用户数执行时间思考时间   100 10000 搜索页面 随机产生 30分钟 加入思考时间   100 30000 搜索页面 随机产生 30分钟 加入思考时间   100 50000 搜索页面 随机产生 30分钟 加入思考时间   100 100000 搜索页面 随机产生 30分钟 加入思考时间   100 200000 搜索页面 随机产生 30分钟 加入思考时间   b、一定数据库数量级,不同量虚拟用户的情况下,搜索的性能   我们定下来一个常规的数据库数据量,在数据量不变的情况下逐步增加虚拟用户数,测试一下不同虚拟用户压力下系统的性能   虚拟用户数 数据库数量级 录制页面 并发用户数执行时间思考时间   50 50000 搜索页面 随机产生 30分钟 加入思考时间   80 50000 搜索页面 随机产生 30分钟 加入思考时间   100 50000 搜索页面 随机产生 30分钟 加入思考时间   120 50000 搜索页面 随机产生 30分钟 加入思考时间   150 50000 搜索页面 随机产生 30分钟 加入思考时间   产品上传   影响上传性能主要因素有上传文件的大小和上传的请求数,所以我们就从这两个方面设计用例。   a、虚拟用户数一定,上传不同大小的文件   虚拟用户数 上传文件大小 录制页面 并发用户数 执行时间 思考时间   50 100k 上传页面 随机产生 30分钟 取消思考时间   50 300k 上传页面 随机产生 30分钟 取消思考时间   50 500k 上传页面 随机产生 30分钟 取消思考时间   50 800k 上传页面 随机产生 30分钟 取消思考时间   50 1M 上传页面 随机产生 30分钟 取消思考时间   b、上传文件大小一定,不同量的虚拟用户   虚拟用户数 上传文件大小 录制页面 并发用户数执行时间思考时间   20 300k 上传页面 随机产生 30分钟 取消思考时间   50 300k 上传页面 随机产生 30分钟 取消思考时间   80 300k 上传页面 随机产生 30分钟 取消思考时间   100 300k 上传页面 随机产生 30分钟 取消思考时间   产品下载   影响下载性能主要因素有下载文件的大小和下载的请求数,所以我们就从这两个方面设计用例   a、虚拟用户数一定,下载不同大小的文件   虚拟用户数 下载文件大小 录制页面 并发用户数执行时间思考时间   50 100k 下载页面 随机产生 30分钟 取消思考时间   50 300k 下载页面 随机产生 30分钟 取消思考时间   50 500k 下载页面 随机产生 30分钟 取消思考时间   50 800k 下载页面 随机产生 30分钟 取消思考时间   50 1M 下载页面 随机产生 30分钟 取消思考时间   b、下载文件大小一定,不同量的虚拟用户   虚拟用户数 下载文件大小 录制页面 并发用户数 执行时间 思考时间   20 300k 下载页面 随机产生 30分钟 取消思考时间   50 300k 下载页面 随机产生 30分钟 取消思考时间   80 300k 下载页面 随机产生 30分钟 取消思考时间   100 300k 下载页面 随机产生 30分钟 取消思考时间

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值