内容大部分参考自 解锁浏览器背后的运行机制[1]
浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并没有十分明确的区分,但随着 JS 引擎越来越独立,内核也成了渲染引擎的代称(下文我们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。
目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。
浏览器渲染会使用以下几个关键的概念:
每个页面的渲染都经过了下面几个阶段:
浏览器的 performance 指标:
相关的事件可以参考 chrome-performance 页面性能分析使用教程[2]
正常
这种加载模式会阻塞浏览器,浏览器需要加载并执行完脚本才会继续往下面解析,这种脚本如果放在 中要比较谨慎,建议放在
的尾部。
async
async
模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。
也就是说,async
加载的脚本的执行时间是不固定的,一旦加载完成就会立即执行,执行的时候会阻塞浏览器。
defer
defer
模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded
事件即将被触发时,被标记了 defer
的 JS 文件才会开始依次执行。
从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async
;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer
。
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.appendChild
和Node.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
),浏览器不需要重新计算元素的位置和大小,直接重新绘制。
回流包括几何尺寸计算和重绘。
常见的几何属性有 width
、height
、padding
、margin
、left
、top
、border
等等。要特别注意下面这些属性:offsetTop
、offsetLeft
、 offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、clientTop
、clientLeft
、clientWidth
、clientHeight
, 这些属性是经过即时计算得到,也就是说每次调用它们取值时都会触发内部的计算,会导致回流。
当我们调用了 getComputedStyle
方法,或者 IE 里的 currentStyle
时,也会触发回流。原理是一样的,都为求一个“即时性”和“准确性”。
避免回流和重绘
除了前两节介绍的 缓存变量和 DocumentFragment
,还有以下方法。
.xxx
,用 JS 控制添加选择器或者删除这个选择器。避免一条一条的设定 CSS 属性从而触发频繁的回流重绘。 display: none
将元素先隐藏,然后对元素进行 CSS 设置,最后恢复显示。 display: none
后容器将不会触发回流重绘。 现在的浏览器都比较智能,内部维护有一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。上面提到的经过即时计算得到的属性会在计算前强制清空队列,导致回流和重绘发生。 --- 最后一击——回流(Reflow)与重绘(Repaint)[4]
解锁浏览器背后的运行机制: 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