0. 优化的目的

加载过慢导致的白屏、计算耗时太久,导致响应不及时或卡顿,降低用户对网站的评价,导致流失率。

1. 如何度量性能

You can’t optimize what you can’t measure

必须以真实统计的数据为依据,优化过程中必须看妨碍性能的关键指标是否发生了变化。

1.1 合成监控(Synthetic Monitoring,SYN)

PageSpeed Insights 是由 Google 提供的一个检测网站性能的工具,输入网站地址后它会返回网站性能的得分,以及具体改善性能的建议。

原创 谈一谈前端性能的指标和阻塞 - 图1

1.2 真实用户监控(Real User Monitoring,RUM)

通过将用户访问过程产生的性能指标上传到我们的日志服务器上,然后清洗加工进行展示。在阿里巴巴有诸如ARMS、AEM等大量产品做这样的事情。

1.3 调试工具

Chrome DevTools作为最重要的网页开发调试工具,几乎能查看到网页加载使用过程的一切信息,作为开发者有必要熟练运用开发者工具。

2. 指标如何计算

2.1 Navigation Timing

W3C的 Navigation Timing API 规范定义了页面的加载性能的
image.png

对于前端而言,能干预的更多是 Processing 及之后的阶段,在W3C的模型中,Processing包含了多个指标,那么他们都代表什么含义呢?

原创 谈一谈前端性能的指标和阻塞 - 图3

domLoading

这是整个过程的起始时间戳,浏览器即将开始解析第一批收到的 HTML 文档字节。

domInteractive

表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。

domContentLoaded

一般表示 DOM 和 CSSOM 均准备就绪的时间点,可以构建渲染树了。jQuery的ready就是在此时使用,它的具体用法是:

  1. document.addEventListener('DOMContentLoaded', function(){});

domComplete

网页及其所有子资源(图像等)都准备就绪,都已下载完毕。

loadEvent

作为每个网页加载的最后一步,浏览器会触发 onload 事件,以便触发额外的应用逻辑。

  1. window.onload = function(){}

2.2 阶段指标

下面以某监控平台的指标为例:可以看到 DOM解析耗时 是最多的,对应的是 responseEnd 到 domInteractive 的时间。

image.png DNS查询耗时:
domainLookupEnd - domainLookupStart
TCP建连耗时:
connectEnd - connectStart
HTML请求耗时:
responseStart - requestStart
HTML响应耗时:
responseEnd - responseStart
DOM解析:
domInteractive(domLoading) - responseEnd
资源加载(不包含阻塞DOM解析的资源):
domComplete - domInteractive(domLoading)
onload事件回调耗时:
loadEventEnd - loadEventStart

2.2 度量指标

如下度量指标,每个指标的起始位置都是访问开始,结束位置 越后面的指标的时间越长

fpt(first paint time)

首次渲染时间 / 白屏时间,计算公式为:domLoading - navigationStart,即键入网址到浏览器开始解析DOM到时间。

tti (time to interact)

首次可操作时间,计算公式为 domInteractive - navigationStart,包含了 fpt 的时间。

ready

html 加载完成时间, 即 dom ready 时间,计算公式为 domContentLoadEventEnd - navigationStart,如果页面有同步执行的 js,则同步 js 执行时间 = ready - tti

load

页面完全加载时间,计算公式为loadEventStart - navigationStart
[

](https://yuque.antfin.com/retcode/arms-retcode/keywords)

3. 资源的阻塞问题

为了解决上节中 domInteractive 耗费时间过长的问题,我们需要理解前端资源加载的问题。这里先给出加载和阻塞的概念 和 一般性结论,最后会用例子去证实它们。

3.1 概念

  • 加载,资源从服务器到客户端的过程,HTML/CSS/JavaScript/Image都必须有这个过程
  • parse,HTML作为富文本语言,需要parse + 构建后能转化为DOM
  • 构建,HTML被构建为DOM,CSS被构建为CSSOM,DOM+CSSOM被构建为Render Tree
  • render,对应Render Tree被Layout的过程

3.2 结论

HTML加载
1)是最先加载的资源,其他资源依次并行加载;
2)资源加载完成时间和加载顺序无关,和网络和文件大小有关。

