浏览器渲染原理

进程


  • 我们知道,应用程序能够运行,是需要占用计算机资源的。

  • 为了稳定的并行多个应用程序,计算机必须得做好资源的合理分配和管理,这是操作系统的基本职责。

  • 进程 便是操作系统进行 资源分配基本单位,是 应用程序 的载体。

  • 一个应用程序,又可以是 多进程 设计, 比如 现代浏览器

现代浏览器是多进程的


在应用程序中,为了满足功能的需要,主进程会创新新的辅助进程来处理其他任务。这些辅助进程拥有全新的独立的内存空间。如果一个应用程序的这些进程需要通信,可以通过IPC (Inter process Communication )机制来进行。

我们都知道,现代浏览器 都支持多 tab ,一个 tab 对应一个 网页,其实也对应一个网页进程。进程之间互相独立的这种互不影响性 就保证了一个网页的奔溃,不会影响其他网页。当然,如果主进程奔溃,就会影响所有网页了。

不同的浏览器使用不同的架构,下面主要以Chrome为例,主要有 4 个进程:

浏览器主进程(Browser Process): 负责协调、主控,有且只有一个。

  • 负责 浏览器界面与用户交互。如前进,后退等;
  • 负责各个tab页面的管理,创建和销毁;
  • 网络资源下载,文件访问等。

渲染进程(Render Process):也称 渲染引擎,也是我们常说的 浏览器内核

  • 负责 页面渲染,脚本执行,事件处理等
  • 它是 多线程的。

插件进程(Plugin Process):负责控制网页使用到的插件。

  • 每种插件对应一个进程,仅当使用该插件时才创建。

GPU进程(GPU process):负责所有显示任务。

  • 最多一个,用于 3D 绘制和硬件加速。

多进程架构的好处


更高的容错性

  • 现代 Web(Html Js Css) 的复杂性已经越来越高,代码出现 Bug 可能直接导致 渲染引擎 奔溃。
  • 多个 tab 页面会开多个 渲染进程,一个页面的崩溃不会影响其他页面的正常运行。

更高的安全性和沙盒性(sanboxing)

  • 网络上一直以来,都充斥着各种恶意代码攻击,甚至会利用一些漏洞安装恶意软件和插件。
  • 浏览器多进程的设计,通过对不同进程设置不同的权限,创造 沙盒式 运行环境,使其更安全可靠。

更高的相应速度

  • 进程是操作系统进行资源分配的基本单位。
  • 单进程设计就意味着,进程中的各个任务会相互竞争,抢夺CPU资源。而多进程架构正好改善了这一点,提升了响应速度。

多进程虽然有诸多好处,但是缺点也很明显,那就是 费内存

为了节约内存,Chorme浏览器设计和提供了四种 进程模式Process Models

  • Process-per-site-instance : 默认;同一个site-instance使用一个进程。
  • Process-per-site: 同一个 site 使用一个进程。
  • Process-per-tab: 每个tab使用一个进程。
  • Single process: 所有tab公用一个进程。

那么,sitesite-instance 的区别是什么呢?

  • site: 协议和主域名一致则为 同site (和 同源策略 不同,不需要 子域名端口号 一致)。
  • site-instance: connected pages from the same site。包括 同 site 页面,还包括由这些页面打开的新页面。即通过<a target='_blank'></a>window.open 打开的页面。

Chorme浏览器默认选择Process-per-site-instance模式,兼容了性能和易用性,是一种中庸的选择之道。

线程


前面提到,渲染进程又是 多线程 设计,那 线程 又是什么由来呢?

  • 应用程序越来越多,功能迭代使得应用程序对 资源的消耗 也越来越大。

  • 为了提升进程的效率,避免计算机资源的浪费,一个进程又可以有多个 线程

  • 线程 是CPU进行任务调度的基本单位。可以理解为进程的一个控制单元。

  • 一个进程至少有一个线程。所有线程可共享进程所拥有的全部资源。

浏览器内核是多线程的


浏览器内核 主要有以下几个 线程:

GUI线程

  • DOM解析, CSS解析,生成 渲染树
  • RenderObject树需要 更新样式 属性时,即发生重绘(Repaint)
  • RenderObject树中元素 尺寸布局显示隐藏等发生变化,即发生回流(reflow)
  • 在发生 重绘回流时,会形成 GUI更新队列
  • GUI线程JS引擎线程互斥。当JS引擎执行时,GUI线程会被挂起GUI更新队列 只能等到 JS引擎 空闲时被执行。

