这篇文章简单介绍一下浏览器渲染的相关知识,主要从浏览器组件、浏览器进程和线程、浏览器渲染及浏览器渲染优化几个点来进行简单介绍

一、浏览器组件

浏览器组件主要包括界面控件、浏览器引擎、渲染引擎、网络、UI后端、JS解释器、数据存储持久层。

组件结构如下:

1.用户接口:即界面控件,包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分

2.浏览器引擎:包括渲染引擎和JS引擎,是查询与操作渲染引擎的接口。主要有Trident、Webkit、Blink等几大类

3.渲染引擎:负责显示请求的内容。负责解析HTML、CSS并将结果显示到窗口中

4.网络:用于网络请求,如HTTP请求。它包括平台无关的接口和各平台独立的实现

5.JS解释器:用于解析并执行JavaScript代码

6.UI后端:绘制基础原件,比如组合框和窗口。它提供平台无关的接口,内部使用操作系统的相应实现

7.数据存储持久层:浏览器需要把所有数据存到硬盘上,比如cookies。HTML5规定了一个完整的浏览器中的数据库web database

二、浏览器的进程与线程

Chrome浏览器使用多个进程来隔离不同的网页,在Chrome中打开一个网页相当于起了一个进程,每个tab网页都有由其独立的渲染引擎实例。如果非多进程的话,浏览器中的一个tab网页崩溃,将会导致其他被打开的网页也崩溃。另外相对于线程,进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题。多个线程共享着相同的地址空间和资源,所以线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题,浏览器中通常由以下常驻线程

~1. GUI 渲染线程

   GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了。在介绍js引擎线程的时候会介绍为什么js引擎运行时GUI会被挂起。

2. JavaScript引擎线程

   JS为处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JS是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;如果JS是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果,当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JS在最初就选择了单线程执行。

  GUI渲染线程与JS引擎线程互斥,是由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致。当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。由于GUI渲染线程与JS执行线程是互斥的关系,当浏览器在执行JS程序的时候,GUI渲染线程会被保存在一个队列中,直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

3. 定时触发器线程

  浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

4. 事件触发线程

  当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

5. 异步http请求线程

  在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。

三、浏览器渲染

  当用户请求的HTML文本通过浏览器的网络层到达渲染引擎后,渲染引擎开始工作,每次渲染不超过8K的数据块。

  浏览器的渲染主要分为以下四个步骤:构建DOM树,构建渲染树,布局渲染树和绘制渲染树。首先是生成DOM树,渲染引擎先解析HTML文档,生成DOM树。构建Render树,不管是接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM(CSS Object Model)树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree)。

  布局Render树: 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置

  绘制Render树:最后遍历渲染树并用UI后端层将每一个节点绘制出来。

  生成DOM树的过程:通过深度遍历构建DOM树,当前节点的所有子节点都构建好后才会构建下一个兄弟节点。DOM树生成过程可能会被CSS和JS的加载执行阻塞。

  生成Render树过程:生成DOM树的同时会生成样式结构体CSSOM(CSS Object Model)Tree,两者可以并行解析;再根据CSSOM树构造渲染树,渲染树包含带有颜色、尺寸等显示属性的矩形,矩形顺序与显示顺序基本一致。

 DOM树和Render树之间的关系:没有DOM树就没有Render树,二者又不是简单的一对一的关系。有些DOM元素在Render树上没有位置,有些DOM元素会对应Render树上好几个位置。DOM元素和Render树上的位置可能也不是一一对应的关系。比如:display等于none的也不会被显示在Render树中,但是visibility等于hidden的元素是会显示在这棵树里头的。下拉列表的select元素,我们就需要三个Render树的节点:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。renderer与DOM元素的位置也可能是不一样的。那些添加了 float或者 position:absolute的元素,因为它们脱离了正常的文档流,构造Render树的时候会针对它们实际的位置进行构造。

  布局和绘制:确定了Render树的样式规则后,进行布局。浏览器布局以浏览器可见区域为画布,从左到右,从上到下从DOM的根节点开始绘制。布局阶段输出的结果成为box盒模型,表示了每一个元素的位置和大小。在绘制阶段,渲染引擎会遍历Render树,调用paint()方法,绘制工作由UI后端组件完成。