JS的加载
1)会阻塞HTML parse 和 CSS render;
2)JS的执行按照标签出现的顺序执行。

CSS的加载
1)CSS的加载和解析同步进行的
2) 会阻塞HTML parse 和 JS 执行;
3)CSS render(绘制CSSOM并且完成渲染)需要等待整个文件加载解析完成;
4)CSS render 的顺序和标签的出现顺序相同。

3.3 例子

a. HTML资源并行加载的例子

虽然引入资源的代码在HTML文件的不同位置,但浏览器预加载程序会扫描页面以并行的方式加载资源(包括CSS资源)。结果就是一些比较大的文件先加载但后完成。

  1. // html-CRP-block.html
  2. <head>
  3. <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
  4. <script src="link-jquery.js"></script>
  5. <link rel="stylesheet" href="bg-color.css"></link>
  6. </head>
  7. <body>
  8. <p>Hello, CRP!!!</p>
  9. </body>
  1. //link-jquery.js
  2. console.log('jquery download and executed ? ' + (!!$ ? 'yes' : 'not ye'));
  1. //bg-color.css
  2. p {
  3. background: red;
  4. }

我们用Chrome Devtools的Performance记录下来网络情况(如下图),蓝色是HTML,黄色是JS,紫色是CSS。首当加载完成的是HTML,然后是本地的 link-jquery.jsbg-color.css文件,jquery-3.4.1.js先排队了一段时间,然后请求到达,是最晚完成的。

原创 谈一谈前端性能的指标和阻塞 - 图5

b. JS的加载解析阻塞HTML和CSS的解析的例子

阻塞HTML的parse

  1. // html-CRP-block.html
  2. <head>
  3. <link rel="stylesheet" href="bg-color.css"></link>
  4. </head>
  5. <body>
  6. <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
  7. <script src="link-jquery.js"></script>
  8. <p id="hello">Hello, CRP!!!</p>
  9. </body>
  1. //link-jquery.js
  2. console.log(document.getElementById('hello'));

如上代码中,我们可以在console中看到的输出结果为null,说明 JS的加载执行过程,block了HTML的解析,因为直接查询DOM Tree是没有该结构的。

阻塞CSS的render

  1. //bg-color-red.css
  2. p {
  3. background: red;
  4. }
  1. //bg-color-green.css
  2. p {
  3. background: green;
  4. }
  1. // html-CRP-block.html
  2. <body>
  3. <p id="hello">Hello, CRP!!!</p>
  4. <link rel="stylesheet" href="bg-color-red.css"></link>
  5. <script src="https://code.jquery.com/jquery-3.4.1.js"></script>
  6. <link rel="stylesheet" href="bg-color-green.css"></link>
  7. </body>

原创 谈一谈前端性能的指标和阻塞 - 图6

上图可以得到的信息包括:

  1. 两个CSS文件都早于JS文件加载完成
  2. 在JS文件加载完成之前,第一个CSS的样式已经成功render(此时应该没有所有的CSSOM构建完成才对,为何能红色呢,难道renderTree分批次的吗🤔️)
  3. 在JS文件加载即将完成之前(截图没体现),第二个CSS的样式生效
  4. DCL(DomContentLoadedEvent)的触发时间在JS加载完成之后,说明 DOM + CssDOM都准备好了才能触发,和第二节中 domContentLoaded 的描述是相符的。

JS的加载同样也阻塞CSS的解析?和渲染。

c. JS按照加载顺序执行的例子

如下为console中的输出结果,可见虽然本地的js后面的先加载完,但是执行顺序能获取到jquery,说明执行是后执行的。即JavaScript的执行是严格依赖标签的出现顺序的。

  1. > jquery download and executed ? yes

d. CSS按照出现顺序进行绘制

CSS文件的加载是加载和解析同步进行的;但只有等到整个CSS文件都被解析好,才会绘制CSSOM并且完成绘制,这是因为CSS文件中属性是可以相互覆盖的,加载部分就进行渲染无疑会降低性能。

