skip to content
City 的博客

Search

浏览器渲染

13 min read

浏览器渲染流程及其相关概念。

浏览器渲染流程

首先需要了解浏览器将 HTML 渲染成屏幕上像素的基本流程:

  1. 解析 HTML,生成 DOM 树
  2. 解析 CSS,生成 CSSOM 树
  3. DOM 树和 CSSOM 树结合起来,生成渲染树。渲染树只包含需要显示在页面上的节点及其样式信息。
  4. 布局 (Layout / Reflow):根据渲染树,计算出每个节点在屏幕上的确切位置和尺寸(几何信息)。这个过程就是回流
  5. 绘制 (Paint / Repaint):根据布局计算出的信息,将每个节点绘制成屏幕上的实际像素。这个过程就是重绘
  6. 合成 (Composite):将绘制出的各个层(Layers)按照正确的顺序合并在一起,最终显示在屏幕上。

回流与重绘

这两个是浏览器渲染中最重要的概念,其中回流必然会引起重绘,而重绘不一定会引起回流。

回流

回流是指当元素的几何属性(如尺寸、位置、边距等)发生变化,导致浏览器需要重新计算元素的几何结构和位置的过程。

特征

  • 开销巨大:回流是一个成本非常高的操作。一个节点的回流,通常会导致其所有子节点、甚至后续兄弟节点和祖先节点的回流。
  • 连锁反应:就像移动了房子里的一件大家具,可能整个房间的布局都需要重新调整。

常见触发回流的操作

  1. 页面首次渲染:这是不可避免的一次。
  2. DOM 节点的操作
    • 添加或删除可见的 DOM 节点。
  3. 元素几何属性的改变
    • 修改 width, height, margin, padding, border, top, left 等。
  4. 字体改变font-size 的变化会影响文字所占的空间,从而触发回流。
  5. 浏览器窗口尺寸改变 (resize)。
  6. 获取某些特定属性值(强制同步布局)
    • 这是一个非常重要的性能陷阱。当你用 JavaScript 读取某些需要即时计算的属性时,浏览器为了给你一个准确的值,必须立即执行待处理的渲染队列,强制触发回流。
    • 这些属性包括:offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientWidth, getComputedStyle() 等。

重绘

重绘是指当元素的外观样式(如颜色、背景、可见性等)发生变化,但其几何属性没有改变时,浏览器需要重新将元素绘制出来的过程。

特征

  • 开销相对较小:因为重绘不涉及重新计算布局,只是重新“上色”,所以它的性能开销比回流小得多。

常见触发重绘的操作

  • 修改 color, background-color, outline, box-shadow, text-decoration 等不影响布局的样式。
  • 修改 visibility

GPU 加速合成

浏览器最初的使命是展示静态的超文本文档,这个任务对于 CPU 来说绰绰有余。但随着 Web 2.0、富交互应用以及 HTML5 的兴起,网页承载的内容变得空前复杂和动态。这迫使浏览器必须从一个“文档阅读器”进化为一个高性能的“应用平台”。这场进化中最关键的一步,就是将图形渲染的重担从 CPU 分摊给 GPU

GPU 的到来也带来了 CSS 动画与过渡、视频解码与播放、Canvas 2D/3D、页面合成、滤镜(filter)、backdrop-filter 等复杂的CSS视觉效果。

我们这里主要讲的是页面合成,我们来看看它的原理。

原理

通过特定的 CSS 属性,我们可以将一个 DOM 元素提升到它自己的合成层。可以把它想象成 PhotoShop 里的一个独立图层。 一旦一个元素位于独立的图层上,对它进行 transformopacity 的动画操作,就相当于只移动或改变这个图层的属性,而无需影响和重新绘制页面上的其他图层。这个任务被直接交给了 GPU完全绕过了 CPU 的回流和重绘,因此性能极高,能轻松实现流畅的 60fps 动画。

开启方法

你可以通过以下方法开启它。

现代化做法 transform opacity

这是最推荐、最标准的做法。现代浏览器非常智能,当你对一个元素使用 transformopacity 属性来实现动画时,浏览器会自动判断并可能将其提升到独立的合成层以进行优化。

  • 不推荐的做法(触发回流,CPU计算):

    .box {
      position: absolute;
      left: 10px;
      transition: left 0.5s;
    }
    .box:hover {
      left: 100px;
    }
  • 推荐的做法(触发GPU加速):

    .box {
      transition: transform 0.5s;
    }
    .box:hover {
      transform: translateX(90px); /* 移动90px,效果同上 */
    }

在实现位移、缩放、旋转、透明度等动画效果时,优先且务必使用 transformopacity

明确的提示 will-change