JS引擎线程(比如 V8

  • 也称JS内核,负责解析Javascript脚本,运行代码。
  • GUI渲染线程JS引擎线程互斥的。所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

事件触发线程

  • 用来控制事件循环(Event Loop)。
  • 配合JS引擎处理 异步代码JS 异步代码的 回调函数 会形成一个 任务队列 ,等待 JS引擎 空闲时被调用。

定时触发器线程

  • setIntervalsetTimeout所在线程(因为JS引擎单线程 的, 如果处于 阻塞线程 状态就会影响记计时的准确)。
  • 定时器事件 会在计时完毕后,添加到 事件队列 中,等待 JS引擎 空闲后执行。

异步http请求线程

  • XMLHttpRequest连接 后是通过浏览器新开一个 异步http请求线程 来请求。
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生 状态变更事件,将这个回调再放入事件队列中。等待 JS引擎 空闲后执行。

浏览器渲染流程


主要任务

浏览器渲染流程的主要任务如下(不一定按顺序):

html文档解析

  • 遇到JS脚本,会加载并执行。
  • 遇到CSS代码,同步进行解析。
  • 过程中可能会被CSSJS的加载而执行阻塞
  • 解析完成,生成Dom树。
  • 在控制台console里面输入document可查看Dom树.

CSS解析

  • html解析过程中,会对遇到的CSS同步进行解析:

    • CSS包括: 标签自带样式;通过link/@import引用的样式文件;style标签内的样式;元素内嵌的样式。
    • 转换样式表中的属性值,使其标准化,比如empxbold转换成一个具体的数值,red转换城rgb(255,0,0)等。
    • 计算每个DOM的具体样式。
    • CSS继承。
  • 不会阻塞html解析流程。

  • 解析完成,生成 CSSOM(CSS Object Model)

  • 在控制台console里面输入document.styleSheets可查看 CSSOM

JS脚本执行

  • html解析过程中,会对遇到的JS脚本进行加载并执行。
  • CSS解析与JS脚本的执行互斥(JS可能会等待CSSOM生成以后执行)。
  • Webkit内核中进行了JS执行优化,只有在JS访问CSS时才会发生互斥。

Dom树CSSOM 结合生成 渲染树(Rendering Tree

  • Dom树节点不是完全对应的。
  • 比如head标签下的所有内容,display:one 的元素就不会被添加到渲染树中。
  • visibility: hidden的元素在渲染树中。
  • 渲染树是一系列将被渲染的对象。

布局(layout):

  • 计算渲染树各节点元素的尺寸、位置等。

  • 这里的计算实际上是“三维”的计算,分层布局(这里涉及复合图层硬件加速的概念)。

  • 布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。

  • 打开Chrome控制台,输入command+shift+p,选择 show Rendering, 选择Layer borders中看到,黄色的就是复合图层。

绘制(paint):绘制页面像素信息。

  • 浏览器会遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。
  • 渲染树的绘制工作是浏览器通过将各层的信息发送给GPUGPU会进行合成(composite),显示在屏幕上。
  • 打开chrome控制台,输入command+shift+p,选择show Layers,可以查看图层。

回流 和 重绘

回流(Reflow): 基于渲染树的页面布局,是一种流式布局。当页面元素修改,引起布局的变化,浏览器就会从html 这个 root 根结点 自上而下遍历,进行重新计算渲染。引发布局变化的操作主要有:

  • 页面第一次渲染(初始化)
  • DOM树变化(如:增删节点)
  • Render树变化(如:显示隐藏,位置,大小等修改)
  • 浏览器窗口resize

重绘(Repaint): 页面元素改变的时候,浏览器会对涉及内容进行重画。

  • 回流必定引起重绘。
  • 仅元素样式改变,不影响布局的情况下,重绘可单独触发。

相关事件

domContentLoaded:当html解析完成,生成Dom树后触发。

  • 此时 Dom 元素可以被访问。
  • 此时 document.readystateloading 变成 interactive

onload事件触发: 页面资源全部已加载完成后触发。

  • 此时 页面上所有的DOM,样式表,脚本,图片都已经加载完成。
  • 此时 document.readystateinteractive 变成 completed

异步脚本 和 延迟脚本

异步脚本: 带async的脚本

  • HTML 还没有被解析完的时候,async脚本已经加载完了,那么 HTML 停止解析,去执行脚本,脚本执行完毕后触发DOMContentLoaded事件。
  • HTML 解析完了之后,async脚本才加载完,然后再执行脚本,那么在HTML解析完毕、async脚本还没加载完的时候就触发DOMContentLoaded事件。
  • 一定会在 load 事件之前执行。

延迟脚本: 带 defer的脚本

  • 不会影响 HTML 文档的解析,而是等到 HTML 解析完成后才会执行。
  • 肯定在 defer 脚本执行结束后,DOMContentLoaded 才会被触发。

渲染阻塞

JS阻塞页面: JS可能操作和修改DOM,也可能操作CSSOM来修改节点样式,所以浏览器在遇到<script>标签时,DOM构建将暂停,直至脚本完成执行(外部脚本还要先下载完成再执行),然后继续构建DOM。浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。这就是所谓的JS阻塞页面。

  • 现在可以在script标签上增加属性defer或者async来改善。
  • 浏览器会将脚本中改变DOMCSS的地方分别解析出来,追加到DOM树和CSSOM规则树上。
  • 所以,script标签的位置很重要。

CSS阻塞渲染: 页面是通过渲染树绘制的,渲染树的生成又必须等所有的CSS(内联、内部和外部)都已经下载完,并解析完生成CSSDOM。这就是CSS阻塞渲染。

  • CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更早的解析CSS,保证更快的首次渲染。
  • 需要注意的是,即便你没有给页面任何的样式声明,CSSOM依然会生成,默认生成的CSSOM自带浏览器默认样式。

优化建议


关键是要深入理解浏览器渲染流程,合法的书写 HTMLCSSJS

减少JS阻塞

  • 合理运用defer或者async
  • domContentLoaded或者onload事件触发时,通过动态创建script标签的方式引入js文件。
  • 合理运用缓存策略。
  • 要避免过多JS请求,也要避免单个JS过大。

减少CSS阻塞

  • 样式尽量前置(放 head标签),尽早解析生成 CSSDOM
  • 样式精简,嵌套层级最小化。

减少重绘

  • 合法,合理书写Html布局是基础,避免无用的深层嵌套。
  • 减少 或者 合并 Dom 操作,读/写操作尽量放一起。
  • 通过document对象的 createDocumentFragment()``cloneNode()方法创建离线Dom,完成操作后再用于真实 Dom
  • window.requestAnimationFrame()进行动画优化。

参考资料


从浏览器多进程到JS单线程
setTimeout和requestAnimationFrame
浏览器渲染原理与过程
DOMContentLoaded 与 load事件