平时咱们几乎天天都在和浏览器打交道,在一些兼容任务比较繁重的团队里,苦逼的前端攻城师们甚至为了兼容各个浏览器而不断地去测试和调试,还要在脑子中记下各类遇到的 BUG 及解决方案。即使如此,咱们好像并无去主动地关注和了解下浏览器的工做原理。我想若是咱们对此作一点了解,在项目过程当中就能够有效地避免一些问题,并对页面性能作出相应的改进。html
“知己知彼,百战不殆”,如今咱们就一块儿来揭开浏览器渲染过程的神秘面纱!前端
浏览器的“心”,说的就是浏览器的内核。在研究浏览器微观的运行机制以前,咱们首先要对浏览器内核有一个宏观的把握。后端
开篇我提到许多工程师由于业务须要,免不了须要去处理不一样浏览器下代码渲染结果的差别性。这些差别性正是由于浏览器内核的不一样而致使的——浏览器内核决定了浏览器解释网页语法的方式。
浏览器内核能够分红两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并无十分明确的区分,但随着 JS 引擎愈来愈独立,内核也成了渲染引擎的代称(下文咱们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。浏览器
目前市面上常见的浏览器内核能够分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。bash
这里面你们最耳熟能详的可能就是 Webkit 内核了。不少同窗可能会据说过 Chrome 的内核就是 Webkit,却不知 Chrome 内核早已迭代为了 Blink。可是换汤不换药,Blink 其实也是基于 Webkit 衍生而来的一个分支,所以,Webkit 内核仍然是当下浏览器世界真正的霸主。网络
下面咱们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。异步
什么是渲染过程?简单来讲,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程(以下图)。async
从这个流程来看,浏览器呈现网页这个过程,宛如一个黑盒。在这个神秘的黑盒中,有许多功能模块,内核内部的实现正是这些功能模块相互配合协同工做进行的。其中咱们最须要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:ide
HTML 解释器:将 HTML 文档通过词法分析输出 DOM 树。布局
CSS 解释器:解析 CSS 文档, 生成样式规则。
图层布局计算模块:布局计算每一个对象的精确位置和大小。
视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。
JavaScript 引擎:编译执行 Javascript 代码。
有了对零部件的了解打底,咱们就能够一块儿来走一遍浏览器的渲染流程了。在浏览器里,每个页面的首次渲染都经历了以下阶段(图中箭头不表明串行,有一些操做是并行进行的,下文会说明):
在这一步浏览器执行了全部的加载解析逻辑,在解析 HTML 的过程当中发出了页面渲染所需的各类外部资源请求。
浏览器将识别并加载全部的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。
页面中全部元素的相对位置信息,大小等信息均在这一步获得计算。
在这一步中浏览器会根据咱们的 DOM 代码结果,把每个页面图层转换为像素,并对全部的媒体文件进行解码。
最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,咱们有时会手动区分不一样的图层)。
上面的内容没有理解透彻?别着急,咱们一块儿来捋一捋这个过程当中的重点——树!
为了使渲染过程更明晰一些,咱们须要给这些”树“们一个特写:
DOM 树:解析 HTML 以建立的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。
CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)建立的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。
渲染树:CSSOM 与 DOM 结合,以后咱们获得的就是渲染树(Render tree )。
布局渲染树:从根节点递归调用,计算每个元素的大小、位置等,给每一个节点所应该出如今屏幕上的精确坐标,咱们便获得了基于渲染树的布局渲染树(Layout of the render tree)。
绘制渲染树: 遍历渲染树,每一个节点将使用 UI 后端层来绘制。整个过程叫作绘制渲染树(Painting the render tree)。
基于这些“树”,咱们再梳理一番:
渲染过程说白了,首先是基于 HTML 构建一个 DOM 树,这棵 DOM 树与 CSS 解释器解析出的 CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,咱们页面的初次渲染就大功告成了。
以后每当一个新元素加入到这个 DOM 树当中,浏览器便会经过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,而后再从新去绘制它。
有心的同窗可能已经在思考了,查表是个花时间的活,我怎么让浏览器的查询工做又快又好地实现呢?OK,讲了这么多原理,咱们终于引出了咱们的第一个可转化为代码的优化点——CSS 样式表规则的优化!
在给出 CSS 选择器方面的优化建议以前,先告诉你们一个小知识:CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看以下规则:
#myList li {}
复制代码
这样的写法其实很常见。你们平时习惯了从左到右阅读的文字阅读方式,会本能地觉得浏览器也是从左到右匹配 CSS 选择器的,所以会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等因而缩小了范围后再去查找它后代中的 li 元素,没毛病。
事实上,CSS 选择符是从右到左进行匹配的。咱们这个看似“没毛病”的选择器,实际开销至关高:浏览器必须遍历页面上每一个 li 元素,而且每次都要去确认这个 li 元素的父元素 id 是否是 myList,你说坑不坑!
说到坑,不知道你们还记不记得这个经典的通配符:
* {}
复制代码
入门 CSS 的时候,很多同窗拿通配符清除默认样式(我曾经也是通配符用户的一员)。但这个家伙很恐怖,它会匹配全部元素,因此浏览器必须去遍历每个元素!你们低头看看本身页面里的元素个数,是否是心凉了——这得计算多少次呀!
这样一看,一个小小的 CSS 选择器,也有很多的门道!好的 CSS 选择器书写习惯,能够为咱们带来很是可观的性能提高。根据上面的分析,咱们至少能够总结出以下性能提高的方案:
避免使用通配符,只对须要用到的元素进行选择。
关注能够经过继承实现的属性,避免重复匹配重复定义。
少用标签选择器。若是能够,用类选择器替代,举个🌰:
错误示范:
#myList li{}
复制代码
课表明:
.myList_li {}
复制代码
不要多此一举,id 和 class 选择器不该该被多余的标签选择器拖后腿。举个🌰:
错误示范
.myList#title
复制代码
课表明
#title
复制代码
减小嵌套。后代选择器的开销是最高的,所以咱们应该尽可能将选择器的深度降到最低(最高不要超过三层),尽量使用类来关联每个标签元素。
搞定了 CSS 选择器,万里长征才刚刚开始的第一步。但如今你已经理解了浏览器的工做过程,接下来的征程对你来讲并再也不是什么难题~
说完了过程,咱们来讲一说特性。
HTML、CSS 和 JS,都具备阻塞渲染的特性。
HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。
那么 CSS 和 JS 的阻塞又是怎么回事呢?
在刚刚的过程当中,咱们提到 DOM 和 CSSOM 协力才能构建渲染树。这一点会给性能形成严重影响:默认状况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程当中,不会渲染任何已处理的内容。即使 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了不没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。
咱们知道,只有当咱们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。不少时候,DOM 不得不等待 CSSOM。所以咱们能够这样总结:
CSS 是阻塞渲染的资源。须要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
事实上,如今不少团队都已经作到了尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)。这个“把 CSS 往前放”的动做,对不少同窗来讲已经内化为一种编码习惯。那么如今咱们还应该知道,这个“习惯”不是空穴来风,它是由 CSS 的特性决定的。
不知道你们注意到没有,前面咱们说过程的时候,花了不少笔墨去说 HTML、说 CSS。相比之下,JS 的出镜率也过低了点。
这固然不是由于 JS 不重要。而是由于,在首次渲染过程当中,JS 并非一个非登场不可的角色——没有 JS,CSSOM 和 DOM 照样能够组成渲染树,页面依然会呈现——即便它死气沉沉、毫无交互。
JS 的做用在于修改,它帮助咱们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。所以 JS 的执行会阻止 CSSOM,在咱们不做显式声明的状况下,它也会阻塞 DOM。
咱们经过一个🌰来理解一下这个机制:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS阻塞测试</title>
<style>
#container {
background-color: yellow;
width: 100px;
height: 100px;
}
</style>
<script>
// 尝试获取container元素
var container = document.getElementById("container")
console.log('container', container)
</script>
</head>
<body>
<div id="container"></div>
<script>
// 尝试获取container元素
var container = document.getElementById("container")
console.log('container', container)
// 输出container元素此刻的背景色
console.log('container bgColor', getComputedStyle(container).backgroundColor)
</script>
<style>
#container {
background-color: blue;
}
</style>
</body>
</html>
复制代码
三个 console 的结果分别为:
注:本例仅使用了内联 JS 作测试。感兴趣的同窗能够把这部分 JS 当作外部文件引入看看效果——它们的表现一致。
第一次尝试获取 id 为 container 的 DOM 失败,这说明 JS 执行时阻塞了 DOM,后续的 DOM 没法构建;第二次才成功,这说明脚本块只能找到在它前面构建好的元素。这二者结合起来,“阻塞 DOM”获得了验证。再看第三个 console,尝试获取 CSS 样式,获取到的是在 JS 代码执行前的背景色(yellow),而非后续设定的新样式(blue),说明 CSSOM 也被阻塞了。那么在阻塞的背后,到底发生了什么呢?
咱们前面说过,JS 引擎是独立于渲染引擎存在的。咱们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 所以与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。
如今理解了阻塞的表现与原理,咱们开始思考一个问题。浏览器之因此让 JS 阻塞其它的活动,是由于它不知道 JS 会作什么改变,担忧若是不阻止后续的操做,会形成混乱。可是咱们是写 JS 的人,咱们知道 JS 会作什么改变。假如咱们能够确认一个 JS 文件的执行时机并不必定非要是此时此刻,咱们就能够经过对它使用 defer 和 async 来避免没必要要的阻塞,这里咱们就引出了外部 JS 的三种加载方式。
正常模式:
<script src="index.js"></script>
复制代码
这种状况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去作其它事情。
async 模式:
<script async src="index.js"></script>
复制代码
async 模式下,JS 不会阻塞浏览器作任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会当即执行。
defer 模式:
<script defer src="index.js"></script>
复制代码
defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。
从应用的角度来讲,通常当咱们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,咱们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,咱们会选用 defer。
经过审时度势地向 script 标签添加 async/defer,咱们就能够告诉浏览器在等待脚本可用期间不阻止其它的工做,这样能够显著提高性能。
咱们知道,当 JS 登场时,每每意味着对 DOM 的操做。DOM 操做所致使的性能开销的“昂贵”,你们可能早就有所耳闻,雅虎军规里很重要的一条就是“尽可能减小 DOM 访问”。
那么 DOM 到底为何慢,咱们如何去规避这种慢呢?这里咱们就引出了须要重点解释的两个概念:CSS 中的回流(Reflow)与重绘(Repaint)。