Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浏览器渲染原理 #49

Open
lxfriday opened this issue Nov 24, 2019 · 3 comments
Open

浏览器渲染原理 #49

lxfriday opened this issue Nov 24, 2019 · 3 comments
Labels
browser browser

Comments

@lxfriday lxfriday added the browser browser label Nov 24, 2019
@lxfriday
Copy link
Owner Author

lxfriday commented Nov 24, 2019

关于浏览器

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

浏览器内核可以分成两部分:渲染引擎(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 代码

浏览器渲染过程

浏览器渲染过程

image

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

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

image

浏览器的 performance 指标:

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

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

@lxfriday
Copy link
Owner Author

script 的三种加载模式

正常 <script src="index.js"></script>

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

async <script src="index.js" async></script>

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

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

defer <script src="index.js" defer></script>

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

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

@lxfriday
Copy link
Owner Author

JS 操作 DOM 时的性能优化

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

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

缓存变量

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

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

解决办法

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

counter.innerHTML = countText

DocumentFragment

以下内容来自 MDN DocumentFragment

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 = '<span>添加内容</span>'
  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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
browser browser
Projects
None yet
Development

No branches or pull requests

1 participant