画了20张图,详解浏览器渲染引擎工作原理(下)

简介: 今天我们来学习一下浏览器渲染引擎的工作原理,文章内容较多,建议先收藏再学习!先来看看Chrome浏览器的架构图:

2. 绘制图层

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,下面就来看看渲染引擎是怎么实现图层绘制的。

渲染引擎在绘制图层时,会把一个图层的绘制分成很多绘制指令,然后把这些指令按照顺序组成一个待绘制的列表:


网络异常,图片无法展示
|


可以看到,绘制列表中的指令就是一系列的绘制操作。通常情况下,绘制一个元素需要执行多条绘制指令,因为每个元素的背景、边框等属性都需要单独的指令进行绘制。所以在图层绘制阶段,输出的内容就是绘制列表。


在Chrome浏览器的开发者工具中,通过Layer标签可以看到图层的绘制列表和绘制过程:

网络异常,图片无法展示
|


绘制列表只是用来记录绘制顺序和绘制指令的列表,而绘制操作是由渲染引擎中的合成线程来完成的。当图层绘制列表准备好之后,主线程会把该绘制列表提交给合成线程。

注意:合成操作是在合成线程上完成的,所以,在执行合成操作时并不会影响到主线程的执行。


很多情况下,图层可能很大,比如掘金的一篇长文,需要滚动很久才能到底,但是用户只能看到视口的内容,所以没必要把整个图层都绘制出来。因此,合成线程会将图层划分为图块,这些图块的大小通常是 256x256 或者 512x512。合成线程会优先将视口附近的图块生成位图。实际生成位图的操作是在光栅化阶段来执行的,所谓的光栅化就是按照绘制列表中的指令生成图片。


当所有的图块都被光栅化之后,合成线程就会生成一个绘制图块的命令,浏览器相关进程收到这个指令之后,就会将其页面内容绘制在内存中,最后将内存显示在屏幕上,这样就完成了页面的绘制。


至此,整个渲染流程就完成了,其过程总结如下:

  1. 将HTML内容构建成DOM树;
  2. 将CSS内容构建成CSSOM树;
  3. 将DOM 树和 CSSOM 树合成渲染树;
  4. 根据渲染树进行页面元素的布局;
  5. 对渲染树进行分层操作,并生成分层树;
  6. 为每个图层生成绘制列表,并提交到合成线程;
  7. 合成线程将图层分成不同的图块,并通过栅格化将图块转化为位图;
  8. 合成线程给浏览器进程发送绘制图块指令;
  9. 浏览器进程会生成页面,并显示在屏幕上。


六、其他


1. 重排和重绘

说完浏览器引擎的渲染流程,再来看两个重要的概念:重排(Reflow)和重绘(Repaint)。


我们知道,渲染树是动态构建的,所以,DOM节点和CSS节点的改动都可能会造成渲染树的重新构建。渲染树的改动就会造成页面的重排或者重绘。下面就来看看这两个概念,以及它们触发的条件和减少触发的操作。


(1)重排

当我们的操作引发了 DOM 树中几何尺寸的变化(改变元素的大小、位置、布局方式等),这时渲染树里有改动的节点和它影响的节点都要重新计算。这个过程就叫做重排,也称为回流。在改动发生时,要重新经历页面渲染的整个流程,所以开销是很大的。


以下操作都会导致页面重排:

  • 页面首次渲染;
  • 浏览器窗口大小发生变化;
  • 元素的内容发生变化;
  • 元素的尺寸或者位置发生变化;
  • 元素的字体大小发生变化;
  • 激活CSS伪类;
  • 查询某些属性或者调用某些方法;
  • 添加或者删除可见的DOM元素。


在触发重排时,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:


  • 全局范围:从根节点开始,对整个渲染树进行重新布局;
  • 局部范围:对渲染树的某部分或者一个渲染对象进行重新布局。


(2)重绘

