最近再次学习了关于浏览器渲染网页的过程,并且新了解到了些关于 Chrome 和 node 中所使用的执行 js 代码的 V8 引擎相关知识,在此做个分享。正文部分总计 3000 字左右,故而拆分成上下两篇,上篇主要介绍浏览器渲染网页的过程,下篇主要介绍 V8 引擎。有不足之处或是任何意见建议,欢迎各位大佬不吝斧正~
网页大致渲染过程
一般情况下,在浏览器输入网址后,首先网络线程会下载一个 index.html 文件,然后就会产生一个渲染任务,传递给渲染主线程的事件队列,最后由渲染主线程执行渲染任务,html 解析器开始解析。
浏览器为了提高解析效率还会开启一个预解析线程,遇到 <link>
标签,就由网络线程去服务器下载对应的 css 文件,然后进行一些解析工作,把结果交给渲染主线程生成 css 样式树,所以 css 不会阻塞 html 的解析。
预解析线程遇到 <script>
标签,也是由网络线程去下载对应的 js 文件,与处理 css 文件不同的是,在这一过程中,渲染主线程会暂停,等待 js 下载完毕,然后启动 V8 引擎执行 js,再继续解析 html。因为 js 可能会修改当前的 dom 树,或是涉及样式的修改,不阻塞 html 解析可能导致无用功。
有一张很经典的图,用于描述浏览器(webkit 内核)的大致渲染过程,我稍做修改,重绘如下:
- 遇见 html 标记,调用 html 解析器构建 dom 树;
- 遇见 style / link 标记,调用相应解析器处理 css 标记,构建 css 样式树;
- 遇见 script 标记,调用 js 引擎,处理 script 标记、绑定事件、修改 dom 树(也就是上图中紫色椭圆形所代表的含义)或 css 树等;
- 将 dom 树和 css 树合并(Attachment)成一个渲染树(Render Tree),此时每个 dom 节点的所有样式都是计算处理过的,比如相对单位(rem)会变成绝对单位(px):
- 根据渲染树来渲染,计算每个节点的几何信息,包括尺寸和位置,生成 layout(布局)树。注意,dom 树和 layout 树不一定会一一对应,因为有些 dom 节点是隐藏的(比如
display: none
),那么就不会出现在 layout 树;而有些 dom 节点使用了伪元素选择器(比如::before
),虽然 dom 树上不会有伪元素节点,但因为它有几何信息,所以 layout 树上会多出一个伪元素的节点;
这里补充说明下,所谓的回流 (Reflow) ,其本质就是重新计算 layout 树。为了避免过于频繁地回流,对于诸如改变某个节点的宽高这种操作,浏览器会进行合并,异步地去完成。但是对于获取布局属性的操作(比如
.clientHeight
),浏览器会立即 Reflow 以保证数据的准确性。
- 现代浏览器中,渲染主线程会使用一套复杂的策略对 layout 树进行分层(Layer),以便某一层改变时,仅需要对该层进行处理。比如滚动条(下图中的 #301)就会单独分为一层,
transform
、opacity
或will-change
,也会影响分层结果:
- 接着渲染主线程为每个层单独产生绘制指令集(类似 canvas 的绘制命令),将绘制信息交给合成(Compositor)线程完成,至此渲染主线程的工作就完成了;
所谓的重绘 (Repaint) ,其本质就是重新根据分层信息计算了绘制指令。比如更改了某个节点的颜色,或是布局样式,故而 Reflow 一定会导致 Repaint。
- 合成线程会对每个图层进行分块(Tiling),这一步将会从线程池调用多个线程完成;
- 之后调用 GPU 进程,将各个块(优先处理靠近视口的块)光栅化(Raster)为位图,再交给合成线程;
- 最后进行绘制(Draw),合成线程根据位图信息,生成指引(quad)信息 —— 标识出各个位图应该在屏幕的哪个位置,并考虑旋转、缩放的变形(
transform
),这也是为何动画使用transform
效率更高的原因,它与渲染主线程无关 —— 交给 GPU 进程,进而产生系统调用,交给 GPU 硬件完成绘制。
下面对上述中的一些概念做个解释:
浏览器内核
浏览器内核也叫渲染引擎,或是浏览器引擎(browser engine)、排版引擎(layout engine)等。不同的浏览器内核不同,主要有以下这么 4 种:
- Gecko:早期 Firefox 使用
- Trident:IE 使用
- WebKit:Safari 使用
- Blink:Chrome、Edge 使用,是 WebKit 的分支引擎
HTML 解析器
HTML 解析器是浏览器内核的一个主要模块。一个渲染引擎主要包括 5 大模块:
- html 解析器:将 html 文本解析并生成一个 dom 树。
- css 解析器:为 dom 中的各个元素对象计算出样式信息,为布局提供基础设施。
- js 引擎:执行 js 代码,js 是高级语言,需要通过 js 引擎转成最终的机器指令来执行。
- 布局(Layout) 模块:将 dom 节点和样式信息结合,计算大小位置等,形成一个能表达这所有信息的内部表示模型。
- 绘图模块(Painting):使用图形库将布局计算后的各个网页的节点绘制成图像结果。
这 5 大模块依赖很多其它基础模块,比如网络、存储、2d/3d 图像、音/视频/图片解码器等。
注:WebKit 事实上可以分为两部分:WebCore(css、svg、布局、渲染树、html、dom 等) 和 JSCore(js 引擎)。
dom 树
dom 树,即 Document Object Model Tree(文档对象模型树),是浏览器解析 html 文档后生成的树形结构。每个 html 标记、文本节点和属性都作为一个节点(对象)在 dom 树中表示:
document
└── html
├── head
│ ├── meta (charset)
│ ├── meta (viewport)
│ └── title
│ └── "Document"
└── body
└── h1
└── "Hello, World!"
如果我们直接在浏览器控制台打印查看 document
,看不出它的结构:
可以使用 console.dir(document)
来显示 document
的所有属性和方法:
css 样式树
一个 html 文档中的样式文件可能来自 <link>
、<style>
、内联样式或是浏览器默认样式等,一个样式文件就是一个 CSSStyleSheet,里面有不同的规则 CSSStyleRule,规则中又包含着选择器和样式 style,它们共同组成了 css 样式树:
StyleSheetList
└── CSSStyleSheet
├── CSSStyleRule
│ ├── body
│ └── style
│ └── font-size: 16px
│
└── CSSStyleRule
└── h1
└── style
└── color: red
随便打开一个网页,在控制台输入 document.styleSheets
就能查看到该网页的 StyleSheetList
对象,里面有着所有除浏览器默认样式之外的 CSSStyleSheet
,包括内联样式和外联样式:
可以通过 js 控制这些 CSSStyleSheet,比如添加条规则让所有 div 的字体颜色为红色:
document.styleSheets[4].addRule('div', 'color: red!important')
如此,页面就会添加一个样式:
注意其和直接使用 dom 的 .style
设置样式的区别。
高级语言
上面还提到, js 是高级语言,那么什么是高级语言呢?为什么要用 js 引擎来执行?
编程语言,按照发展历史划分,大致可以分为三个阶段:
- 机器语言
就是 010101 这样的机器指令。机器能直接识别而无需经过翻译,每一操作码在计算机内部都有相应的电路来完成。从使用的角度看,机器语言是最低级的语言。
- 汇编语言
也叫作符号语言,如 MOV(代表数据传递)、ADD(代表数字逻辑上的加减) 等汇编指令就是汇编语言。属于第二代计算机语言。
- 高级语言
c、c++、c#、java 等,js 也是一门高级编程语言。计算机本身并不认识高级语言,所以这些高级语言想要最终在 cpu 运行,最终需要转换成机器指令。
js 引擎
我们知道 .js 文件可以通过浏览器或 node 执行,其实最后,都是被 cpu 执行。cpu 只认识自己的指令集,是机器语言,所以需要 js 引擎将 js 文件翻译成 cpu 指令来执行。js 引擎有好几种,比如第一款 js引擎,由 js 作者 Brendan Eich 开发的 SpiderMonkey;webkit 和小程序中使用的 js 引擎,由苹果公司开发的 JavaScriptCore 和下篇主要介绍的由谷歌开发,在 Chrome 中使用的 V8 引擎等。