前端性能优化
by Teobler on 21 / 07 / 2017
views
性能优化一直是程序猿中讨论的比较多的一个问题,不论是算法的优化,代码逻辑的优化,还是各种各样的优化,都是在每一个程序中非常重要的一环。在前端开发中,性能优化能够给用户带来更好的用户体验,同时能够缓解服务器的压力,与日常开发密不可分,今天对性能优化做了一个比较系统全面的整理。
说到前端的性能优化,就不得不提CRP(关键路径渲染),那么什么是CRP呢?要了解CRP首先我们需要知道,一个网页是如何渲染出来的。
浏览器的页面渲染
DOM(文档对象模型)构建
首先,渲染一个页面需要构建该页面的DOM模型,需要经过以下四个步骤:
- **转换:**浏览器从磁盘或网络读取HTML的原始字节,然后根据指定的文件编码格式(例如 UTF-8)将其转换为相应字符,即从网络传输字节流到HTML字符的转换
- **令牌化:**浏览器把字符转化成 W3CHTML5标准 指定的各种确切的令牌,比如
<html>
、<body>
以及其他在尖括号内的字符串。每个令牌都有特殊的含义以及它自己的一套规则 - **词法分析:**生成的令牌转化为对象,这个对象定义了它们的属性及规则
- **DOM构建:**最后,由于HTML标记定义了不同标签之间的关系(某些标签嵌套在其他标签中),创建的对象在树状的数据结构中互相链接,树状数据结构也捕获了原始标签定义的父子关系:html对象是body对象的父对象,body是p对象的父对象等等
经过上面的流程之后,浏览器就会在本地构建好一个DOM模型,下面这张图描述了这个过程:
打开Chrome DevTools => performance,录制时间轴,上述过程对应Loading事件中的Parse HTML事件,可以查看到执行这一过程所需要的时间。
那么DOM模型构建好之后就直接渲染了么?当然不是,我们还缺少一个CSSOM来告诉浏览器,DOM模型需要渲染成什么样子。
CSSOM(CSS对象模型)构建
当浏览器构建上述网页DOM的时候,在head里面碰到一个link标签,这个标签引用了一个外部的CSS样式表。浏览器预测会需要这个资源来渲染页面,因此会立即发出一个该资源的请求。
与DOM模型构建相似,我们这里直接上图看结果:
想要查看CSS处理过程所花费的时间,可以在录制的时间轴中查看Rendering事件中的Recalculate Style事件:与DOM解析不同,timeline不显示单独的“Parse CSS”条目,而是在Recalculate Style事件下一同捕获CSS解析、CSSOM构建以及computed styles的递归计算。
渲染树构建
此时我们已经有了DOM模型跟CSSOM模型,接下来浏览器要做的是将两个模型树进行合并,构建成一个我们最终需要的渲染树,构建渲染树时浏览器将DOM和CSSOM的每个节点一一对应进行合并,大致过程如下:
- 从DOM树的根节点开始,遍历每个可见的节点
- 某些节点不可见(例如 script 标签、meta 标签等),因为它们不会体现在渲染结果中,所以会被忽略
- 某些通过 CSS 隐藏的节点在渲染树中也会被忽略,比如应用了 display:none 规则的节点
- 为每一个可见的节点匹配并应用对应的CSSOM规则
- 生成有内容和计算样式的可见节点
最终输出的就是一个包含了所有可见节点的内容及样式信息的渲染树:
现在我们已经有了目标渲染树了,但是我们还不知道应该以多大的宽高来进行页面的渲染,所以浏览器需要进行布局,也就是重排。
布局与绘制
为了计算出页面中每个对象的准确大小和位置,浏览器从渲染树的根节点开始遍历,计算页面上每个对象的几何信息。例如:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
布局过程浏览器需要做的就是捕获每个节点在视口中的准确位置以及尺寸的大小,并且输出这个“盒子模型”;同时需要注意的是,所有相对度量单位都会被转换为屏幕上的绝对像素。
以上面的例子来说,页面的 body 包含两个嵌套 div:第一个 div(父元素)将节点尺寸大小设置为视口宽度的 50%,第二个 div 的宽度为父元素的 50%,即视口宽度的 25%。则最终结果如下:
在得到了这些“盒子模型”之后,最后需要做的当然就是按照规则将渲染树与“盒子模型“相”结合“,把渲染树上的每一个节点都转化成设备上的像素点,进行最后的绘制,之后我们就能看到我们的页面了。
CRP
那么,我们经过了哪些步骤呢?
- 处理 HTML 标记,构建 DOM 树
- 处理 CSS 标记,构建 CSSOM 树
- 将 DOM 树和 CSSOM 树融合成渲染树
- 根据渲染树进行布局,计算每个节点的几何信息
- 在屏幕上绘制各个节点
上面的5个步骤是渲染一个页面必不可少的步骤,所以也称作CRP(Critical Rendering Path,关键渲染路径)
而前端优化最主要的就是做到尽可能缩短这些步骤的时间
CRP优化
那么优化是不是直接上手,胡乱优化一通呢?当然不是,为了做到有的放矢,我们再引入一个概念
CRP性能
我们先定义三个用于描述CRP性能的词汇:
- **关键资源:**能够阻止网页首次渲染的资源
- **关键路径长度:**往返过程的数量,或者获取所有关键资源所需的总时间
- **关键字节:**网页首次渲染所需的总字节数,是所有关键资源的传输文件大小总和
为了使用这些概念,我们举几个小例子:
- 假如该网页只有HTML文件,且假设大小为3KB,那么我们就只需要请求一次HTML的文件,并进行接下来的步骤,此时有:
- 1个关键资源(HTML文件)
- 1个关键路径长度(假设一次请求能拉下该文件)
- 3KB关键字节 (该文件大小)
- 假设在上面的基础上加入了CSS文件(6KB),则有:
- 2个关键资源
- 最少2个关键路径长度(两次请求能拉下来就2个)
- 9KB关键字节
- 在 2 的基础上加上js文件(6KB):
- 3个关键资源
- 最少2个关键路径长度(可以异步请求CSS与JS)
- 15KB关键字节
优化方法
好了,在说了这么多前提之后,我们正式进入优化阶段,在这边我将优化分为4个部分:
关键资源
CSS与JS文件的加载
阻塞渲染的CSS
CRP要求DOM和CSSOM两者融合在一起才能构建渲染树。这就导致了一个性能问题:HTML和CSS都是阻塞渲染的资源。HTML很显然,没有DOM就没有内容去渲染。CSS没有那么明显,但确实是阻塞渲染的资源。如果CSS不阻塞渲染,我们看到的很可能是这样的一个画面:页面刚加载出来的时候其丑无比,过了一会,页面又变漂亮了。
既然CSS是阻塞渲染的资源,这就意味着在CSSOM构建完成之前,浏览器不会去渲染任何已处理的内容。要尽早、尽快地把CSS下载到客户端以优化首次渲染的时间。
使用CSS“媒体类型”和“媒体查询”优化阻塞渲染的CSS:
<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">
- 第一条声明阻塞渲染,匹配所有情况
- 第二条声明只适用于打印(媒体类型),因此,页面在浏览器中首次加载时,不会阻塞渲染
- 第三条声明提供了媒体查询,由浏览器判断:如果条件符合,则在该样式表下载并处理完以前,浏览器会阻塞渲染
阻塞渲染的JS
JS可以修改页面的内容、样式以及响应用户的交互,JS在DOM、CSSOM和JS执行之间引入了很多新的依赖关系,导致浏览器在处理和渲染页面上出现大幅延迟:
- 当浏览器遇到
<script>
标签时,DOM构建会暂停,直到脚本执行完毕 - JavaScript 执行会暂停,直到CSSOM准备就绪
默认情况下,JavaScript 执行会阻塞解析器:当浏览器在文档中遇到<script>
标签时,DOM构建必须暂停,浏览器把控制权移交给JS引擎,JS引擎编译并执行脚本,脚本执行完毕后再继续构建DOM。
事实上,内联脚本始终会阻塞解析器,除非你编写额外的代码来延迟它们的执行。那通过<script>
引入的外联脚本呢?结果是一样的,浏览器都会暂停,然后执行脚本,脚本执行完毕之后再去处理文档的剩余部分。尽管如此,通过<script>
引入外联脚本还是有一个很大的好处。
默认情况下,所有 JS 均会阻塞解析器,因为浏览器不知道脚本想在页面上做什么,因此它必须假定最糟的情况并阻塞解析器。但是,如果我们能够有个信号告知浏览器,说脚本无需在文档中引用它的确切位置被执行呢?这样一来,浏览器就会继续构建DOM,并在脚本准备就绪后执行脚本。
这个信号就是async——在script标签里面添加async关键字,其有两个特性:
- 告诉浏览器当它碰到
<script>
标签时不用阻塞DOM构建,因此浏览器会忽略脚本请求,继续解析DOM - JS执行不依赖CSSOM:如果在CSSOM就绪之前脚本已经就绪,脚本可以立即执行
进行图片等资源的懒加载
关键路径长度
- 减少http请求 --- 减少了连接的时间(dns解析、三次握手、四次挥手)、减少了服务器压力(cpu相应)、减少了请求头释放了带宽
- CSS/JS合并打包
- 小图标使用iconfont 或 雪碧图
- 多彩小图标无法使用iconfont可以使用base64格式直接嵌入到src中(需要注意的是base64无法缓存)
关键字节
- 压缩静态资源体积
- CSS/JS文件压缩
- 冗余代码去除
- 图片压缩
- 代码优化
非前端优化
除了在前端可以进行性能优化以外,在非前端也能进行优化,例如:
- 服务器端开启gzip压缩
- 使用多域名,一方面为解决浏览器对相同域名的并发连接数限制(提高并发);另一方面可做到互为backup。感兴趣的可以看下又拍云、阿里云联合云存储
- CDN缓存,简单来讲就是在不同地点缓存内容,通过负载均衡,将用户请求定向到最合适的缓存服务器上获取内容。通过就近访问,加速用户对网站的访问;解决网络拥堵状况,提高用户访问网络的响应速度。使用cdn的时候有一点需要注意,即文件的缓存更新问题。实际上,若你的js/css文件缓存时间较长且是覆盖式发布,那需要注意,哪怕反复多次强刷cdn,它的刷新率也是很低的,做不到100%所有节点的缓存文件都被刷新。