原文发表于知乎专栏cloudjfed 《chrome浏览器页面渲染工做原理浅析》css
本篇文章基于本身对chrome浏览器工做原理的一些理解,重点在于页面渲染的分析。此文会随着我理解的深刻持续更新,如有错误,欢迎随时指正!比心 (。♥‿♥。)html
参考资料重点来源于:前端
文章目录:node
对于前端同窗来讲,webkit这个名词很是熟悉了,那么当咱们在说chrome是Webkit内核的时候,咱们到底在说什么?git
浏览器有一个重要的模块,它主要的做用是将页面变成可视(听)化的图形、音频结果,这就是浏览器内核。不一样浏览器有不一样内核,经常使用的有Trident(IE)、Gecko(Firefox)、Blink(Chrome)、Webkit(Safari)github
浏览器内核又能够分红两部分:渲染引擎和 JS 引擎。web
最开始渲染引擎和 JS 引擎并无区分的很明确,后来 JS 引擎愈来愈独立,内核就倾向于只指渲染引擎。ajax
一说到Webkit,最早想起来的可能就是chrome。但其实Webkit最先是苹果公司的一个开源项目。算法
苹果同窗哭瞎了眼。chrome
Webkit项目的结构以下
(图片来自《WebKit技术内幕》第三章)
从图中能够看到,Webkit主要由WebCore(渲染引擎)、JavaScriptCore(JavaScript引擎)和Webkit Port(不一样浏览器本身实现的移植部分)构成。
整个项目称为Webkit,而咱们前端开发者在谈到Webkit的时候,每每指的是WebCore,即渲染引擎部分。
当咱们打开一个页面的时候,网页的渲染流程以下:
(图片来自《WebKit技术内幕》第一章)
图中DOM和js引擎的双向箭头,指的是dom和js引擎的桥接接口,用于调用对方的一些方法。
那为何咱们提到Webkit的时候,每每会和chrome联系在一块儿呢?
2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium。
谷歌公司研发了本身的JavaScript引擎,v8, 但fork 了开源引擎 Webkit。后来谷歌公司在 Chromium 项目中研发 Blink 渲染引擎,也就是说,最初Blink 是从Webkit复制过来,没有太大区别,但谷歌逐渐进行了优化,并慢慢将其中于chromium 不相关的代码进行移除。因此能够预见的是,之后二者差距会越来越大。
对此,我采访了一下苹果公司,他们表示:
可能须要给他们寄一箱原谅套餐== 固然是假的!毕竟我又不是隔壁老王!
渲染引擎顾名思义,负责渲染,它将网络或者本地获取的网页和资源从字节流进行解释,呈现出来,流程以下图:
从图中能够看到,渲染引擎具体作了:
1. 用HTML 解释器 将字节流解释成DOM树
HTML解释器的工做以下:
(图片来自《Webkit技术内幕》第五章)
解释器进行分词后,生成节点,并从节点生成DOM树。
那如何从“并列”的节点,生成具备层次结构的树呢?
解释器在构建节点属性的时候,使用了栈结构,即这样一个代码片断<div><p><span></span></p></div>
,当解释到span
时,此时栈中元素就是 div、p、span
,当解释到</span>
时,span
出栈,遇到</p> p
出栈,以此类推。
固然,HTML解释器在工做时颇有可能遇到全局的js代码!我知道此刻你要说,那就停下来执行js代码啊!
事实上,解释器确实是停下来了,但并不会立刻执行js代码,浏览器的预扫描和预加载机制会先扫描后面的词语,若是发现有资源,那就会请求并发下载资源,而后,再执行js代码。
详细可参考:HTML5解析算法
2. CSS解释器:把css字符串解释后生成style rules
3. RenderObject 树
Webkit检查DOM树中每一个DOM节点,判断是否生成RenderObject对象,由于有些节点是不可见的,好比 style head 或者 display为none的节点(如今你知道为啥display:none和visibility:hidden为何表现不同了吧)。RenderObject对象叠加了2中相应的css属性。
4. 布局(Layout)
此时的RenderObject 树,并不包含位置和大小信息。Webkit根据模型来进行递归的布局计算。因此当样式发生变化时,就须要从新计算布局,这很耗费性能,更糟糕的是,一旦重排就要重绘了!
5. 绘制(Paint)
布局完,终于能够调用方法进行绘制了!
而咱们常说的重绘(repaint),就是当这些元素的颜色、背景等发生变化时,须要进行的。
6. 复合图层化(Composite)
事实上,网页是有层次结构的,基于RenderObject树,创建了 RenderLayer树,每一个节点都是RenderLayer节点,一个RenderLayer节点上有n个RenderObject。
什么是RenderLayer呢? 举个栗子:好比有透明效果的RenderObject节点和使用Canvas(或WebGL技术)的RenderObject节点都须要新建一个RenderLayer。
事实上,能够从chrome的开发面板中看到这些层,下图是淘宝页面的RenderLayer:
最后,浏览器使用GPU对这些层合成!
咱们都知道js是单线程的。为何呢?js设计之初是为了进行简单的表单验证,操做DOM,与用户进行互动。如果多线程操做,则极有可能出现冲突,好比同时操做同一个DOM元素,那到底听谁的?咱们固然可使用 “锁”机制来解决这些冲突,但这提升了复杂度。毕竟,js是由js之父 Brendan Eich 花了10天开发出来的。
妈妈问我为何跪着打下了这些字=。=
JavaScript引擎的主要做用,就是读取文件中的JavaScript,处理它并执行。
js是一门解释型语言。解释型语言和编译型语言分别由解释器和编译器处理,下面是二者的处理过程:
解释型语言和编译型语言的区别在于,它不提早编译,或者说,你能不能拿到中间代码。
通常的JavaScript引擎(好比JavaScriptCore)的执行过程是这样的:
源代码→抽象语法树(AST)→字节码 → JIT →本地代码
解释执行效率很低,由于相同的语句被反复解释。所以优化的思路是动态观察哪些代码常常被调用,对于那些被高频率调用的代码,就用编译器把它编译而且缓存下来,下次执行的时候就不用从新解释,从而提高速度。这就是 JIT(Just-In-Time)。
4.2.2.1 v8以前的作法----机器码
基于字节码的实现是主流,然而v8独辟蹊径,它的解释过程是这样的
源代码→抽象语法树(AST)→JIT→本地代码
v8放弃了编译成字节码的过程,少了AST转化成字节码转化,节约了转化时间,并且原生机器码执行更快。在V8生成本地代码后,也会经过Profiler采集一些信息,来优化本地代码。换句话说,v8的作法,是牺牲空间换时间。
4.2.2.3 v8 新版本—字节码
然而,今年4月末,v8推出了新版本,他们启动了 Ignition 字节码解释器。v8又回归了字节码。
讲道理,机器码既然执行快,为何又要“回退”到字节码呢?不能由于我超可爱,你就欺负我啊!
详细能够看《V8 Ignition:JS 引擎与字节码的不解之缘》
文章做者认为缘由以下:
1. 减轻机器码占用的内存空间,即牺牲时间换空间(主要动机)
字节码是机器码的抽象,同一段代码,在字节码和机器码中的存储以下:
(图片来自Understanding V8’s Bytecode)
显然,机器码占用内存过大
2. 提升代码的启动速度;
3. 对 v8 的代码进行重构,下降 v8 的代码复杂度
个人补充解释以下:
JIT优化过程当中,safari的JSC的作法以下图:
(图片来自:[WebKit] JavaScriptCore解析)
然而,js是无类型语言,也就是变量的类型有可能会改变。举一个典型的栗子:
function add(a, b) {
return a + b;
}
复制代码
若是这里的 a 和 b 都是整数,可见最终的代码是汇编中的 add 命令。若是相似的加法运算调用了不少次,解释器可能会认为它值得被优化,因而编译了这段代码。但若是下一次调用的是 add("你好哇", "云霁!"),以前的优化就无效了,由于字符串加法的实现和整数加法的实现彻底不一样。
而v8以前并无字节码这个中间表示,因此优化后的代码(二进制格式)还得被还原成原先的形式(字符串格式),这样的过程被称为优化回滚。反复的优化 -> 优化回滚 -> 优化 …… 很是耗时,大大下降了引入 JIT 带来的性能提高。
因而JIT 就很难过
而如今的v8 使用 Ignition(字节码解释器) 加 TurboFan(JIT 编译器)的组合,缓解了这个问题
先后性能对好比下图:
(图片来自:emm...找不到出处了,有好心人知道望告知)
js是单线程的,但为何能执行ajax和setTimeout等异步操做呢? 很简单,由于浏览器是多线程的呀!
一个浏览器一般由如下线程组成:
5.2.1 我是一段野生代码
咱们先来看一段代码
var init = new Date().getTime()
function a1(){
console.log('1')
}
function a2(){
console.log('2')
}
function a3(){
console.log('3')
}
function a4(){
console.log('4')
}
function a5(){
console.log('5')
}
function a6(){
console.log('6')
}
function a7(){
console.log('7')
}
function a8(){
console.log('8')
}
function a9(){
console.log('9')
}
function a10(){
for(let i = 1;i<10000;i++){}
console.log('10')
}
a1()
setTimeout(() => {
a2()
console.log(new Date().getTime()-init)
Promise.resolve().then(() => {
a3()
}).then(() => {
a4()
})
a5()
}, 1000)
setTimeout(()=>{
a6()
console.log(new Date().getTime()-init)
}, 0)
Promise.resolve().then(() => {
a7()
}).then(() => {
a8()
})
a9()
a10()
复制代码
之因此有n个a*函数,是为了后续方便调试,核心代码从a1()开始
执行结果:你猜?
代码里用到了定时器和异步请求,那么他们究竟是怎么配合执行的呢?
这里须要引入一个概念,event loop。
5.2.2.1 事件机制的概念
浏览器的主线程是event loop即事件循环,什么是eventloop呢?
HTML5规范是这么说的
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
为了协调事件、用户交互、脚本、UI 渲染、网络请求,用户代理必须使用 eventloop。
5.2.2.2 事件机制的原理
理解事件循环机制的工做原理是这样的:
咱们基于规范学习一下这几个名词:
task queue(任务队列)
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for work as...
一个事件循环会有一个或者多个任务队列,每一个任务队列都是一系列任务按照顺序组成的列表。
而多个任务列表源于:每一个任务都有指定的任务源,相同的任务源的任务按顺序放在同一个任务列表里。不一样的任务列表按照优先级执行任务。
哪些是task任务源呢?
规范在Generic task sources中有说起(原文可看连接,为节省篇幅,此处直接给出翻译):
DOM操做任务源
此任务源用于对DOM操做做出反应,例如一个元素以非阻塞的方式插入文档。
用户交互任务源
此任务源用于对用户交互做出反应,例如键盘或鼠标输入
响应用户操做的事件(例如 click)必须使用task队列。
网络任务源
此任务源用于响应网络活动。
历史遍历任务源
此任务源用于将对history.back()和相似API的调用排队
此外 setTimeout、setInterval、IndexDB 数据库操做等也是任务源。
Microtask
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.
一个事件循环会有一个microtask列表,microtask中的任务一般指:
简单来讲,事件循环机制是这样运行的(此处规范原文):
5.2.3.1 分析
根据以上理论,咱们很容易分析到上述代码执行的事件循环,以下:
执行栈读到script,开始执行任务
第一次循环:
(计时线程到时间后,将计时器的回调函数按顺序放入任务队列中)
第二次循环:
从任务队列中读到setTimeout2 cb
由于setTimeout老是计时结束以后,在任务队列中排队等待执行,因此它执行的时间,老是大于等于开发者设置的时间
可是,即使设置为0,且当前没有正在执行的任务的状况下,时间也不可能为0,由于规范规定,最小时间为4ms!
第三次循环:
从任务队列中读到setTimeout1 cb
5.2.3.2 验证
好了,我说了不算,咱们用chrome developer tools的Perfomance面板来验证是否正确
步骤是酱的:
1. 打开隐身模式,或者去掉chrome启动的插件,由于这些插件会干扰咱们分析
2. 打开控制台
3. 打开面板:新版chrome是Perfomance面板,老版是Timeline面板
4. 看见左上角那个实心圈圈没有?
趁他不注意,赶忙怼他一下!
5. 如今已经开始录制了,迅速刷新一下页面,等个3,4s就中止录制
6. 仔细看下面那个 Main那条来一块儿分析。
第一次循环:
看到一个很醒目的a1(紫条)了!
a1 后面是 黄色的setTimeout(黄条),再后面是a9 a10(紫条) run microtasks(黄条),下面一次是a7 a8(紫条)
(这就是为何要写函数名,否则全世界都是匿名函数,乍一看还分不清谁是谁)
来镜头拉近看一下setTimeout那里的两个小黄条在作什么
红色框里的文字,是鼠标移上去看到的字,橙色框是详细信息,点击最后一行 index.html 能够看到具体代码,这里忘了截图。戳进去会跳转到第一个setTimeout那一行(也就是89行)。
这个是第二个setTimeout,定位是在第二个setTimeout那里。
可验证第一次循环判断正确!First Blood!
第二次循环:
Double Kill!
第三次循环:
可能有人会疑惑这里为何没有a4,那是由于代码执行太快,而控制面板显示时间是精确到0.0m的,因此会有些偏差,事实上,咱们在a3中多执行一些耗时代码就能看到了。或者也能够多录制几回,每次结果都会有些出入,可是函数执行顺序是不会不一致滴!
Aced!一百昏!一百昏!老铁们双击666!
说了那么多,此时难道咱们不该该作点什么?
《WebKit技术内幕》