先有问题再有答案
浏览器的渲染流程是什么?
如何理解回流重绘?
哪些行为会导致强制同步布局?
渲染任务是什么?
如何理解浏览器的渲染队列?
为什么浏览器需要渲染队列?
渲染优化有哪些方案?
如下代码渲染多少次?
javascript
let app = document.getElementById('app');
app.style.width = app.offsetWidth + 1 + 'px';
app.style.width = 11 + 'px';
app.style.width = app.offsetWidth + 2 + 'px';
app.style.width = 12 + 'px';
app.style.width = app.offsetWidth + 3 + 'px';
渲染流程
回流&重绘
回流
回流是指当网页的布局和几何属性发生变化时,浏览器需要重新计算元素的位置和大小,并重新布局页面的过程。
涉及到DOM树的结构变化,改变页面布局。回流会重新计算渲染树中元素的位置和大小,是一种 性能消耗较大
的操作,因为它会触发浏览器重新布局整个页面
例如:
- 修改DOM元素的位置和大小,例如通过JavaScript修改元素的样式属性、添加或删除元素。
- 修改浏览器窗口的大小。
- 修改DOM树的结构,例如添加或删除元素、修改元素的属性。
- 修改文本内容,例如通过JavaScript修改文本节点的内容或字体大小
重绘
重绘则是指当网页的外观属性(例如颜色、背景、阴影等)发生变化时,浏览器需要重新绘制页面的过程
不涉及DOM树的变化及页面布局变化,只是重新渲染页面样式。重绘的性能开销相对较小,因为它只需要重新绘制元素的外观,而不需要重新计算布局。
例如:
- 修改CSS样式,例如修改元素的背景颜色、字体颜色等。
- 使用
visibility: hidden
隐藏元素,这只会触发重绘而不会触发回流,因为没有布局或位置的变化。
强制同步布局
强制同步布局是指在读取某些布局属性(如offsetWidth
、offsetHeight
等)时,浏览器会 强制清空渲染队列并立即进行布局计算
,以确保返回的值是最新的。这种操作会触发回流,因为浏览器需要重新计算元素的几何属性。
例如:
offsetLeft
offsetTop
offsetWidth
offsetHeight
offsetParent
clientLeft
clientTop
clientWidth
clientHeight
scrollTop
scrollLeft
scrollWidth
scrollHeight
getClientRects()
getBoundingClientRect()
因为浏览器需要确保返回的值是最新的,从而强制清空渲染队列并立即进行布局计算。
渲染任务
渲染任务是指浏览器在渲染页面时需要执行的具体操作。这些任务包括 计算样式、布局、绘制、合成
等步骤。每个渲染任务都是浏览器渲染引擎需要完成的一个独立的工作单元
渲染队列
渲染队列是指浏览器在渲染过程中,将需要执行的渲染任务按照一定顺序排列。渲染队列确保这些任务按照预定义的顺序执行,以保证页面的正确渲染。浏览器会将多个渲染任务放入队列中,并在合适的时机 批量
处理这些任务,以提高渲染效率。
为什么需要渲染队列?
渲染队列可以帮助浏览器将多次样式和布局的修改合并在一起,减少回流和重绘的次数,通过将多个渲染任务放入队列中,可以在合适的时机批量处理这些任务,从而减少频繁的渲染操作,提高渲染效率。
示例分析
demo
javascript
let app = document.getElementById('app');
app.style.width = app.offsetWidth + 1 + 'px';
app.style.width = 11 + 'px';
app.style.width = app.offsetWidth + 2 + 'px';
app.style.width = 12 + 'px';
app.style.width = app.offsetWidth + 3 + 'px';
分析如下:
- 第一次遇到offsetWidth 触发强制同步布局 清空当前的渲染队列 当前队列为空 不触发回流。
- 将app.style.width = app.offsetWidth + 1 + 'px';作为渲染任务加入到渲染队列中。
- 将app.style.width = 11 + 'px';作为渲染任务加入到渲染队列中。
- 第二次遇到offsetWidth 触发强制同步布局 清空当前的渲染队列 触发回流重绘机制。
- 将app.style.width = app.offsetWidth + 2 + 'px';作为渲染任务加入到渲染队列中。
- 将app.style.width = 12 + 'px';作为渲染任务加入到渲染队列中。
- 第三次遇到offsetWidth 触发强制同步布局 清空当前的渲染队列 触发回流重绘机制。
- 将app.style.width = app.offsetWidth + 3 + 'px';作为渲染任务加入到渲染队列中。
- 运行结束 渲染队列依然有内容 在渲染时机到来 再次触发回流重绘 重新渲染页面。
所以一共触发了三次回流重绘。
渲染优化
优化的本质:
减少浏览器渲染管道的计算量 (样式计算、布局、绘制、合成
等阶段的耗时),最终目标是保障帧率(如 60fps)
1:降低渲染频率
- 批处理 DOM 操作:将多次 DOM 更新合并为一次(如虚拟 DOM 的 Diff 算法、框架的批量更新机制)。
- 避免高频触发回流/重绘 :例如在
resize
、scroll
等事件中使用防抖/节流。 - 使用
requestAnimationFrame
:将动画逻辑对齐到浏览器的渲染周期,避免无效更新
2:减少渲染范围
- 通过
will-change
或transform
创建独立的合成层,将变化限制在局部(视图分层)。 - 使用 CSS
contain: strict
声明元素的布局/样式独立性,减少影响范围。 - 按需更新仅更新可视区域内的内容(如虚拟滚动)。
- 惰性加载非关键资源(图片、组件)。
3:提高渲染效率
- 优先使用合成器(Compositor)线程 :使用
transform
和opacity
实现动画,跳过布局和绘制阶段,直接进入合成。 - 避免强制同步布局(Layout Thrashing) :避免在 JavaScript中交替读写
offsetWidth
等布局属性,导致浏览器被迫立即计算布局 - 简化选择器复杂度 :减少 CSS 选择器的层级(如避免
div:nth-child(3) > a:hover
)。 - 减少样式冗余:避免频繁修改内联样式,改用 CSS 类切换。
- 视图分层(分层机制):本质是通过创建合成层,将渲染工作分摊到 GPU,并减少重绘区域。
4:利用硬件加速与并行化
- GPU 加速 :通过
transform: translateZ(0)
等属性触发 GPU 渲染。 - Web Workers:将非 UI 计算任务(如数据预处理)移至后台线程,减轻主线程压力。