回流和重绘

  回流:当浏览器发现某个部分发生变化影响布局,需要倒回去重新渲染,reflow 会从 <html>这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。

  重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。

关键路径渲染:

  关键渲染路径(Critical Rendering Path):浏览器将接收到的HTML,CSS以及JavaScript转换为屏幕上所呈现的实际像素,这期间所经历的一系列步骤。优化关键渲染路径对提升页面加载速度有很大的作用。

阻塞渲染

  CSS被视为渲染阻塞资源(包括JS),JS被认为是解释器阻塞资源,它可以读取、修改 DOM 和CSSOM 属性。存在阻塞的CSS资源时,浏览器会延迟JS的执行和DOM构建,直至CSS加载完毕。浏览器遇到script标签时,DOM将停止构建,直到脚本执行完毕。

优化关键渲染路径:

  1.CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。

  2.JavaScript 应尽量少影响 DOM 的构建。

  2.把多个CSS文件和JS文件进行合并。

CSS和JS的加载阻塞情况,首先是CSS阻塞情况:可以用媒体类型(media type)和媒体查询(media query)来解除对渲染的阻塞。设置了媒体类型,会加载但不会阻塞;媒体查询,会在符合条件时阻塞渲染。

   CSS的加载分三种情况:

1.CSS加载不会阻塞DOM树的解析

2.CSS加载会阻塞DOM树的渲染

3.CSS加载会阻塞后面JS语句的执行

没有JS的情况下,HTML与CSS会并行解析,分别生成DOM与CSSOM树,但不完整的CSS无法使用,就算DOM已经构建完,它也得等CSSOM,然后才能进入下一个阶段,合并成Render Tree。

但如果有JS,CSS加载会阻塞后面JS语句的执行,而同步JS脚本执行会阻塞其后的DOM解析。JavaScript阻塞资源的情:如果没有 defer 或 async,浏览器会立即加载并执行指定的脚本。碰到script标签就会阻塞HTML的解析并等到CSSOM构建完毕才会执行脚本。 defer与 async属性会改变阻塞情形,这两个属性都会使 script 异步加载,然而执行的时机是不一样的。async和defer 属性对于 inline-script 都是无效的。 defer 属性载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。 async 属性表示异步执行引入的 JavaScript,如果已经加载好,就会开始执行。

一般情况:

在HTML中插入js代码

将css放在头部,css放在尾部

js加上defer与async与不加的对比

defer和async都会使script异步加载,defer的脚本下载完成后,会延迟执行,在DOMContentLoader之后,load事件之前执行

async,如果脚本已经加载好,就会开始执行,可能在DOMContentLoaded之前或之后执行,但一定回在load触发之前执行。

四、一些简单优化

1.降低样式选择器的复杂度,尽量保持class的简短。减少需要执行样式计算的元素个数,现代浏览器的样式计算直接对目标元素执行,而不是对整个页面执行,所以应该尽可能减少需要执行样式计算的元素的个数。

2.布局的消耗主要消耗在需要布局的DOM元素的数量和布局过程的复杂程度。应该尽可能避免触发布局;使用flexbox代替老的布局;避免强制同步布局事件的发生。

强制同步布局:根据渲染流程,JS脚本是在layout之前执行,如果强制浏览器在执行js脚本之前先执行布局了,这就是强制同步布局。

3.简化绘制的复杂度,减少绘制区域。浏览器会把相邻区域的渲染任务合并在一起进行,所以需要对动画效果进行精密设计,以保证各自的绘制区域不会有太多重叠。

4.长耗时的JS代码放到Web Workers中执行,每帧的渲染应该在16ms内完成, JavaScript代码运行时间过长,就会阻塞其他渲染工作,如果有特别耗时且不操作DOM元素的纯计算工作,可以放到Web Workers中执行。

以上就是对浏览器的一些简单介绍,大家有兴趣的话,可以依据这些点搜索一下浏览器渲染的其他优化方法。

编撰人:yinyanting

快速跳转