初探浏览器渲染原理

初探浏览器渲染原理_第1张图片

关于浏览器

内容大部分参考自 解锁浏览器背后的运行机制[1]

浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并没有十分明确的区分,但随着 JS 引擎越来越独立,内核也成了渲染引擎的代称(下文我们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器CSS 解释器布局网络存储图形音视频图片解码器等等零部件。

目前市面上常见的浏览器内核可以分为这四种:Trident(IE)Gecko(火狐)Blink(Chrome、Opera)Webkit(Safari)

浏览器渲染会使用以下几个关键的概念:

  1. HTML 解释器:对 HTML 进行词法解析输出 DOM tree
  2. CSS 解析器:解析 CSS,生成 CSSOM tree
  3. 图层布局计算模块:计算每个对象的位置和大小
  4. 视图绘制模块:进行具体节点的图像绘制,绘制像素到屏幕上
  5. JS 引擎:编译执行 JS 代码

浏览器渲染过程

初探浏览器渲染原理_第2张图片 浏览器渲染过程

每个页面的渲染都经过了下面几个阶段:

  • 解析 HTML
  • 解析 CSS 生成 DOM 树,并与 DOM 合并生成 render 树
  • 计算所有元素的位置大小
  • 绘制图层:把每一个页面图层转换为像素,并对所有的媒体文件进行解码
  • 整合图层:浏览器合并各图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上
初探浏览器渲染原理_第3张图片 performance

浏览器的 performance 指标:

  • 蓝色(Loading):网络通信和 HTML 解析
  • 黄色(Scripting):JavaScript 执行
  • 紫色(Rendering):样式计算和布局,即重排 (reflow)
  • 绿色(Painting):重绘(repaint)
  • 灰色(other):其它事件花费的时间
  • 白色(Idle):空闲时间

相关的事件可以参考 chrome-performance 页面性能分析使用教程[2]

script 三种加载模式

正常

这种加载模式会阻塞浏览器,浏览器需要加载并执行完脚本才会继续往下面解析,这种脚本如果放在 中要比较谨慎,建议放在 的尾部。

async

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行

也就是说,async 加载的脚本的执行时间是不固定的,一旦加载完成就会立即执行,执行的时候会阻塞浏览器。

defer

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer

JS 操作 DOM 时的性能优化

JS 操作 DOM 是有代价的,JS 操作 DOM 本质上是 JS 引擎和渲染引擎之间进行了跨界交流,交流依赖于桥接接口。所以频繁操作 DOM 会产生大量的过桥开销,导致整体运行效率低下。

另一个问题是 DOM 元素被更改之后可能触发浏览器重新布局和绘制,也就是回流和重绘。

缓存变量

操作 DOM 时一定要注意不要在循环中频繁地获取 DOM,例如下面,每次循环都要获取一次然后再赋值一次,这其实很没有必要。

for(let i =0;i < 100000; i++) {
       
document.querySelector('#count').innerHTML += '添加内容'
}

解决办法

let counter = document.querySelector('#count')
let countText = counter.innerHTML
for(let i =0;i < 100000; i++) {
count += '添加内容'
}

counter.innerHTML = countText

DocumentFragment

以下内容来自 MDN DocumentFragment[3]

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的 XML 片段。最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题

最常用的方法是使用文档片段作为参数(例如,任何 Node 接口类似 Node.appendChildNode.insertBefore 的方法),这种情况下被添加(append)或被插入(inserted)的是片段的所有子节点, 而非片段本身。因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作。

可以使用 document.createDocumentFragment 方法或者构造函数来创建一个空的 DocumentFragment

使用

let counter = document.querySelector('#count')
let counterFragment = document.createDocumentFragment()
let countText = counter.innerHTML
for (let i = 0; i < 1000; i++) {
const span = document.createElement('span')
span.innerHTML = '添加内容'
counterFragment.appendChild(span)
}
counter.appendChild(counterFragment)

回流和重绘

回流:对 DOM 进行修改导致 DOM 几何尺寸发生变化(修改宽、高,隐藏元素)时,浏览器会重新计算元素几何属性,然后绘制。

重绘:不修改 DOM 元素的几何属性,只修改元素的显示效果(背景色、文字颜色、visibility),浏览器不需要重新计算元素的位置和大小,直接重新绘制。

回流包括几何尺寸计算重绘

常见的几何属性有 widthheightpaddingmarginlefttopborder 等等。要特别注意下面这些属性:offsetTopoffsetLeftoffsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeightclientTopclientLeftclientWidthclientHeight, 这些属性是经过即时计算得到,也就是说每次调用它们取值时都会触发内部的计算,会导致回流。

当我们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。原理是一样的,都为求一个“即时性”和“准确性”。

避免回流和重绘

除了前两节介绍的 缓存变量和 DocumentFragment,还有以下方法。

  1. 提前命名一个选择器 .xxx,用 JS 控制添加选择器或者删除这个选择器。避免一条一条的设定 CSS 属性从而触发频繁的回流重绘。
  2. 使用 display: none 将元素先隐藏,然后对元素进行 CSS 设置,最后恢复显示。 display: none 后容器将不会触发回流重绘。

现在的浏览器都比较智能,内部维护有一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。上面提到的经过即时计算得到的属性会在计算前强制清空队列,导致回流和重绘发生。 --- 最后一击——回流(Reflow)与重绘(Repaint)[4]

参考资料

[1]

解锁浏览器背后的运行机制: https://juejin.im/book/5b936540f265da0a9624b04b/section/5bac3a4df265da0aa81c043c

[2]

chrome-performance页面性能分析使用教程: https://www.cnblogs.com/ranyonsue/p/9342839.html

[3]

MDN DocumentFragment: https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment

[4]

最后一击——回流(Reflow)与重绘(Repaint): https://juejin.im/book/5b936540f265da0a9624b04b/section/5bb1826af265da0a972e3038

你可能感兴趣的:(初探浏览器渲染原理)