用户在使用浏览器访问一个网站时需要先通过HTTP
协议向服务器发送请求,之后服务器返回HTML
文件与响应信息。这时,浏览器会根据HTML
文件来进行解析与渲染(该阶段还包括向服务器请求非内联的CSS
文件与JavaScript
文件或者其他资源),最终再将页面呈现在用户面前。
其中网页的渲染都是由浏览器完成的,那么如果一个网站的页面加载速度太慢会导致用户体验不够友好,通过详解浏览器渲染页面的过程来引入一些基本的浏览器性能优化方案,让浏览器更快地渲染你的网页并快速响应从而提高用户体验。
关键渲染路径
浏览器接收到服务器返回的HTML
、CSS
和Javascript
字节数据并对其进行解析和转变成像素的渲染过程称为关键渲染路径。包括以下步骤:
- 解析HTML,生成DOM树(DOM)
- 解析CSS,生成CSSOM树(CSSOM)
- 将DOM和CSSOM合并,生成渲染树(Render-Tree)
- 计算渲染树的布局(Layout)
- 将布局渲染到屏幕上(Paint)
通过优化关键渲染路径即可以缩短浏览器渲染页面的时间。
构建DOM树与CSSOM树
浏览器在渲染页面前需要先构建出DOM
树和CSSOM
树(如果没有DOM树和CSSOM树就无法确定页面的结构与样式,所以这两项是必须先构建出来的)。
DOM
树
全称为Document Object Model
文档对象模型,它是HTML
和XML
文档的编程接口,提供了对文档的结构化表示,并定义了一种可以使程序对该结构进行访问的方式。
比如JavaScript
就是通过DOM
来操作结构、样式和内容。DOM
将文档解析为一个由节点和对象组成的集合,可以说一个WEB
页面其实就是一个DOM
。
DOM
构建过程
浏览器从网络或者硬盘中获取HTML
字节数据后会经过一个流程将字节解析成DOM
树,流程如下:
编码:先将
HTML
的原始字节数据转换为文件指定编码的字符;-
令牌化:然后浏览器会根据
HTML
规范来将字符串转换成各种令牌;如
<html>
、<body>
这样的标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义的一组规则。令牌记录了标签的开始与结束,通过这个特性可以轻松判断一个标签是否为子标签(假设有
<html>
与<body>
两个标签,当<html>
标签的令牌还没遇到它的结束令牌</html>
就遇见了<body>
标签令牌,那么<body>
就是<html>
的子标签)。 生成对象:接下来每个令牌都会被转换成定义其属性和规则的对象,这个对象就是节点对象;
-
构建完成:
DOM
树构建完成,整个对象集合就像是一棵树形结构。为什么
DOM
是一个树形结构?这是因为标签之间含有复杂的父子关系,树形结构正好可以诠释这个关系,同理CSSOM
也是树形结构,层叠样式也含有父子关系。例如:div p { font-size: 18px }
会先寻找所有
p
标签并判断它的父标签是否为div
之后才会决定要不要采用这个样式进行渲染。
整个DOM
树的构建过程其实就是:
字节->字符->令牌->节点对象->对象模型
在解析DOM
过程中,会碰到几类特殊的节点需要特殊处理:
<style>
、<link>
标签以及具有内联样式的标签,交给CSSOM
生成;-
<script>
标签。Javascript
可以操作修改DOM
结构,可以操作CSSOM
修改节点样式,就会导致了浏览器在解析DOM
时候,一碰到<script>
就会停止DOM
的解析(CSS
不会),执行完Javascript
再返还控制权。事实上,
Javascript
执行前不仅仅是停止了DOM
的解析,它还必须等待CSS
的解析完成。当浏览器碰到<script>
标签时,发现该元素前面的CSS
还未解析完成,就会等它解析完成再去执行。Javascript
阻塞了DOM
的解析,也阻塞了其后的CSS
解析,整个解析进程必须等待Javascript
的完成才能够继续,这就是JS阻塞页面。一个<script>
标签推迟了DOM
、CSSOM
的生成以及以后的所有渲染过程,从性能角度上看,将<script>
放在页面底部,也就合情合理了。
CSSOM
树
全称为Cascading Style Sheets Object Model
层叠样式表对象模型,它与DOM
树的含义相差不大,只不过他是CSS
的对象集合。
CSSOM
和DOM
是两个独立的数据结构。
浏览器解析DOM
的时候,遇到了<style>
和内联样式时候,会根据样式的声明生成CSSOM
,因为他们本身含有样式内容。
而遇到了<link>
标签时,浏览器会首先发送请求,待请求成功获取外联样式后,便会解析该外联样式,并就会像生成DOM
树一样生成相应的CSSOM
树。
由于CSSOM
负责储存渲染信息,浏览器就必须保证再合成渲染树之前,CSSOM
是完备的,这种完备是指所有的CSS
(内联、内部、外部)都已经下载完,并解析完,只有DOM
和CSSOM
解析完全结束,浏览器才会进入下一步的渲染,这就是传说中的CSS
阻塞渲染。
CSS
阻塞渲染意味着,在CSSOM
完备前,页面将一直处理白屏状态,这就是为什么样式放在<head>
标签中,仅仅是为了更快的解析CSS
,保证更快的首次渲染。
需要注意的是,即使没有编写任何样式声明,
CSSOM
依然会生成,默认生成的CSSOM
是浏览器自带默认样式。
构建渲染树
在构建了DOM
和CSSOM
树之后,浏览器只是拥有了两个独立的对象集合,DOM
树描述了文档的结构与内容,CSSOM
树描述了对文档应用的样式规则,想要渲染出页面,就需要将DOM
树和CSSOM
树结合在一起,生成渲染树。这颗树就包含了页面所有可见元素及其渲染信息。
- 浏览器会先从
DOM
树的根节点开始遍历每个可见节点;<script>
、<link>
、<meta>
都属于不可视节点,另外,display: none
的节点也属于不可视节点,没必要渲染在页面上,值得注意的是visibility: hidden
属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着局部空间,所以会被渲染成一个空框。 - 从
CSSOM
中适配可视节点的样式规则; - 计算这些样式,将计算值应用到可视节点上;
- 渲染树构建完成,每个节点都是可见节点并且都含有其内容和对应规则的样式。
计算布局
渲染树构建完毕后,浏览器得到了每个可视节点的内容与其样式,下一步则需要计算每个节点在窗口内的确切位置与大小, 也就是布局阶段。
CSS
采用了一种叫做盒子模型的思维模型来表示每个节点与其他元素之间的距离,盒子模型包括外边距Margin
、内边距Padding
、边框Border
、内容Content
。页面中的每个标签其实都是一个个盒子。
布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对的测量值也都会被转换为屏幕内的绝对像素值。
渲染
得到了渲染树及其节点的布局信息,浏览器便可以将最终的页面渲染到屏幕。
渲染阻塞的优化方案
浏览器想要渲染一个页面就必须先构建出DOM
树和CSSOM
树,如果HTML
和CSS
文件结构非常庞大与复杂,这显然会给页面加载速度带来严重影响。
所谓渲染阻塞资源,即是对该资源发送请求后还需要先构建对应的DOM
树或CSSOM
树,这种行为显然会延迟渲染操作的开始时间。HTML
、CSS
、JavaScript
都是会对渲染产生阻塞的资源,HTML
是必需的,但还可以从CSS
与JavaScript
着手优化,尽可能地减少阻塞的产生。
优化CSS
如果可以让CSS
资源只在特定的条件下使用,可以在首次加载时先不进行构建CSSOM
树,只有在特定条件下,才会让浏览器进行阻塞渲染然后构建CSSOM
。
比如:
CSS
的媒体查询用来实现某些功能或者场景的,它由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。
<!-- 没有使用媒体查询,这个css资源会阻塞渲染 -->
<link href="style.css" rel="stylesheet">
<!-- all是默认类型,它和不设置媒体查询的效果是一样的 -->
<link href="style.css" rel="stylesheet" media="all">
<!-- 动态媒体查询, 将在网页加载时计算。
根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。-->
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<!-- 只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。 -->
<link href="print.css" rel="stylesheet" media="print">
使用媒体查询可以让
CSS
资源不在首次加载中阻塞渲染,但不管是哪种CSS
资源它们的下载请求都不会被忽略,浏览器仍然会先下载CSS
文件
优化Javascript
当浏览器的HTML
解析器遇到一个<script>
标记时会暂停构建DOM
,然后将控制权移交至JavaScript
引擎,这时引擎会开始执行JavaScript
脚本,直到执行结束后,浏览器才会从之前中断的地方恢复,然后继续构建DOM
。每次去执行JavaScript
脚本都会严重地阻塞DOM
树的构建,如果JavaScript
脚本还操作了CSSOM
,而正好这个CSSOM
还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM
,直至完成其CSSOM
的下载和构建。
使用async
可以通知浏览器该脚本不需要在引用位置执行,这样浏览器就可以继续构建DOM
,JavaScript
脚本会在就绪后开始执行,这样将显著提升页面首次加载的性能。
<!-- 下面2个用法效果是等价的 -->
<script type="text/javascript" src="demo_async.js" async="async"></script>
<script type="text/javascript" src="demo_async.js" async></script>
优化关键渲染路径
优化关键渲染路径就是在对关键资源、关键路径长度和关键字节进行优化。关键资源越少,浏览器在渲染前的准备工作就越少;同样,关键路径长度和关键字节关系到浏览器下载资源的效率,它们越少,浏览器下载资源的速度就越快。
加载部分HTML
服务端在接收到请求时先只响应回HTML
的初始部分,后续的HTML
内容在需要时再通过AJAX
获得。由于服务端只发送了部分HTML
文件,这让构建DOM
树的工作量减少很多,从而让用户感觉页面的加载速度很快。
注意,这个方法不能用在
CSS
上,浏览器不允许CSSOM
只构建初始部分,否则会无法确定具体的样式。
压缩
通过对外部资源进行压缩可以大幅度地减少浏览器需要下载的资源量,它会减少关键路径长度与关键字节,使页面的加载速度变得更快。
对数据进行压缩其实就是使用更少的位数来对数据进行重编码。
在对HTML
、CSS
和JavaScript
这些文件进行压缩之前,还需要先进行一次冗余压缩。所谓冗余压缩,就是去除多余的字符,例如注释、空格符和换行符。这些字符对于程序员是有用的,毕竟没有格式化的代码可读性是非常恐怖的,但它们对于浏览器是没有任何意义的,去除这些冗余可以减少文件的数据量。在进行完冗余压缩之后,再使用压缩算法进一步对数据本身进行压缩,例如GZIP
(GZIP
是一个可以作用于任何字节流的通用压缩算法,它会记忆之前已经看到的内容,然后再尝试查找并替换重复的内容。)。
HTTP缓存
通过网络来获取资源通常是缓慢的,如果资源文件过于膨大,浏览器还需要与服务器之间进行多次往返通信才能获得完整的资源文件。缓存可以复用之前获取的资源,既然后端可以使用缓存来减少访问数据库的开销,那前端自然也可以使用缓存来复用资源文件。
资源预加载
Pre-fetching
是一种提示浏览器预先加载用户之后可能会使用到的资源的方法。
使用dns-prefetch
来提前进行DNS
解析,以便之后可以快速地访问另一个主机名(浏览器会在加载网页时对网页中的域名进行解析缓存,这样你在之后的访问时无需进行额外的DNS
解析,减少了用户等待时间,提高了页面加载速度)。
<link rel="dns-prefetch" href="other.hostname.com">
使用prefetch
属性可以预先下载资源,不过它的优先级是最低的。
<link rel="prefetch" href="/some_other_resource.jpeg">
Chrome
允许使用subresource
属性指定优先级最高的下载资源(当所有属性为subresource
的资源下载完完毕后,才会开始下载属性为prefetch
的资源)。
<link rel="subresource" href="/some_other_resource.js">
prerender
可以预先渲染好页面并隐藏起来,之后打开这个页面会跳过渲染阶段直接呈现在用户面前(推荐对用户接下来必须访问的页面进行预渲染,否则得不偿失)。
<link rel="prerender" href="//domain.com/next_page.html">
参考文献
Web Fundamentals | Google Developers