下例子中先引入了bootstrap样式文件,界面长时间处于白色背景(哪怕本地的css已经加载完),然后才出现红色背景,并且body的padding消失,可见 CSS 的绘制,同样按照标签的出现顺序进行绘制

  1. // html-CRP-block.html
  2. <body>
  3. <p id="hello">Hello, CRP!!!</p>
  4. <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
  5. <link rel="stylesheet" href="bg-color-red.css"></link>
  6. </body>

e. CSS的加载会阻塞HTML parse 和 JS 执行

  1. //bg-color-red.css
  2. p {
  3. background: red;
  4. }
  1. // html-CRP-block.html
  2. <body>
  3. <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
  4. <p id="hello">Hello, CRP!!!</p>
  5. <script src="link-jquery.js"></script>
  6. <link rel="stylesheet" href="bg-color-red.css"></link>
  7. </body>

网页中打开上述HTML可以看到,首先是页面空白,并且console中没有输出,然后过了数秒,才会出现红色背景的文字区域。

打开分析工具录制,可以看到蓝色的HTML先加载、然后是link-jquery.js、background-red.css先完成(虽然bootstrap.css先加载),在bootstrap.css加载完后,先Parse Stylesheet,然后继续 Parse HTML、Evaluate Script,可见 CSS的加载和解析也会阻塞HTML的解析 和 JS的解析执行

原创 谈一谈前端性能的指标和阻塞 - 图7

原创 谈一谈前端性能的指标和阻塞 - 图8

3.4 小测验

通过上述的介绍相信你能基本理解网页加载过程中的顺序,尤其是JS/CSS将会如何相互作用,下面是到练习题,你可以试试看:

原创 谈一谈前端性能的指标和阻塞 - 图9

文章底部有正确答案,你也可以参考udacity上的视频讲解。

4. 优化性能的方法

4.1 关键渲染路径

CRP(关键渲染路径) 简单来说就是浏览器将HTML、CSS和JavaScript转化为可见的像素的过程。它被用于衡量网页低性能,它包含3个指标:

  • CRP resourcs,代表了请求资源的数量
  • CRP KB,代表了所有CRP resources资源的大小
  • CRP length,代表了从请求从客户端到服务端来回的次数

注:内联的CSS,async或defer的JavaScript不参与到上述几个指标的计算中。

  1. <html>
  2. <head>
  3. <meta name="viewport" content="width=device-width">
  4. <link href="style.css" rel="stylesheet">
  5. <link href="print.css" rel="stylesheet" media="print">
  6. </head>
  7. <body>
  8. <p>
  9. Hello <span>web performance</span> students!
  10. </p>
  11. <div><img src="awesome-photo.jpg"></div>
  12. <script src="app.js"></script>
  13. <script src="analytics.js" async></script>
  14. </body>
  15. </html>

举例:在上述网页中,这几个指标分别为

  • CRP resourcs,数量为2
  • CRP KB,大小为style.cssanalytics.js文件大小之和
  • CRP length,长度为2

优化关键渲染路径的思路在于:优化资源数和路径长度,你需要在项目开发过程中,去把握如何发送尽可能少的资源请求,以及那些资源是可以通过async(JavaScript)或inline(CSS)的方式来加载的。

4.2 网页渲染过程

这个过程包含如下几个步骤:

原创 谈一谈前端性能的指标和阻塞 - 图10

网页的第一请求个是HTML请求,请求到达服务器后,服务器处理并且返回响应,客户端收到响应后开始进行处理,最后进行展示和布局,对应的过程为:

  • request
  • loading
  • response
  • scripting:HTML parsing,DOM Tree,CSSOM Tree,Render Tree
  • rendering:layout
  • painting: paint

script

在scripting阶段,HTML parsing过程创建了DOM(document object model)Tree,遇到任意链接的外部资源(比如样式文件、js文件、图片文件),浏览器都会发起一个新的请求。

浏览器会持续不断进行parsing的过程构建DOM,发送获取资源的请求,直到最终结束以后,它开始构建 CSSOM(CSS object model)。在DOM和CSSOM都完成了,浏览器会构建好Render Tree,计算所有可见内容的样式,在浏览器的Performance中通常对应了Recalculate Style这个阶段。

