你们都知道万维网的应用层使用了HTTP
协议,而且用浏览器做为入口访问网络上的资源。用户在使用浏览器访问一个网站时须要先经过HTTP
协议向服务器发送请求,以后服务器返回HTML
文件与响应信息。这时,浏览器会根据HTML
文件来进行解析与渲染(该阶段还包括向服务器请求非内联的CSS
文件与JavaScript
文件或者其余资源),最终再将页面呈如今用户面前。javascript
如今知道了网页的渲染都是由浏览器完成的,那么若是一个网站的页面加载速度太慢会致使用户体验不够友好,本文经过详解浏览器渲染页面的过程来引入一些基本的浏览器性能优化方案。让浏览器更快地渲染你的网页并快速响应从而提升用户体验。css
本文做者为: SylvanasSun(sylvanas.sun@gmail.com).转载请务必将下面这段话置于文章开头处(保留超连接).
本文首发自SylvanasSun Blog,原文连接: sylvanassun.github.io/2017/10/03/…html
浏览器接收到服务器返回的HTML
、CSS
和JavaScript
字节数据并对其进行解析和转变成像素的渲染过程被称为关键渲染路径。经过优化关键渲染路径便可以缩短浏览器渲染页面的时间。前端
浏览器在渲染页面前须要先构建出DOM
树与CSSOM
树(若是没有DOM
树和CSSOM
树就没法肯定页面的结构与样式,因此这两项是必须先构建出来的)。java
DOM
树全称为Document Object Model
文档对象模型,它是HTML
和XML
文档的编程接口,提供了对文档的结构化表示,并定义了一种可使程序对该结构进行访问的方式(好比JavaScript
就是经过DOM
来操做结构、样式和内容)。DOM
将文档解析为一个由节点和对象组成的集合,能够说一个WEB
页面其实就是一个DOM
。git
CSSOM
树全称为Cascading Style Sheets Object Model
层叠样式表对象模型,它与DOM
树的含义相差不大,只不过它是CSS
的对象集合。程序员
浏览器从网络或硬盘中得到HTML
字节数据后会通过一个流程将字节解析为DOM
树:github
编码: 先将HTML
的原始字节数据转换为文件指定编码的字符。web
令牌化: 而后浏览器会根据HTML
规范来将字符串转换成各类令牌(如<html>
、<body>
这样的标签以及标签中的字符串和属性等都会被转化为令牌,每一个令牌具备特殊含义和一组规则)。令牌记录了标签的开始与结束,经过这个特性能够轻松判断一个标签是否为子标签(假设有<html>
与<body>
两个标签,当<html>
标签的令牌还未遇到它的结束令牌</html>
就碰见了<body>
标签令牌,那么<body>
就是<html>
的子标签)。算法
生成对象: 接下来每一个令牌都会被转换成定义其属性和规则的对象(这个对象就是节点对象)。
构建完毕: DOM
树构建完成,整个对象集合就像是一棵树形结构。可能有人会疑惑为何DOM
是一个树形结构,这是由于标签之间含有复杂的父子关系,树形结构正好能够诠释这个关系(CSSOS
同理,层叠样式也含有父子关系。例如: div p {font-size: 18px}
,会先寻找全部p
标签并判断它的父标签是否为div
以后才会决定要不要采用这个样式进行渲染)。
整个DOM
树的构建过程其实就是: 字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,下面将经过一个示例HTML
代码与配图更形象地解释这个过程。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>复制代码
当上述HTML
代码碰见<link>
标签时,浏览器会发送请求得到该标签中标记的CSS
文件(使用内联CSS
能够省略请求的步骤提升速度,但没有必要为了这点速度而丢失了模块化与可维护性),style.css
中的内容以下:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }复制代码
浏览器得到外部CSS
文件的数据后,就会像构建DOM
树同样开始构建CSSOM
树,这个过程没有什么特别的差异。
若是想要更详细地去体验一下关键渲染路径的构建,可使用Chrome
开发者工具中的Timeline
功能,它记录了浏览器从请求页面资源一直到渲染的各类操做过程,甚至还能够录制某一时间段的过程(建议不要去看太大的网站,信息会比较杂乱)。
在构建了DOM
树和CSSOM
树以后,浏览器只是拥有了两个互相独立的对象集合,DOM
树描述了文档的结构与内容,CSSOM
树则描述了对文档应用的样式规则,想要渲染出页面,就须要将DOM
树与CSSOM
树结合在一块儿,这就是渲染树。
浏览器会先从DOM
树的根节点开始遍历每一个可见节点(不可见的节点天然就不必渲染到页面了,不可见的节点还包括被CSS
设置了display: none
属性的节点,值得注意的是visibility: hidden
属性并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,因此它会被渲染成一个空框)。
对每一个可见节点,找到其适配的CSS
样式规则并应用。
渲染树构建完成,每一个节点都是可见节点而且都含有其内容和对应规则的样式。
渲染树构建完毕后,浏览器获得了每一个可见节点的内容与其样式,下一步工做则须要计算每一个节点在窗口内的确切位置与大小,也就是布局阶段。
CSS
采用了一种叫作盒子模型的思惟模型来表示每一个节点与其余元素之间的距离,盒子模型包括外边距(Margin
),内边距(Padding
),边框(Border
),内容(Content
)。页面中的每一个标签其实都是一个个盒子。
布局阶段会从渲染树的根节点开始遍历,而后肯定每一个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每一个元素在屏幕内的确切位置与大小,全部相对的测量值也都会被转换为屏幕内的绝对像素值。
<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>复制代码
当Layout
布局事件完成后,浏览器会当即发出Paint Setup
与Paint
事件,开始将渲染树绘制成像素,绘制所需的时间跟CSS
样式的复杂度成正比,绘制完成后,用户就能够看到页面的最终呈现效果了。
咱们对一个网页发送请求并得到渲染后的页面可能也就通过了1~2秒,但浏览器其实已经作了上述所讲的很是多的工做,总结一下浏览器关键渲染路径的整个过程:
处理HTML
标记数据并生成DOM
树。
处理CSS
标记数据并生成CSSOM
树。
将DOM
树与CSSOM
树合并在一块儿生成渲染树。
遍历渲染树开始布局,计算每一个节点的位置信息。
将每一个节点绘制到屏幕。
浏览器想要渲染一个页面就必须先构建出DOM
树与CSSOM
树,若是HTML
与CSS
文件结构很是庞大与复杂,这显然会给页面加载速度带来严重影响。
所谓渲染阻塞资源,便是对该资源发送请求后还须要先构建对应的DOM
树或CSSOM
树,这种行为显然会延迟渲染操做的开始时间。HTML
、CSS
、JavaScript
都是会对渲染产生阻塞的资源,HTML
是必需的(没有DOM
还谈何渲染),但还能够从CSS
与JavaScript
着手优化,尽量地减小阻塞的产生。
若是可让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文件
当浏览器的HTML
解析器遇到一个script
标记时会暂停构建DOM
,而后将控制权移交至JavaScript
引擎,这时引擎会开始执行JavaScript
脚本,直到执行结束后,浏览器才会从以前中断的地方恢复,而后继续构建DOM
。每次去执行JavaScript
脚本都会严重地阻塞DOM
树的构建,若是JavaScript
脚本还操做了CSSOM
,而正好这个CSSOM
尚未下载和构建,浏览器甚至会延迟脚本执行和构建DOM
,直至完成其CSSOM
的下载和构建。显而易见,若是对JavaScript
的执行位置运用不当,这将会严重影响渲染的速度。
下面代码中的JavaScript
脚本并不会生效,这是由于DOM
树尚未构建到<p>
标签时,JavaScript
脚本就已经开始执行了。这也是为何常常有人在HTML
文件的最下方写内联JavaScript
代码,又或者使用window.onload()
和JQuery
中的$(function(){})
(这两个函数有一些区别,window.onload()
是等待页面彻底加载完毕后触发的事件,而$(function(){})
在DOM
树构建完毕后就会执行)。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Hello,World</title>
<script type="text/javascript"> var p = document.getElementsByTagName('p')[0]; p.textContent = 'SylvanasSun'; </script>
</head>
<body>
<p>Hello,World!</p>
</body>
</html>复制代码
使用async
能够通知浏览器该脚本不须要在引用位置执行,这样浏览器就能够继续构建DOM
,JavaScript
脚本会在就绪后开始执行,这样将显著提高页面首次加载的性能(async
只能够在src
标签中使用也就是外部引用的JavaScript
文件)。
<!-- 下面2个用法效果是等价的 -->
<script type="text/javascript" src="demo_async.js" async="async"></script>
<script type="text/javascript" src="demo_async.js" async></script>复制代码
上文已经完整讲述了浏览器是如何渲染页面的以及渲染以前的准备工做,接下来咱们如下面的案例来总结一下优化关键渲染路径的方法。
假设有一个HTML
页面,它只引入了一个CSS
外部文件:
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>复制代码
它的关键渲染路径以下:
首先浏览器要先对服务器发送请求得到HTML
文件,获得HTML
文件后开始构建DOM
树,在碰见<link>
标签时浏览器须要向服务器再次发出请求来得到CSS
文件,而后则是继续构建DOM
树和CSSOM
树,浏览器合并出渲染树,根据渲染树进行布局计算,执行绘制操做,页面渲染完成。
有如下几个用于描述关键渲染路径性能的词汇:
关键资源:可能阻塞网页首次渲染的资源(上图中为2个,HTML
文件与外部CSS
文件style.css
)。
关键路径长度: 获取关键资源所需的往返次数或总时间(上图为2次或以上,一次获取HTML
文件,一次获取CSS
文件,这个次数基于TCP
协议的最大拥塞窗口,一个文件不必定能在一次链接内传输完毕)。
关键字节:全部关键资源文件大小的总和(上图为9KB
)。
接下来,案例代码的需求发生了变化,它新增了一个JavaScript
文件。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
</html>复制代码
JavaScript
文件阻塞了DOM
树的构建,而且在执行JavaScript
脚本时还须要先等待构建CSSOM
树,上图的关键渲染路径特性以下:
关键资源: 3(HTML
、style.css
、app.js
)
关键路径长度: 2或以上(浏览器会在一次链接中一块儿下载style.css
和app.js
)
关键字节:11KB
如今,咱们要优化关键渲染路径,首先将<script>
标签添加异步属性async
,这样浏览器的HTML
解析器就不会阻塞这个JavaScript
文件了。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js" async></script>
</body>
</html>复制代码
关键资源:2(app.js
为异步加载,不会成为阻塞渲染的资源)
关键路径长度: 2或以上
关键字节: 9KB(app.js
再也不是关键资源,因此没有算上它的大小)
接下来对CSS
进行优化,好比添加上媒体查询。
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.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" async></script>
</body>
</html>复制代码
关键资源:1(app.js
为异步加载,style.css
只有在打印时才会使用,因此只剩下HTML
一个关键资源,也就是说当DOM
树构建完毕,浏览器就会开始进行渲染)
关键路径长度:1或以上
关键字节:5KB
优化关键渲染路径就是在对关键资源、关键路径长度和关键字节进行优化。关键资源越少,浏览器在渲染前的准备工做就越少;一样,关键路径长度和关键字节关系到浏览器下载资源的效率,它们越少,浏览器下载资源的速度就越快。
除了异步加载JavaScript
和使用媒体查询外还有不少其余的优化方案可使页面的首次加载变得更快,这些方案能够综合起来使用,但核心的思想仍是针对关键渲染路径进行了优化。
服务端在接收到请求时先只响应回HTML
的初始部分,后续的HTML
内容在须要时再经过AJAX
得到。因为服务端只发送了部分HTML
文件,这让构建DOM
树的工做量减小不少,从而让用户感受页面的加载速度很快。
注意,这个方法不能用在CSS
上,浏览器不容许CSSOM
只构建初始部分,不然会没法肯定具体的样式。
经过对外部资源进行压缩能够大幅度地减小浏览器须要下载的资源量,它会减小关键路径长度与关键字节,使页面的加载速度变得更快。
对数据进行压缩其实就是使用更少的位数来对数据进行重编码。现在有很是多的压缩算法,且每个的做用领域也各不相同,它们的复杂度也不相同,不过在这里我不会讲压缩算法的细节,感兴趣的朋友能够本身Google。
在对HTML
、CSS
和JavaScript
这些文件进行压缩以前,还须要先进行一次冗余压缩。所谓冗余压缩,就是去除多余的字符,例如注释、空格符和换行符。这些字符对于程序员是有用的,毕竟没有格式化的代码可读性是很是恐怖的,但它们对于浏览器是没有任何意义的,去除这些冗余能够减小文件的数据量。在进行完冗余压缩以后,再使用压缩算法进一步对数据自己进行压缩,例如GZIP
(GZIP
是一个能够做用于任何字节流的通用压缩算法,它会记忆以前已经看到的内容,而后再尝试查找并替换重复的内容。)。
经过网络来获取资源一般是缓慢的,若是资源文件过于膨大,浏览器还须要与服务器之间进行屡次往返通讯才能得到完整的资源文件。缓存能够复用以前获取的资源,既而后端可使用缓存来减小访问数据库的开销,那前端天然也可使用缓存来复用资源文件。
浏览器自带了HTTP
缓存的功能,只须要确保每一个服务器响应的头部都包含了如下的属性:
ETag: ETag是一个传递验证令牌,它对资源的更新进行检查,若是资源未发生变化时不会传送任何数据。当浏览器发送一个请求时,会把ETag一块儿发送到服务器,服务器会根据当前资源核对令牌(ETag一般是对内容进行Hash
后得出的一个指纹),若是资源未发生变化,服务器将返回304 Not Modified
响应,这时浏览器没必要再次下载资源,而是继续复用缓存。
Cache-Control: Cache-Control定义了缓存的策略,它规定在什么条件下能够缓存响应以及能够缓存多久。
no-cache: no-cache表示必须先与服务器确认返回的响应是否发生了变化,而后才能使用该响应来知足后续对同一网址的请求(每次都会根据ETag对服务器发送请求来确认变化,若是未发生变化,浏览器不会下载资源)。
no-store: no-store直接禁止浏览器以及全部中间缓存存储任何版本的返回响应。简单的说,该策略会禁止任何缓存,每次发送请求时,都会完整地下载服务器的响应。
public&private: 若是响应被标记为public,则即便它有关联的HTTP
身份验证,甚至响应状态代码一般没法缓存,浏览器也能够缓存响应。若是响应被标记为private,那么这个响应一般只为单个用户缓存,所以不容许任何中间缓存(CDN)对其进行缓存,private通常用在缓存用户私人信息页面。
max-age: max-age定义了从请求时间开始,缓存的最长时间,单位为秒。
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">复制代码