Chrome 浏览器页面渲染工做原理浅析

原文发表于知乎专栏cloudjfed 《chrome浏览器页面渲染工做原理浅析》css

1. 简介

本篇文章基于本身对chrome浏览器工做原理的一些理解,重点在于页面渲染的分析。此文会随着我理解的深刻持续更新,如有错误,欢迎随时指正!比心 (。♥‿♥。)html

参考资料重点来源于:前端

  1. 《WebKit技术内幕》
    做者是朱永盛,Chromium项目的committer。做者的我的博客:http://blog.csdn.net/milado_nju
  2. HTML5规范
    该规范来自whatwg。和w3c制定的html规范不一样的是,前者来自Mozilla、Chrome、Safari等,然后者背后是微软公司。由于本文主要探究的是chrome浏览器的行为,因此主要参考的是whatwg的规范。

文章目录:node

  1. 简介
  2. 当咱们谈论Webkit的时候,咱们在谈论什么?
    1. 浏览器内核
    2. Webkit
    3. Chromium
  3. 渲染引擎作了啥
  4. JavaScript引擎作了啥
    1. js是单线程的
    2. JavaScript引擎怎么作
      1. 解释过程
      2. v8的解释过程
        1. v8以前的作法——机器码
        2. v8新版本——字节码
  5. 浏览器的多线程
    1. 多线程有哪些
    2. 线程之间如何配合
      1. 我是一段野生代码
      2. 事件循环机制
        1. 事件机制的概念
        2. 事件机制的原理
      3. 代码执行过程分析
        1. 分析
        2. 验证
  6. 基于浏览器(chrome内核)工做原理的代码优化
  7. 参考资料

2. 当咱们谈论Webkit的时候,咱们在谈论什么?

对于前端同窗来讲,webkit这个名词很是熟悉了,那么当咱们在说chrome是Webkit内核的时候,咱们到底在说什么?git


2.1 浏览器内核

浏览器有一个重要的模块,它主要的做用是将页面变成可视(听)化的图形、音频结果,这就是浏览器内核。不一样浏览器有不一样内核,经常使用的有Trident(IE)、Gecko(Firefox)、Blink(Chrome)、Webkit(Safari)github

浏览器内核又能够分红两部分:渲染引擎和 JS 引擎。web

最开始渲染引擎和 JS 引擎并无区分的很明确,后来 JS 引擎愈来愈独立,内核就倾向于只指渲染引擎。ajax


2.2 Webkit

一说到Webkit,最早想起来的可能就是chrome。但其实Webkit最先是苹果公司的一个开源项目。算法

苹果同窗哭瞎了眼。chrome

Webkit项目的结构以下

(图片来自《WebKit技术内幕》第三章)


从图中能够看到,Webkit主要由WebCore(渲染引擎)、JavaScriptCore(JavaScript引擎)和Webkit Port(不一样浏览器本身实现的移植部分)构成。

整个项目称为Webkit,而咱们前端开发者在谈到Webkit的时候,每每指的是WebCore,即渲染引擎部分。

当咱们打开一个页面的时候,网页的渲染流程以下:

(图片来自《WebKit技术内幕》第一章)


图中DOM和js引擎的双向箭头,指的是dom和js引擎的桥接接口,用于调用对方的一些方法。


2.3 chromium

那为何咱们提到Webkit的时候,每每会和chrome联系在一块儿呢?

2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium。

谷歌公司研发了本身的JavaScript引擎,v8, 但fork 了开源引擎 Webkit。后来谷歌公司在 Chromium 项目中研发 Blink 渲染引擎,也就是说,最初Blink 是从Webkit复制过来,没有太大区别,但谷歌逐渐进行了优化,并慢慢将其中于chromium 不相关的代码进行移除。因此能够预见的是,之后二者差距会越来越大。

对此,我采访了一下苹果公司,他们表示:

可能须要给他们寄一箱原谅套餐== 固然是假的!毕竟我又不是隔壁老王!


3. 渲染引擎作了啥!

渲染引擎顾名思义,负责渲染,它将网络或者本地获取的网页和资源从字节流进行解释,呈现出来,流程以下图:



从图中能够看到,渲染引擎具体作了:

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对这些层合成!

4.JavaScript引擎作了啥!

4.1 js是单线程的

咱们都知道js是单线程的。为何呢?js设计之初是为了进行简单的表单验证,操做DOM,与用户进行互动。如果多线程操做,则极有可能出现冲突,好比同时操做同一个DOM元素,那到底听谁的?咱们固然可使用 “锁”机制来解决这些冲突,但这提升了复杂度。毕竟,js是由js之父 Brendan Eich 花了10天开发出来的。

妈妈问我为何跪着打下了这些字=。=


4.2 JavaScript引擎怎么作

JavaScript引擎的主要做用,就是读取文件中的JavaScript,处理它并执行。

js是一门解释型语言。解释型语言和编译型语言分别由解释器和编译器处理,下面是二者的处理过程:


解释型语言和编译型语言的区别在于,它不提早编译,或者说,你能不能拿到中间代码。


4.2.1 解释过程

通常的JavaScript引擎(好比JavaScriptCore)的执行过程是这样的:

源代码→抽象语法树(AST)→字节码 → JIT →本地代码