rendering

在rendering阶段,Render Tree构建好以后,开始进行Layout操作,它决定元素放置的位置和大小。

painting

等这一切结束以后,在painting阶段,网页通过称为被paint的步骤被绘制出来。

在请求得到HTML后,请求JS和CSS,最终解析的得到DOM、CSSOM构成Render Tree。因此我们总结下来让网页性能优化的两种途径:

4.3 性能优化的方法

  1. 通过压缩、合并、混淆,我们可以减少请求资源的大小
  2. 通过浏览器和服务端缓存,我们能减少请求的次数
  3. 通过媒体查询,可以减少请求资源的数量
  4. 使用CDN,利用物理距离的缩短,加快请求的速度
  5. 使用雪碧图,减少请求次数
  6. 使用裁切得大小合适的图,减少请求资源大小
  7. 通过给不需要马上执行的JS设置async 或 延缓执行,减少阻塞
  8. 使用内联的CSS,减少阻塞(虽然增加了HTML文件的大小,但是减少了请求的数量)
  9. 使用骨架屏进行可用之前的展示

第1-6条的思路都是 减少请次数求、请求更小体积、加快请求速度;第7-8条则是 提前让用户看到内容(通过减少阻塞或交互设计)。

5. 优化的具体实践

5.1 analyzer工具使用

webpack-bundle-analyzer是一个工具,使用它我们可以打包项目的过程,将打包结果输出到它的配置文件中,然后在用可伸缩的区块来展示依赖资源的大小。

原创 谈一谈前端性能的指标和阻塞 - 图11

webpack-bundle-analyzer的简单使用为:

安装

  1. $ npm i -D webpack-bundle-analyzer

增加配置文件

配置完全拷贝自己项目的一份配置命名为webpack.generate.js即可。

配置package.json

  1. {
  2. "scripts": {
  3. "generate": "webpack --config webpack.generate.js --profile --json > stats.json",
  4. "analyze": "webpack-bundle-analyzer --port 8888 stats.json"
  5. }
  6. }

依次执行命令

  1. $ npm run generate
  2. $ npm run analyze

可以在浏览器上看到打开的资源大小的色图,在知道了各项资源的大小了以后,可以分析项目中是否有很大的资源,但是在项目中实际使用率很低,或者完全可以被更轻便的方法替代。

5.2 具体问题分析

在2.2的例子中,domInteractive 耗时间是最长的,它表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。即要减少对 HTML解析 和 构建的影响。

5.3 经常采用的关键指标

fpt(first paint time)

首次渲染时间 / 白屏时间,计算公式为:domLoading - navigationStart,即键入网址到浏览器开始解析DOM到时间。

tti (time to interact)

首次可操作时间,计算公式为 domInteractive - navigationStart

ready

html 加载完成时间, 即 dom ready 时间,计算公式为 domContentLoadEventEnd - navigationStart,如果页面有同步执行的 js,则同步 js 执行时间 = ready - tti

load

页面完全加载时间,计算公式为loadEventStart - navigationStart

注:附图中没有loadEventStart,可以理解为 domComplete 时间戳之后。

更细粒度的时间戳可以在 www.w3.org 规范中看到。

第1节内容来自Google的开发者文档,第2节内容来自retcode文档中的指标说明

5. 练习答案

image.png

6. 更多思考

对于DOMContentLoaded,Google的文档中有这么一段描述

如果JavaScript没有阻塞解析器,则 DOMContentLoaded 将在 domInteractive 后立即触发。

结合第 ready 指标中描述,可以推测出一些结论:

  1. DOM 准备就绪后,CSSOM 几乎同步准备就绪,因为 CSS 的引入通常是在head中,并且是阻塞的,DOM 就绪时候,CSSOM 也会是就绪的。
  2. 阻塞的 JavaScript 即同步执行的 js 代码。domInteractive 到 domContentLoadEventEnd 的时间,就是同步的 JavaScript 的执行时间;如果没有阻塞的JavaScript,domContentLoadEventEnd 几乎在 domInteractive 后马上执行。

    7. 参考文档

www.w3.org 规范
Google的开发者文档
retcode文档中的指标说明