当对 DOM 的修改导致了样式的变化、但未影响其几何属性(比如修改颜色、背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(会跳过重排环节),这个过程叫做重绘。简单来说,重绘是由对元素绘制属性的修改引发的。


当我们修改元素绘制属性时,页面布局阶段不会执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。


下面这些属性会导致回流:

  • color、background 相关属性:background-color、background-image 等;
  • outline 相关属性:outline-color、outline-width 、text-decoration;
  • border-radius、visibility、box-shadow。


注意: 当触发重排时,一定会触发重绘,但是重绘不一定会引发重排。

相对来说,重排操作的消耗会比较大,所以在操作中尽量少的造成页面的重排。为了减少重排,可以通过以下方式进行优化:

  • 在条件允许的情况下尽量使用 CSS3 动画,它可以调用 GPU 执行渲染。
  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制


浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列, 浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。


2. JavaScript对DOM的影响

最后我们再看看看JavaScript脚本对DOM的影响。当解析器解析HTML时,如果遇到了

来看一段代码:

<html>
    <body>
        <div>hello juejin</div>
        <script>
            document.getElementsByTagName('div')[0].innerText = 'juejin yyds'
        </script>
        <p>hello world</p>
    </body>
</html>
复制代码


这里,当解析完div标签后,就会解析script标签,这时的DOM结构如下:

网络异常,图片无法展示
|


这时,HTML解析器就会暂停工作,JavaScript引擎就会开始工作,并执行script标签中的脚本内容。由于这段脚本修改了第一个div的内容,所以执行完这个脚本之后,div中的文本就变成了“juejin yyds”,当脚本执行完成之后,HTML解析器就会恢复解析过程,继续解析后面的内容,直至生成最终的DOM。


上面我们说的JavaScript脚本是通过script标签直接嵌入到HTML中的。当在页面中引入JavaScript脚本时,情况就会变得复杂。比如:

<html>
    <body>
        <div>hello juejin</div>
        <script type="text/javascript" src='./index.js'></script>
        <p>hello world</p>
    </body>
</html>
复制代码


其实这里的执行流程和上面时一样的,当遇到script标签时,HTML解析器都会暂停解析并去执行脚本文件。不过这里执行 JavaScript 脚本时,需要先下载脚本。脚本的下载过程会阻塞 DOM 的解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 脚本文件大小等因素的影响。


经过上面的分析可知,JavaScript 线程会阻塞 DOM 的解析,我们可以通过CDN、压缩脚本等方式来加速 JavaScript 脚本的加载。如果脚本文件中没有操作DOM的相关代码,就可以将JavaScript脚本设置为异步加载,可以给script标签添加 async 或 defer 属性来实现脚本的异步加载。两者的使用方式如下:

<script async type="text/javascript" src='./index.js'></script>
<script defer type="text/javascript" src='./index.js'></script>
复制代码


下图可以直观的看出异步加载和直接加载的区别:

网络异常,图片无法展示
|
其中蓝色代表JavaScript脚本加载时间,红色代表JavaScript脚本执行时间,绿色代表HTML解析。


defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:

  • 执行顺序: 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;
  • 脚本是否并行执行: async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行进行的,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),JavaScript 脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。


再来看另外一种情况,示例代码如下:

<html>
    <head>
    <style src='./style.css'></style>
    </head>
    <body>
        <div>hello juejin</div>
        <script>
            const ele = document.getElementsByTagName('div')[0];
            ele.innerText = 'juejin yyds';    // 操作DOM
            ele.style.color = 'skyblue';      // 操作CSSOM
        </script>
        <p>hello world</p>
    </body>
</html>
复制代码


上面的代码中,第9行是操作DOM的,而第10行是操作CSSOM的,所以在执行 JavaScript 脚本之前,还需要先解析 JavaScript 语句之上所有的 CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要 等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。


而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。


所以,JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行,我们在开发时需要格外注意这一点。


最后再来看一种情况,示例代码如下:

<html>
    <head>
        <style src='./style.css'></style>
    </head>
    <body>
        <div>hello juejin</div>
        <script type="text/javascript" src='./index.js'></script>
        <p>hello world</p>
    </body>
</html>
复制代码


这段HTML代码中包含了CSS外部引用和JavaScript外部文件,在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,就会同时发起两个文件的下载请求。

相关文章
|
3月前
|
存储 缓存 前端开发
浏览器缓存工作原理是什么?
浏览器缓存工作原理是什么?
|
3月前
|
存储 安全 前端开发
浏览器跨窗口通信:原理与实践
浏览器跨窗口通信:原理与实践
54 0
|
3月前
|
消息中间件 JavaScript 前端开发
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
|
9月前
|
JavaScript 前端开发 容器
从零开始实现一个玩具版浏览器渲染引擎(三)
从零开始实现一个玩具版浏览器渲染引擎(三)
31 0
|
9月前
|
前端开发 JavaScript
从零开始实现一个玩具版浏览器渲染引擎(二)
从零开始实现一个玩具版浏览器渲染引擎(二)
46 0
|
4月前
|
消息中间件 前端开发 Java
【面试题】前端必修-浏览器的渲染原理
【面试题】前端必修-浏览器的渲染原理
|
5月前
|
Web App开发 JavaScript 前端开发
从浏览器原理出发聊聊Chrome插件
本文从浏览器架构演进、插件运行机制、插件基本介绍和一些常见的插件实现思路几个方向聊聊Chrome插件。
786 0
|
8月前
|
安全 算法 网络协议
浏览器基础原理-安全: HTTPS
浏览器基础原理-安全: HTTPS
63 0
|
8月前
|
Web App开发 存储 监控
浏览器基础原理-安全: 渲染进程-安全沙盒
浏览器基础原理-安全: 渲染进程-安全沙盒
39 0
|
8月前
|
安全
浏览器基础原理-安全: CSRF攻击
浏览器基础原理-安全: CSRF攻击
54 0

相关实验场景

更多