大家好,我是徐徐。今天我们聊聊浏览器的渲染绘制过程,看看渲染树是如何展示到屏幕上的。
前言
之前我们讨论了一下浏览器渲染的基石:DOM Tree 与 CSSOM,简单的说明了一下他们的基本概念和形成过程,有了他们,页面的渲染就有了基础,但真正的渲染魔法发生在之后的步骤中。让我们继续探索浏览器如何利用这些信息创建出最终的视觉输出。
1. 渲染树 (Render Tree) 的构建
渲染树是将 DOM 树与 CSSOM 结合的结果, 它代表了最终要在屏幕上绘制的内容。
详细过程:
- 遍历 DOM 树: 从根节点开始, 递归遍历每个可见的节点。
- 匹配样式 : 对每个 DOM 节点, 查找并应用 CSSOM 中的匹配规则。
- 过滤不可见元素: 排除不需要渲染的节点, 如
<script>,<meta>, 以及display: none的元素。 - 处理伪元素: 为
:before和:after等伪元素创建渲染对象。
代码示例:
function createRenderTree(domRoot, cssom) {function isNodeVisible(node) {
return node.nodeType === Node.ELEMENT_NODE &&
getComputedStyle(node).display !== 'none';
}
function createRenderObject(domNode) {const styles = matchStyles(domNode, cssom);
return {
domNode,
styles,
children: []};
}
function processNode(domNode) {if (!isNodeVisible(domNode)) return null;
const renderObject = createRenderObject(domNode);
for (let child of domNode.childNodes) {const childRenderObject = processNode(child);
if (childRenderObject) {renderObject.children.push(childRenderObject);
}
}
return renderObject;
}
return processNode(domRoot);
}
优化建议:
- 减少 DOM 深度和复杂性, 以加快渲染树的构建。
- 使用更具体的选择器, 减少样式匹配时间。
2. 布局 (Layout) 过程
布局阶段计算每个渲染对象的精确位置和大小。
详细过程:
- 确定视口大小 : 根据浏览器窗口确定可用空间。
- 计算相对单位 : 将如
em,vh,vw等相对单位转换为绝对像素值。 - 级联布局: 从顶到底计算每个元素的几何信息。
- 对于块级元素, 计算其在主轴上的大小和位置。
- 对于内联元素, 计算行框和确定换行位置。
- 处理浮动和定位元素: 根据
float和position属性调整元素位置。 - 处理溢出 : 计算滚动区域和裁剪区域。
代码示例:
function performLayout(renderObject, parentLayout) {
const computedStyle = renderObject.styles;
const layout = {
x: parentLayout.x,
y: parentLayout.y,
width: computeWidth(renderObject, parentLayout),
height: computeHeight(renderObject, parentLayout)
};
if (computedStyle.position === 'absolute' || computedStyle.position === 'fixed') {applyPositioning(layout, computedStyle, parentLayout);
}
for (let child of renderObject.children) {performLayout(child, layout);
}
renderObject.layout = layout;
}
function computeWidth(renderObject, parentLayout) {// Complex width calculation logic here}
function computeHeight(renderObject, parentLayout) {// Complex height calculation logic here}
function applyPositioning(layout, computedStyle, parentLayout) {// Apply positioning logic}
优化建议:
- 使用 Flexbox 或 Grid 布局, 它们通常比传统布局方法更高效。
- 避免强制同步布局(forced synchronous layout), 即在 JavaScript 中交替读取布局信息和设置样式。
3. 绘制 (Painting) 阶段
绘制阶段将布局阶段的计算结果转化为实际的像素。
详细过程:
- 创建图层(Layer): 某些元素可能会被提升到单独的图层。
- 绘制顺序确定: 按照特定顺序 (如 z-index) 确定绘制顺序。
- 绘制命令: 为每个渲染对象生成一系列绘制命令。
- 栅格化(Rasterization): 将矢量信息转换为像素。
绘制顺序通常如下:
- 背景颜色
- 背景图像
- 边框
- 子元素
- 轮廓
代码示例:
function paint(renderObject, context) {const { layout, styles} = renderObject;
// 背景
if (styles.backgroundColor) {
context.fillStyle = styles.backgroundColor;
context.fillRect(layout.x, layout.y, layout.width, layout.height);
}
// 边框
if (styles.borderWidth && styles.borderStyle !== 'none') {
context.strokeStyle = styles.borderColor;
context.lineWidth = styles.borderWidth;
context.strokeRect(layout.x, layout.y, layout.width, layout.height);
}
// 文本
if (renderObject.text) {
context.fillStyle = styles.color;
context.font = `${styles.fontSize} ${styles.fontFamily}`;
context.fillText(renderObject.text, layout.x, layout.y + layout.height);
}
// 递归绘制子元素
for (let child of renderObject.children) {paint(child, context);
}
}
优化建议:
- 使用
will-change属性提示浏览器哪些属性可能会改变。 - 对于复杂的绘制操作, 考虑使用 Canvas 或 WebGL。
4. 合成(Compositing)
合成是将所有绘制的图层合并成最终图像的过程。
详细过程:
- 图层分析: 确定哪些元素应该在自己的图层上。
- 图层树构建: 创建一个表示图层之间关系的树状结构。
- 图层绘制: 独立绘制每个图层。
- 合成: 按照正确的顺序将图层组合在一起。
代码示例:
class Layer {constructor(renderObject) {
this.renderObject = renderObject;
this.children = [];}
addChild(childLayer) {this.children.push(childLayer);
}
paint(context) {
// 绘制当前图层的内容
paintRenderObject(this.renderObject, context);
// 绘制子图层
for (let child of this.children) {child.paint(context);
}
}
}
function createLayerTree(renderObject, parentLayer) {const layer = new Layer(renderObject);
if (parentLayer) {parentLayer.addChild(layer);
}
if (needsOwnLayer(renderObject)) {
// 为需要独立图层的渲染对象创建新的图层
for (let child of renderObject.children) {createLayerTree(child, layer);
}
} else {
// 否则, 继续使用当前图层
for (let child of renderObject.children) {createLayerTree(child, parentLayer);
}
}
return layer;
}
function needsOwnLayer(renderObject) {
// 判断渲染对象是否需要自己的图层
return renderObject.styles.willChange === 'transform' ||
renderObject.styles.opacity < 1 ||
renderObject.styles.position === 'fixed';
}
优化建议:
- 适度使用图层: 过多的图层会增加内存使用和管理复杂度。
- 使用
transform和opacity进行动画, 因为这些属性可以在合成阶段高效处理。
5. 渲染优化实践
减少布局抖动 (Layout Thrashing)
布局抖动(Layout Thrashing)是指由于 JavaScript 和 CSS 样式的修改导致浏览器频繁计算和重新渲染布局,从而引发性能问题。它通常发生在读取和修改样式下:在对 DOM 进行修改后立即读取样式,可能会触发重新计算布局。例如,使用 offsetHeight 或 getComputedStyle 读取元素的尺寸或样式,会导致浏览器立即计算布局。
优化策略:
- 批量操作:将多个 DOM 修改操作批量执行,减少频繁的布局计算。例如,使用
documentFragment来批量插入节点。 - 读取样式后再修改:先完成所有样式修改,再读取样式。例如:
// 先修改样式
element.style.width = '100px';
element.style.height = '100px';
// 然后读取样式
let width = element.offsetWidth;
- 使用
requestAnimationFrame:在下一次重绘前处理 DOM 更新,减少不必要的布局计算。
优化绘制性能
绘制性能优化关注于减少页面渲染的开销,特别是降低重绘和合成的成本。
优化策略:
- 减少重绘区域:通过限制元素的重绘区域来提高性能。使用
clip属性可以避免全局重绘。 - 减少复杂的背景和边框:复杂的背景图像和边框样式会增加绘制成本。简化这些样式有助于提升性能。
使用 will-change 属性
will-change 属性是一个优化工具,允许开发者告知浏览器某些元素将发生变化,从而使浏览器预先优化这些元素的渲染。
用法:
.element {will-change: transform, opacity;}
优化策略:
- 适度使用:
will-change可以提高性能,但过度使用可能导致浏览器创建过多的合成层,反而影响性能。 - 只在必要时使用:仅在元素需要动画或过渡时使用
will-change,并在动画结束后移除该属性。
合理使用图层
合成层允许浏览器在独立的层中处理部分元素,从而加速渲染。
优化策略:
- 使用硬件加速:使用 CSS 属性如
transform和opacity来触发硬件加速和合成层。
.animated-element {transform: translateZ(0); /* 启用硬件加速 */
}
- 避免过多图层:过多的合成层会增加合成开销。尽量减少合成层的数量,仅对那些需要频繁更新的元素使用合成层。
高效的动画实现
CSS Transitions 和 Animations
- CSS Transitions:适用于元素状态之间的平滑过渡。非常适合简单的动画效果,如按钮悬停状态变化。
.button {transition: background-color 0.3s ease;}
.button:hover {background-color: red;}
- CSS Animations:用于更复杂的动画效果,支持关键帧(keyframes)。适合复杂或多步骤的动画。
@keyframes slide {from { transform: translateX(0); }
to {transform: translateX(100px); }
}
.box {animation: slide 1s infinite;}
requestAnimationFrame 的正确使用
requestAnimationFrame 是用于高效实现动画的 API。它让浏览器在下一次重绘前执行动画更新,提高性能和流畅度。
使用示例:
function animate() {
// 更新动画状态
element.style.transform = `translateX(${position}px)`;
// 请求下一帧动画
requestAnimationFrame(animate);
}
animate();
优化策略:
- 避免计算重绘:在
requestAnimationFrame回调中只做动画更新,不做复杂计算。 - 减少重复调用:确保
requestAnimationFrame调用时机合适,避免无效的重复请求。
现代浏览器的渲染优化技术
**异步渲染和渐进式渲染 **
- 异步渲染:现代浏览器通过异步渲染技术来平滑渲染过程,避免主线程阻塞。利用
async和defer属性来优化脚本加载,减少渲染阻塞。 - 渐进式渲染:浏览器逐步渲染内容,提高用户感知的加载速度。例如,浏览器先显示页面的初步内容,然后逐步加载和渲染其他内容。
并行渲染技术
- Web Workers:允许在后台线程中运行脚本,不阻塞主线程。适合处理计算密集型任务,减少主线程的负担。
- GPU 加速:利用 GPU 处理图像和动画渲染,减轻 CPU 负担。现代浏览器通过 WebGL 等技术实现 GPU 加速。
浏览器的智能优化策略
- 智能渲染策略:现代浏览器使用智能算法来优化渲染过程。例如,智能判断哪些部分需要重绘,减少不必要的绘制开销。
- 预加载和缓存:浏览器会智能地预加载和缓存资源,减少页面加载时间。例如,使用
<link rel="preload">预加载关键资源。
结语
理解浏览器的渲染绘制过程不仅有助于创建更高效的网页,还能帮助诊断和解决性能问题,当然上面提到的只是比较常见的流程和渲染优化方案,在实际的渲染绘制过程中每一大步都有很多细小的步骤组成,我们有的时候可能需要借助 Chrome DevTools 的 Performance 面板分析去分析每一个步骤。