0. 优化的目的
加载过慢导致的白屏、计算耗时太久,导致响应不及时或卡顿,降低用户对网站的评价,导致流失率。
1. 如何度量性能
You can’t optimize what you can’t measure
必须以真实统计的数据为依据,优化过程中必须看妨碍性能的关键指标是否发生了变化。
1.1 合成监控(Synthetic Monitoring,SYN)
PageSpeed Insights 是由 Google 提供的一个检测网站性能的工具,输入网站地址后它会返回网站性能的得分,以及具体改善性能的建议。
1.2 真实用户监控(Real User Monitoring,RUM)
通过将用户访问过程产生的性能指标上传到我们的日志服务器上,然后清洗加工进行展示。在阿里巴巴有诸如ARMS、AEM等大量产品做这样的事情。
1.3 调试工具
Chrome DevTools作为最重要的网页开发调试工具,几乎能查看到网页加载使用过程的一切信息,作为开发者有必要熟练运用开发者工具。
2. 指标如何计算
2.1 Navigation Timing
W3C的 Navigation Timing API 规范定义了页面的加载性能的
对于前端而言,能干预的更多是 Processing 及之后的阶段,在W3C的模型中,Processing包含了多个指标,那么他们都代表什么含义呢?
domLoading
这是整个过程的起始时间戳,浏览器即将开始解析第一批收到的 HTML 文档字节。
domInteractive
表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。
domContentLoaded
一般表示 DOM 和 CSSOM 均准备就绪的时间点,可以构建渲染树了。jQuery的ready就是在此时使用,它的具体用法是:
document.addEventListener('DOMContentLoaded', function(){});
domComplete
网页及其所有子资源(图像等)都准备就绪,都已下载完毕。
loadEvent
作为每个网页加载的最后一步,浏览器会触发 onload 事件,以便触发额外的应用逻辑。
window.onload = function(){}
2.2 阶段指标
下面以某监控平台的指标为例:可以看到 DOM解析耗时 是最多的,对应的是 responseEnd 到 domInteractive 的时间。
![]() |
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资源)。结果就是一些比较大的文件先加载但后完成。
// html-CRP-block.html
<head>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="link-jquery.js"></script>
<link rel="stylesheet" href="bg-color.css"></link>
</head>
<body>
<p>Hello, CRP!!!</p>
</body>
//link-jquery.js
console.log('jquery download and executed ? ' + (!!$ ? 'yes' : 'not ye'));
//bg-color.css
p {
background: red;
}
我们用Chrome Devtools的Performance记录下来网络情况(如下图),蓝色是HTML,黄色是JS,紫色是CSS。首当加载完成的是HTML,然后是本地的 link-jquery.js
和 bg-color.css
文件,jquery-3.4.1.js
先排队了一段时间,然后请求到达,是最晚完成的。
b. JS的加载解析阻塞HTML和CSS的解析的例子
阻塞HTML的parse
// html-CRP-block.html
<head>
<link rel="stylesheet" href="bg-color.css"></link>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="link-jquery.js"></script>
<p id="hello">Hello, CRP!!!</p>
</body>
//link-jquery.js
console.log(document.getElementById('hello'));
如上代码中,我们可以在console中看到的输出结果为null,说明 JS的加载执行过程,block了HTML的解析,因为直接查询DOM Tree是没有该结构的。
阻塞CSS的render
//bg-color-red.css
p {
background: red;
}
//bg-color-green.css
p {
background: green;
}
// html-CRP-block.html
<body>
<p id="hello">Hello, CRP!!!</p>
<link rel="stylesheet" href="bg-color-red.css"></link>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<link rel="stylesheet" href="bg-color-green.css"></link>
</body>
上图可以得到的信息包括:
- 两个CSS文件都早于JS文件加载完成
- 在JS文件加载完成之前,第一个CSS的样式已经成功render(此时应该没有所有的CSSOM构建完成才对,为何能红色呢,难道renderTree分批次的吗🤔️)
- 在JS文件加载即将完成之前(截图没体现),第二个CSS的样式生效
- DCL(DomContentLoadedEvent)的触发时间在JS加载完成之后,说明 DOM + CssDOM都准备好了才能触发,和第二节中 domContentLoaded 的描述是相符的。
JS的加载同样也阻塞CSS的解析?和渲染。
c. JS按照加载顺序执行的例子
如下为console中的输出结果,可见虽然本地的js后面的先加载完,但是执行顺序能获取到jquery,说明执行是后执行的。即JavaScript的执行是严格依赖标签的出现顺序的。
> jquery download and executed ? yes
d. CSS按照出现顺序进行绘制
CSS文件的加载是加载和解析同步进行的;但只有等到整个CSS文件都被解析好,才会绘制CSSOM并且完成绘制,这是因为CSS文件中属性是可以相互覆盖的,加载部分就进行渲染无疑会降低性能。
下例子中先引入了bootstrap样式文件,界面长时间处于白色背景(哪怕本地的css已经加载完),然后才出现红色背景,并且body的padding消失,可见 CSS 的绘制,同样按照标签的出现顺序进行绘制。
// html-CRP-block.html
<body>
<p id="hello">Hello, CRP!!!</p>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="bg-color-red.css"></link>
</body>
e. CSS的加载会阻塞HTML parse 和 JS 执行
//bg-color-red.css
p {
background: red;
}
// html-CRP-block.html
<body>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
<p id="hello">Hello, CRP!!!</p>
<script src="link-jquery.js"></script>
<link rel="stylesheet" href="bg-color-red.css"></link>
</body>
网页中打开上述HTML可以看到,首先是页面空白,并且console中没有输出,然后过了数秒,才会出现红色背景的文字区域。
打开分析工具录制,可以看到蓝色的HTML先加载、然后是link-jquery.js、background-red.css先完成(虽然bootstrap.css先加载),在bootstrap.css加载完后,先Parse Stylesheet,然后继续 Parse HTML、Evaluate Script,可见 CSS的加载和解析也会阻塞HTML的解析 和 JS的解析执行。
3.4 小测验
通过上述的介绍相信你能基本理解网页加载过程中的顺序,尤其是JS/CSS将会如何相互作用,下面是到练习题,你可以试试看:
文章底部有正确答案,你也可以参考udacity上的视频讲解。
4. 优化性能的方法
4.1 关键渲染路径
CRP(关键渲染路径) 简单来说就是浏览器将HTML、CSS和JavaScript转化为可见的像素的过程。它被用于衡量网页低性能,它包含3个指标:
- CRP resourcs,代表了请求资源的数量
- CRP KB,代表了所有CRP resources资源的大小
- CRP length,代表了从请求从客户端到服务端来回的次数
注:内联的CSS,async或defer的JavaScript不参与到上述几个指标的计算中。
<html>
<head>
<meta name="viewport" content="width=device-width">
<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
</head>
<body>
<p>
Hello <span>web performance</span> students!
</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
<script src="analytics.js" async></script>
</body>
</html>
举例:在上述网页中,这几个指标分别为
- CRP resourcs,数量为2
- CRP KB,大小为
style.css
、analytics.js
文件大小之和 - CRP length,长度为2
优化关键渲染路径的思路在于:优化资源数和路径长度,你需要在项目开发过程中,去把握如何发送尽可能少的资源请求,以及那些资源是可以通过async(JavaScript)或inline(CSS)的方式来加载的。
4.2 网页渲染过程
这个过程包含如下几个步骤:
网页的第一请求个是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 性能优化的方法
- 通过压缩、合并、混淆,我们可以减少请求资源的大小。
- 通过浏览器和服务端缓存,我们能减少请求的次数。
- 通过媒体查询,可以减少请求资源的数量。
- 使用CDN,利用物理距离的缩短,加快请求的速度。
- 使用雪碧图,减少请求次数。
- 使用裁切得大小合适的图,减少请求资源大小。
- 通过给不需要马上执行的JS设置async 或 延缓执行,减少阻塞
- 使用内联的CSS,减少阻塞(虽然增加了HTML文件的大小,但是减少了请求的数量)
- 使用骨架屏进行可用之前的展示
第1-6条的思路都是 减少请次数求、请求更小体积、加快请求速度;第7-8条则是 提前让用户看到内容(通过减少阻塞或交互设计)。
5. 优化的具体实践
5.1 analyzer工具使用
webpack-bundle-analyzer是一个工具,使用它我们可以打包项目的过程,将打包结果输出到它的配置文件中,然后在用可伸缩的区块来展示依赖资源的大小。
webpack-bundle-analyzer的简单使用为:
安装
$ npm i -D webpack-bundle-analyzer
增加配置文件
配置完全拷贝自己项目的一份配置命名为webpack.generate.js
即可。
配置package.json
{
"scripts": {
"generate": "webpack --config webpack.generate.js --profile --json > stats.json",
"analyze": "webpack-bundle-analyzer --port 8888 stats.json"
}
}
依次执行命令
$ npm run generate
$ 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. 练习答案
6. 更多思考
对于DOMContentLoaded,Google的文档中有这么一段描述
如果JavaScript没有阻塞解析器,则 DOMContentLoaded 将在 domInteractive 后立即触发。
结合第 ready 指标中描述,可以推测出一些结论: