本文主要讲解web浏览器的渲染原理、流程到性能优化。主要有以下几点:
(1) script标签中的属性defer和async的区别
(2) 浏览器的渲染顺序
(3) 如何防止阻塞DOM渲染
(4) 如何保证首屏优化、关键渲染路径优化
(5) 如何从浏览器渲染、网络请求、js引擎机制优化性能
一: script标签中的属性defer和async的区别
要想让script标签中的defer和async属性生效,必须引入属性src,即js文件必须是外部引入的脚本文件。
下面讨论两个基本点:
(1) 下载时是否会阻塞DOM的渲染
(2) 执行时是否会阻塞DOM的渲染
首先谈一下async属性的特性:
(1) async属性是赋予脚本异步属性
(2) 页面一加载的时候,就立即下载脚本,不妨碍页面中的其他操作,比如下载其他资源、等待加载其他脚本
(3) js一旦下载好,就会立即执行
(4) 和文档同时呈现,标记为async的js脚本不会按照顺序执行
(5) 一般js不需要改变页面的DOM的时候,可以使用属性async进行异步加载
(6) 注意,异步脚本不要在加载期间修改DOM。有async属性的脚本,下载的时候不会阻塞DOM的解析,但是执行的时候会阻塞页面的解析
(7) 具有async属性的js脚本一定会在页面的load事件前执行,,但可能会在DOMContentLoaded事件触发之前或之后执行
其次谈一下defer属性的特性:
(1) defer属性赋予脚本延迟属性
(2)页面一加载的时候,就立即下载具有defer属性的js脚本,下载期间不阻塞DOM的解析,但等到整个DOM都解析完成之后才运行具有defer属性的JS脚本
(3) 下载的时候不阻塞DOM解析,执行具有defer属性的js时候也不会阻塞DOM的解析
(4) 具有defer属性的JS脚本会按照书写的前后顺序执行,因此脚本具有前后依赖关系可以放心使用(在实际中具有defer属性的脚本并不一定会按照书写顺序执行,所以最好是只让一个js脚本具有defer属性)
(5) 具有defer属性的js脚本会先于DOMContentLoaded事件执行(实际当中,也不一定会在DOMContentLoaded事件前执行)
现在总结一下async和defer属性的共同点:
(1) 两者在下载的时候都不会阻塞DOM解析
(2) 两者都只对外部脚本有效,如<script src='index.js' defer='defer' ></script>或者<script src='index.js' async='async' ></script>
(3) 都可以使用onload事件进行一系列处理
现在总结一下async和defer属性的不同点:
(1) 不具有async和defer属性的js脚本,浏览器根据其所在位置阻塞解析,被下载紧接着执行,直到完成。
(2) 具有async属性的js脚本,在下载的时候不会阻塞DOM的解析。但是js文件一旦下载后之后,就会立即执行脚本,此时有可能会阻塞DOM的解析。即具有async属性的js脚本下载时候不会阻塞DOM的解析,但在执行的时候有可能会阻塞DOM的解析。
(3) 具有defer属性的js脚本,在下载的时候不会阻塞DOM的解析。js加载完之后,等待DOM渲染完成之后在执行js脚本,此时不会阻塞DOM的解析。即具有async属性的js脚本下载和执行的时候都不会阻塞DOM的解析。
两者的应用场景
(1) 具有async属性的js脚本,适合基本没有DOM操作、和模块的加载顺序无关、执行时间要短,否则对首屏还是有很大的影响的。
(2) 具有defer属性的js脚本,按序加载,加载和执行的时候都不会阻塞DOM的解析。
二:浏览器关键渲染顺序
接下来让我们来思考几个问题:
(1) 具有async属性的js脚本可能会阻塞DOM的解析,那么css在加载的时候回阻塞DOM的解析吗?
(2) 其他资源如何下载?
(3) 其他资源会阻塞DOM的解析吗?
(4)DOM parse是什么?
(5) 事件DOMContentLoaded和load分别是基于那个节点触发的?
首先让我们来看一下浏览器的渲染过程
下图展示了整个浏览器的渲染过程

(1) 文档对象模型(DOM)
上图展示了在浏览器里面,html的字节码被转换成DOM的过程
A、Bytes->Characters转换:根据字节Bytes的编码规则,将其转换为特定的字符Characters
B、Characters->Tokens(生成Tokens):将Characters转化为w3c定义的各种特定标签,生成Tokens(令牌)
C、Tokens->Nodes(词法解析):匹配字符串,将Tokens按照规则转换成节点对象(Nodes),其具有属性和规则
D、Nodes->DOM(DOM构建):根据每个节点的层次关系和属性,转换为直观的树形结构,具有明确的父子关系
至此,得到页面完整的DOM模型,以后的页面渲染(Render Tree)包括布局(Layout)和绘制(Paint)都是基于DOM的
注意:HTML都是增量构建的,在HTML文件还在传输的时候,HTML parse就已经开始了。
DOM代表页面的结构,决定整个初始化页面的布局;CSSOM决定页面的样式
(2) CSS对象模型(CSSOM)
如上图所示是CSSOM构建流程图,将css文件的字节码转换为符合浏览器特定规则的字符,然后浏览器对其解析和构成树。整个计算的过程包括一套复杂的特异度计算规则(css属性来源->特异度大小->书写顺序前后覆盖),最终确定每个节点的样式值,形成CSSOM。 css被认为 是一种渲染阻塞资源(所谓的CSS白屏),因为渲染树是依赖CSSOM才能生成,然后才到浏览器的布局渲染流程,因此才将css放到head标签里面。
(3) 渲染树(Render Tree)
渲染树生成的大概过程如下:
A、从DOM的根节点开始遍历每个在HTML和CSS上的可见节点
B、对每个可见节点,为其找到适配的CSSOM并且组合他们
C、将每个节点(包括内容和样式)组建成Render Tree
所谓的可见节点:渲染树包含了渲染网页所需的所有节点,不需要渲染的节点是不会合并到渲染树里面的,比如元数据元素meta,base等,设置display:none的节点
(4) 布局(Layout)-计算渲染树节点大小
布局的最终效果是形成一个“盒子模型”,他需要精确地计算出每个元素所占据的位置坐标,将相对测量值(rem,vw,vh,em)转换成绝对像素。
下面讲解一下相对测量值的转换规则:
A、rem是相对根元素<html>的font-size值确定大小的;
B、vw,vh是相对视窗口的大小来确定的
我们可以在js里面改变节点的样式,但是css元素的位置和大小改变的,从而改变整体布局的话,那么浏览器会重新布局和渲染,这在开发过程中要注意避免和减小性能损耗的。
(5) 绘制(Paint)
根据background,border,box-shadow等样式和HTML内容,将Layout生成的区域填充为最终显示在屏幕上的像素
注意:DOMContentLoaded发生在DOM树构建完成之后发生的,也就是DOM解析完</html>的那一刻。load事件则是在所有资源都加载渲染之后才会触发
三、优化关键渲染路径(在很大程度上是指了解和优化HTML CSS JS 之间的依赖关系)
优化关键渲染路径是指优先显示与用户当前操作有关的内容。
1、CSS阻塞渲染
CSSOM形成前,浏览器不会渲染任何已经处理过的内容,所以css被视为阻塞渲染的资源(主要指chrom浏览器,各大浏览器的实现有差异)
要解决css阻塞的问题有几个维度:
A、网速;
B、大小;
C、尽早并行下载;
D、尽早开始构建CSSOM;
E、构建CSSOM的速度
css阻塞渲染的优化方法:
1、媒体查询
前端的使用场景众多,需要写适配的多端的代码,媒体查询(media)虽然下载全部的代码,但是只解析符合媒体查询的条件的代码,这样就做到尽量少的阻塞渲染
2、preload(尽早并行下载)
<link rel="preload" href="index.css" target="_blank" rel="external nofollow" as='style' οnlοad="this.rel='stylesheet'">
将rel设置成preload,相当于加了一个标志位,浏览器解析的时候会提前建立连接(或加载资源),做到尽早并行下载,然后再onload事件响应之后将link的rel属性改成 stylesheet即可进行解析。
3、动态添加link
var style = document.createElement('link');
style.rel = 'stylesheet';
style.href = 'index.css';
document.head.appendChild(style);
js动态添加DOM元素的link,不会阻塞渲染
4、代码简练、不使用css计算、避免使用统配、高级选择器
使用css的时候需要注意以下几点:
1、将css放在head标签里面:不管内联还是外联都尽早开始下载或者构建CSSOM(前提是这个css是首屏必须的)
2、避免使用css import:在css中可以使用import引入另一个样式表,不过这会在构建CSSOM时增加一次网络来回时间
3、适度内联css,衡量其他因素,如外联网络来回影响多大、HTML大小、CSS大小
4、全面考虑渲染情况:网速差、文件下载失败等,防止白屏时间过长
5、讨论css在 IE Chrom Firefox的区别
A、IE只要一遇到<html>标签就开始绘制
B、Chrom不管css放在前面还是后面,都要等到CSSOM构建形成之后才会绘制在页面上
C、Firefox:css放在head里面则会阻塞绘制,放在body的末尾则会先绘制前面的标签
2、js阻塞渲染
一般情况下,<script>标签的最佳实践是放在</body>的前面。
js能操作DOM CSSOM,为了减少不必要的冲突和低效,浏览器都会做最坏的打算,所以正常情况下js的执行会阻塞DOM构建,等待CSSOM的构建完毕在执行。
A、在HTML解析器解析到script标签的时候,会停止DOM构建,将控制权转交给js引擎,当js执行完毕的时候,浏览器会继续DOM构建
B、js可能会操作CSSOM,所以浏览器未先将CSSOM构建完毕,那么js就会暂停执行,同时DOM构建也会暂停
以上说法可以总结为一下几点:
1、脚本在文档中的位置相当重要,因为其根html css有很强的依赖关系
2、在HTML解析器解析到script标签后,会停止DOM构建
3、js可以操作DOM CSSOM,但进行这些行为的时候要确保相应的DOM和CSSOM已经存在
4、js执行将暂停,直至CSSOM就绪
总的来说,js在DOM CSSOM之间存在大量的依赖关系,根本目的是为了有序的高效的渲染页面,要达到这一目的,则需要明确依赖关系。
优化js 渲染阻塞的方法:(处理依赖关系的方法)
1、js放在</body>的前面
2、defer
3、async
4、避免长时间的运行js,若初始化必须则考虑适当的分割
3、font阻塞渲染
浏览器为了避免FOUT(flash of unstyled text),会尽量等待字体加载完成后,在显示应用该字体的内容。
只有当字体超过一段时间仍未加载成功时,浏览器才会降级使用系统字体,每个浏览器都规定了超时的时间
但存在内容无法尽快的被展示,导致空白。
可以参考异步加载字体库
4、关键资源路径
三大指标:
A、关键资源大小:优化请求时间、解析时间、渲染时间
B、网络请求来回数目-优化氢气时间
C、关键资源数目:优化解析时间、渲染时间
减少资源大小:
A、避免返回无用的内容
B、针对特定语言的源码压缩
C、通用文本压缩
D、图片压缩
减少请求来回时间
A、服务器优化
1、chunked encoding
2、尽早返回数据
3、服务端渲染
B、合理利用缓存
1、cache control
2、ETag
3、localstorage
4、service worker
C、优化网络
1、HTTP2
2、CDN
3、域名分割
4、减少重定向
5、resoure-hint
下面总结一下优化关键渲染路径的一般步骤
1、分析关键渲染路径中的资源的大小、来回、渲染顺序
2、最大限度删减关键资源数目,也就是尽量只渲染首屏必须的资源,其他的异步或延迟(async defer)
3、合并请求数目,减少请求往返次数,减少资源字节数(内联js、css)
4、优化加载渲染顺序,最大化利用浏览器渲染引擎和js引擎(调整资源DOM顺序)
四、性能优化
1、代码层面:避免使用css表达式、避免使用高级选择器、通配选择器等
2、缓存利用:缓存ajax,使用CDN,使用外部的js css文件以便缓存,添加Espires头,服务端配置Etag,减少DNS查找
3、请求数量:合并样式和脚本,使用css图片精灵,初始首屏之外的图片资源按需加载,静态资源延迟加载
4、请求宽带:压缩文件,开启GZI
1、代码层面的优化:
A、应hash-table来优化查找
B、少用全局变量
C、用innerHTML代替DOM操作,减少DOM操作次数,优化js性能
D、用setTimeout来避免页面失去响应
E、缓存DOM节点查找的结果
F、避免使用css表达式
G、避免全局查询
H、避免使用with(with会自己创建自己的作用域,会增加作用域长度)
I、多个变量声明合并
J、避免图片和iframe等的空src。空src会重新加载当前页面,影响速度和效率
K、尽量避免在HTML标签中的写style属性
2、移动端性能优化
A、尽量使用css3动画,开启硬件加速
B、适当使用touch事件代替click事件
C、避免使用css3的渐变和阴影效果
D、可以使用transform:translateZ(0)来开启硬件加速
E、不滥用float。float在渲染时的计算量比较大,尽量减少使用
F、不滥用web字体,web字体需要下载,解析,重绘当前页面,尽量减少使用
G、合理使用requestAnimationFrame动画 代替setTimeout
H、css中的属性(css3 transition、css3 3D transforms、opacity、canvas、webGL、video )会触发GPU渲染,请合理使用。过度使用会引发手机耗电增加
Etag的定义
当发送一个服务器请求的时候,浏览器首先会进行缓存过期判断,浏览器根据缓存过期时间判断缓存文件是否过期
场景一、如果没有过期,则不向服务器发送请求,直接使用缓存中的内容。此时在浏览器console中可以看到200 OK(from cache),此时的情况完全使用缓存,浏览 器和服务器没有任何交互。
场景二、如果已经过期,则向服务器发送请求,此时请求中会带上1中设置的文件的修改时间和Etag,然后进行资源更新判断,服务器根据浏览器传过来的文件的修 改时间,判断自浏览器上一次请求之后,文件是否有被修改过;根据Etag,判断文件内容自上一次请求之后,有没有发生变化
情形一、若两种判断的结论都是文件没有被修改过,则服务器就不给浏览器发index.html的内容,直接告诉他,文件没有被修改过,使用缓存--------304 not modifined,此时浏览器就会从本地缓存中获取index.html的内容。这种情况叫做协议缓存,浏览器和服务器之间有一次请求交互。
情形二、如果修改时间和文件内容判断有任意一个没有通过,则服务器会受理此次请求,之后的操作同1,1只有get请求会被缓存,post请求不会
ETag应用
ETag由服务器端生成,客户端通过if-match(if-none-match)这个条件判断请求来验证资源是否修改。常见的使用是if-none-match,请求一个文件的流程可能如 下:
--------第一次请求-------------
1、客户端发起HTTP GET 请求一个文件;
2、服务器处理请求,返回文件内容和一堆header,包括ETag(例如“2e681a-6-5d044840”)(假设服务器支持Etag生成和已经开启Etag),状态码200
-------第二次请求-------------
客户端发起HTTP GET请求一个文件,注意这个时候客户端同时发送一个if-none-match头,这个头的内容就是第一次请求时服务器返回的Etag:2e681a-6-5d044840
服务器判断发送过来的Etag和计算出来的Etag匹配,因此if-none-match为false,不返回200,返回304,客户端继续使用本地缓存
---------------------
如果服务器又设置了cache-control:max-age和Expires,那如何处理呢?
答案是同时使用,也就是说在完全匹配if-modifined-since和if-none-match,即检查修改时间和Etag之后,服务器才会返回304
-------------
使用Etag的目的是:Etag主要是解决Last-Modified无法解决的一些问题。
Expires和Cache-Control
Expires要求客户端和服务器的时钟严格同步。HTTP1.1引入Cache-Control来科夫Expires头的限制,如果max-age和Expires同时出现,则max-aage有更高的优先级
转载路径:http://blog.csdn.net/allenliu6/article/details/76609929