will-change 是一个专门设计用来提前告知浏览器“这个元素的某个属性即将发生变化,请提前做好优化准备”的 CSS 属性。

当你给一个元素设置了 will-change,浏览器会提前将该元素提升到独立的合成层,从而在动画开始时避免了“提升”操作带来的延迟。

  • 使用方式

    .box {
      /* 告诉浏览器,这个元素的 transform 属性将要改变 */
      will-change: transform;
    }
  • 最佳实践

    • 不要滥用:不要给页面上大量的元素都加上 will-change,这会占用大量 GPU 内存,适得其反。
    • 用完即删:最好的方式是在交互即将发生时(如 :hover 或通过 JavaScript)添加 will-change,在动画结束后再将其移除。
    const box = document.querySelector('.box')
     
    box.addEventListener('mouseenter', () => {
      box.style.willChange = 'transform'
    })
     
    box.addEventListener('transitionend', () => {
      box.style.willChange = 'auto' // 动画结束后移除
    })

隐藏黑科技 translateZ(0)

will-change 属性出现之前,开发者们发现了一个“欺骗”浏览器的方法来强制开启 GPU 加速。

  • 实现方式

    .box {
      transform: translateZ(0);
      /* 或者 transform: translate3d(0, 0, 0); */
    }
  • 原理:当你使用 3D 变换属性时,浏览器会认为这个元素处于一个三维空间中,为了正确处理层叠关系,它会强制为该元素创建一个独立的合成层。translateZ(0) 本身并不会在视觉上产生任何 3D 效果,但能触发这个机制。

  • 现状:现代浏览器已经足够智能,这种 hack 的必要性大大降低。但它在某些场景下仍然有用,比如解决一些特定浏览器下的闪烁或渲染 bug,通过手动提升层级来修复。

独立合成的副作用

元素被提升至独立的合成层后,这个操作在带来巨大性能优势的同时,也像硬币的两面,会产生一些重要的“副作用”。

理解这些副作用,能帮助我们避免写出带有“灵异现象”的 Bug,甚至可以反过来巧妙地利用它们,构建出更健壮、更高性能的组件。

改变 position: fixed 的包含块

这是最常见也最容易让人困惑的一个副作用。

  • 正常行为position: fixed 的元素的定位基准(包含块)是浏览器视口 (Viewport)
  • 副作用:当一个元素的任何祖先元素被提升为合成层后(例如,设置了 transform: translateZ(0)),这个被提升的祖先元素就变成了其后代中 position: fixed 元素新的包含块

理论结合实践:

想象一个模态框(Modal),通常我们会用 position: fixed 让它在屏幕中央。

transform 的情况

<body>
  <div class="content">...页面内容滚动...</div>
  <div class="modal">我相对于浏览器窗口固定</div>
</body>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
}

这里的 Modal 会完美地固定在屏幕正中央。

祖先元素有 transform 的情况

现在,假设我们为了给整个页面添加一个微妙的缩放动画,在 <body> 上加了 transform

body {
  transition: transform 0.3s;
}
body.zoomed-in {
  transform: scale(0.95);
}
 
.modal {
  position: fixed;
  /* ... */
}

此时,<body> 元素创建了一个新的包含块。Modalposition: fixed 将不再相对于浏览器视口,而是相对于被缩放的 <body>。当你滚动页面时,这个 fixedModal 会跟着页面一起滚动,完全失去了固定的效果。

独立的绘制与帧上下文

这个副作用有利有弊,我们把它拆开来看。

  • 绘制隔离 (Paint Isolation) - 优点
    • 当一个元素被提升到自己的合成层后,它就拥有了独立的“画板”。对这个层进行的任何重绘 (Repaint) 都不会影响到页面上的其他层。
    • 实践:一个持续播放的 CSS 动画(如旋转的 loading 图标)在一个独立的层上进行,它就不会导致整个页面的背景或其他静态元素被反复重绘,极大地提升了渲染效率。
  • 渲染差异与开销 - 缺点
    1. 字体渲染变更:在某些操作系统(特别是 Windows)上,GPU 的文本抗锯齿方式(灰度抗锯齿)与 CPU(亚像素抗锯齿)不同,这可能导致被提升到合成层上的文字看起来比普通文字更模糊或更细
    2. 内存与性能开销:每个合成层都需要消耗额外的 GPU 显存和系统内存。将一个元素提升为层的过程(上传纹理到 GPU)本身也有开销。如果滥用 GPU 加速,给页面上成百上千个小元素都加上 transform: translateZ(0),会导致内存暴增、初始化变慢,反而使页面变得更卡顿,尤其是在移动设备上。这就是所谓的“层爆炸 (Layer Explosion)”。