解释执行效率很低,由于相同的语句被反复解释。所以优化的思路是动态观察哪些代码常常被调用,对于那些被高频率调用的代码,就用编译器把它编译而且缓存下来,下次执行的时候就不用从新解释,从而提高速度。这就是 JIT(Just-In-Time)。


4.2.2 v8 的解释过程

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...找不到出处了,有好心人知道望告知)


5. 浏览器的多线程

js是单线程的,但为何能执行ajax和setTimeout等异步操做呢? 很简单,由于浏览器是多线程的呀!

5.1 多线程有哪些

一个浏览器一般由如下线程组成:

  • GUI 渲染线程
  • JavaScript引擎线程
  • 定时触发器线程
  • 事件触发线程(如鼠标点击、AJAX异步请求等)
  • 异步http请求线程


5.2 线程之间如何配合

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 事件循环机制

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中的任务一般指:

  • Promise.then
  • MutationObserver
  • Object.observe

简单来讲,事件循环机制是这样运行的(此处规范原文):

  1. 从任务队列中取出最先的一个任务执行
    执行时产生堆栈
  2. 执行 microtask 检查点
    若是microtask checkpoint的flag(标识)为false,则设为true。执行 队列中的全部 microtask,直到队列为空,而后将microtask checkpoint的flag设为flase
  3. 执行 UI render 操做(可选)
    非每次循环必须,只要知足浏览器60HZ的频率便可
  4. 重复1


5.2.3 代码执行的过程分析

5.2.3.1 分析

根据以上理论,咱们很容易分析到上述代码执行的事件循环,以下:

执行栈读到script,开始执行任务

第一次循环:

  1. a1()
  2. setTimeout1丢到定时线程中去计时
  3. setTimeout2丢到定时线程中去计时
  4. Promise.then() 的cb a7()放入microtask队列
  5. a9()
  6. a10()
  7. 检查执行microtask
  8. a7() ,将cb a8放入microtask
  9. a8()

(计时线程到时间后,将计时器的回调函数按顺序放入任务队列中)


第二次循环:

从任务队列中读到setTimeout2 cb

  1. a6()
  2. 输出时间console.log(new Date().getTime()-init)

由于setTimeout老是计时结束以后,在任务队列中排队等待执行,因此它执行的时间,老是大于等于开发者设置的时间

可是,即使设置为0,且当前没有正在执行的任务的状况下,时间也不可能为0,由于规范规定,最小时间为4ms!


第三次循环:

从任务队列中读到setTimeout1 cb

  1. a2()
  2. 输出时间console.log(new Date().getTime()-init)
  3. 将 Promise.then() 的cb a3放入microtasks
  4. a5()
  5. 检查执行microtask
  6. a3() 将cb a4放入microtasks
  7. a4()

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!


6. 基于浏览器引擎工做原理(chrome内核)的代码优化

说了那么多,此时难道咱们不该该作点什么?

  1. 编写正确的HTML 代码,浏览器在html解释的时候,遇到错误标签,会启动容错机制,开发者应当规避这些错误。
  2. css优先,css优先于js引入,由于渲染树须要拿到DOM树和CSS规则,而js会中止DOM树的构建。
  3. 能够用媒体查询(media query)加载css,来解除对渲染的阻塞,这样,只有当出现符合media的状况时,才会加载该资源。
  4. 尽可能不要使用css import 来加载css,@import不管写在哪一行,都会在页面加载完再加载css
  5. 优化css选择器。浏览器在处理选择器时依照从右到左的原则,所以最右端的选择器应该是优先级最高的,好比 div > span.test 优于 div span。 两个缘由,一是 .test 比 span更准确,二是,浏览器看到 > span.test 会去找 div 的子元素,而不加大于号,则会寻找全局的span标签。
  6. 减小重绘重排
    1. 当你须要修改DOM节点样式时,不要一条一条改n次,直接定义好样式,修改css类便可,尽管chrome作了优化,并不会真的重绘/重排n次,可是不不能保证你没有强制重绘的代码破坏这个机制,更况且,做为开发者,应当有意识编写高质量代码
    2. 将屡次对DOM的修改合并。或者,你先把它从渲染树移除(display:none),这会重排一次,而后你想作什么作什么
    3. 当须要频繁获取元素位置等信息时,可先缓存
    4. 不要使用table布局
    5. transform和opacity属性只会引发合成,因此写css动画的时候,注意两个属性的使用,尽可能只开启GPU合成,而不重绘重排。
    6. 必要时使用函数防抖
  7. 防止js阻塞页面,将script标签放在</body>前面,或者使用defer async 属性加载
  8. 文件大小和文件数量作好平衡,不要由于数量太多,大大超过了浏览器可并行下载的资源数量,要不要由于文件太大,提升了单一资源加载的时间
  9. 优化回滚。不要书写触发优化会滚动的代码。

7. 参考资料

《WebKit技术内幕》

How browsers work

大前端开发者须要了解的基础编译原理和语言知识

《V8 Ignition:JS 引擎与字节码的不解之缘》

浏览器进程?线程?傻傻分不清楚!

event-loop-processing-model

JavaScript 运行机制详解:再谈Event Loop

相关文章
相关标签/搜索