深入理解 CPU 和 GPU 渲染机制

222 阅读11分钟

在图片渲染的过程中,CPU 和 GPU 扮演了截然不同但又紧密协作的角色,它们各自其实都可以实现图片的渲染,那你有没有想过它们之间的差异究竟有何不同?下面就让我们一起揭开图片渲染的工作原理,探索 CPU 和 GPU 渲染的奥秘,以及它们在性能优化中的实际应用。

一、图片渲染过程

首先,我们先来思考一下一张图片究竟是如何渲染在屏幕上的呢?

浏览器图片渲染是一个从文件加载到最终绘制的复杂过程,涉及网络传输、图片解码、位图生成以及 GPU/CPU 渲染等多个环节。其渲染过程如下:

image.png

二、CPU 和 GPU 渲染

或许有人会提出疑问,解码后的图片需要绘制到屏幕上,浏览器会通过 GPU 和渲染管线进行处理,上图为什么没有体现 GPU 渲染的环节?

其实在浏览器处理图片渲染时,不一定都会用到 GPU 渲染,使用 CPU 也一样可以渲染,只不过使用 CPU 渲染容易出现性能瓶颈而已。

在计算机图形学中,图形渲染可以通过 CPU(中央处理器)或 GPU(图形处理单元)来完成。CPU 和 GPU 在处理图像工作的过程中有不同的工作模式和性能特征,下面让我们一起来看下这两种渲染方式的过程。

CPU 渲染图片

image.png

  • CPU 首先会接收准备好的图形数据,包括顶点坐标、颜色、纹理等信息。

  • CPU 逐个处理这些顶点的坐标,进行变换(比如世界坐标到屏幕坐标的变换),通常涉及到矩阵multiplication 和其他数学运算。

  • CPU 会将这些顶点转换为实际在屏幕上显示的像素。这个过程可能需要进行三角形的拆分、插值计算(确定颜色、深度、法线等属性),这个过程也叫光栅化,复杂的图片会涉及到很大的计算,这是瓶颈之一。

  • 在渲染过程中,CPU 计算光照(如漫反射、镜面反射)和阴影,然后将这些信息应用到像素上。

  • 在图像渲染的最后一步,CPU 将所有的像素信息合成一个图像。对于复杂场景,可能还需要通过多次取样(如抗锯齿)来提高图像质量,这个过程会涉及大量缓存区数据的读写操作,这是渲染瓶颈之二。

  • 最后,将渲染好的图像输出到显示设备。

接下来让我们看一看 GPU 的渲染过程。

GPU 渲染图片

image.png

  • CPU 将图形数据(顶点、纹理等)上传到 GPU 内存中。GPU 预期这些数据在渲染过程中会被多次使用。

  • 渲染过程中使用着色器(GPU 编程中独有的)来处理顶点和像素。着色器是运行在 GPU 上的程序,包括顶点着色器、片段着色器等。

  • GPU 的强大之处在于它可以同时处理大量的顶点和像素。不同的块可以同时并行地执行相同的代码,因此可以在短时间内处理大量的数据。其并行计算处理能力,这是与 CPU 渲染最大的不同之处。

  • GPU 会将处理后的顶点数据光栅化,将其转换为实际在屏幕上显示的像素,这一步GPU尤其高效,单位时间内处理海量图像数据,实现高吞吐量

  • GPU 在光栅化后立即开始处理光照、纹理映射和其他特效(如反射、阴影等)。着色器允许高度的灵活性和复杂性。其充分利用计算机的硬件加速,优化了常见图像操作,如纹理映射、光栅化、抗锯齿等。

  • GPU 将渲染好的图像合成并输出到缓冲区,最后将其显示在屏幕上~

上述是 2D 图像的渲染过程,而针对 3D 图像渲染的场景又会变得更复杂,因为它涉及模型、灯光、纹理等各环节,这里引用他人的一张图,原理其实大致都差不多,但会有更多的阶段细分,过程如下:

image.png

(图片来源: 知乎-灵知子)

GPU 和 CPU 渲染的区别

从芯片设计原理上来分析,中央处理单元(CPU) 设计上是通用计算核心,适合复杂逻辑计算和任务调度,而图形处理单元(GPU)设计上专注于并行计算,非常适合处理大量相同类型的操作,如像素着色和矩阵计算等。

它们原本的职责功能就不一样,虽然都能用于处理图形的渲染工作,但两者还是有一定的差别,对比如下:

特性GPU 渲染CPU 渲染
并行计算超强(支持数千个并行线程)较弱(线程数量有限)
适合任务大规模数据、重复任务复杂逻辑、动态任务
速度高速(尤其是大规模任务)较慢(受限于核心数量)
实时性优秀(支持高帧率)较差(耗时较长)
计算精度较低(精度适中)较高(支持高精度)

CPU 则胜在灵活性和精确性,适合逻辑复杂的离线渲染任务。

GPU 优势在于速度和并行处理,适合需要实时渲染或大量重复计算的场景。

实际业务当中,CPU 具有灵活性,GPU 具有高效计算能力,两者是相辅相成的关系,配合利用得当,才能实现复杂图形的最佳渲染效果。

三、GPU加速

既然 GPU 原本就是因图形处理而生,那我们日常中该如何将它应用到我们的图形渲染优化当中呢?这其实也是很多面试官经常提到的问题,如何启用 GPU 加速?

其实一些现代图像格式(比如 WebP、SVG 等)可能会在浏览器内部使用 GPU 加速解析。尽管这些图像本身的加载仍由 CPU 负责,后续的绘制操作可能会通过 GPU 提升性能。

从点到面,从图片渲染到图层渲染,我们在实际性能优化场景中如何利用硬件加速(GPU 加速)?

在前端开发中,启用 GPU 加速的方式其实有两种,一种是利用 CSS,另一种则通过使用 JavaScript,利用得当,两者都可以显著提高动画和视觉效果的性能。下面分别介绍如何通过 CSS 和 JavaScript 启用 GPU 加速。

CSS 启用 GPU 加速

GPU 加速的核心是将特定的渲染任务交给 GPU 处理,而不是依赖 CPU。以下是通过 CSS 启用 GPU 加速的一些方法:

1. 使用 translateZ(0)translate3d(0, 0, 0)

CSS transform 是启用 GPU 加速的最常用方式。通过定义 2D 或 3D 转换,浏览器会创建一个独立的合成层,并将该层交给 GPU 处理。

.div {
    transform: translateZ(0); /* 强制使用 GPU */
}
2. 使用 will-change

will-change 提前告诉浏览器某个属性即将发生变化,优化渲染路径。

.div { 
    will-change: transform, opacity; /* 预告浏览器优化 */ 
}
3. 使用 opacity

opacity 改变元素透明度时通常会触发 GPU 加速。

.div { 
    opacity: 0.5; /* 渐变效果通过 GPU 渲染 */
}
4. 过渡动画中使用opacity或transform

CSS 动画或过渡中使用transform、opacity、filter会触发 GPU 加速。

@keyframes slideIn { 
    from { 
        transform: translate3d(-100%, 0, 0); /* GPU 渲染 */ 
    } 
    to { 
        transform: translate3d(0, 0, 0); /* GPU 渲染 */ 
    } 
} 

.div { 
    animation: slideIn 1s ease-out; 
}

JavaScript 启用 GPU 加速

其实 JavaScript 本身并不直接与 GPU 通信,但可以通过操作 DOM 元素触发 GPU 渲染路径。从上面我们可以知道,CSS 是如何让 GPU 加速的,因此,在 JavaScript 中,通过操作 CSS 属性也可以来启用 GPU 加速。

1. 操作 CSS 3D 相关的属性

改变 CSS 属性:如 transformopacitywill-change 等,其实本质上是 css 启动 GPU 的另一种形式,这里不再赘述。

const box = document.querySelector('.box');

// 设置 transform,启用 GPU 加速
box.style.transform = 'translate3d(100px, 0, 0)';
2. 动画使用 requestAnimationFrame

当你进行动画时,使用 requestAnimationFrame 可以提供平滑的渲染并有效利用 GPU。

const box = document.querySelector('.animationBox');
let start = null;

function animate(timestamp) {
  if (!start) start = timestamp;

  const progress = timestamp - start;
  box.style.transform = `translateY(${Math.min(progress / 2, 200)}px)`; // 使用 transform

  if (progress < 400) {
    requestAnimationFrame(animate);
  }
}

// 触发动画
requestAnimationFrame(animate);
3. 使用 WebGL

如果需要更高级的 GPU 操作,可以通过 WebGL 与 GPU 直接交互。

const canvas = document.querySelector('canvas');
const wgl = canvas.getContext('webgl');

// 设置清屏颜色
wgl.clearColor(0.0, 0.0, 0.0, 1.0);
wgl.clear(gl.COLOR_BUFFER_BIT);

以上就是两种启动 GPU 加速的一些具体方法,通过合理使用 CSS 和 JavaScript,开发者可以有效启用 GPU 加速,从而提升页面的动画性能和用户体验。

但在实际业务中,启动 GPU 加速与另外一个概念息息相关,那就是合成层。从上面图像在浏览器上的渲染原理,我们就可以看出,光栅化后会有一个合成图像的过程,因此合成层的提升也至关重要。

四、合成层的提升

1. 什么是合成层?

当浏览器渲染网页时,通常会将页面拆分成多个图层,这些图层分别负责不同的部分(例如背景、文本、图像等)。合成层允许浏览器在 GPU 上渲染这些层,并在最终一步将它们合成到屏幕上。

image.png

举个不太恰当但形象的例子,接触过 Photoshop(PS)的都知道,图像的最终形态是由右侧小窗口一张张图层叠加而成,而合成层的概念就与 PS 合成最终图像形态的过程类似,而直接影响合成层的就是最上面那几张图层。

2. 合成层的作用

合成层(Compositing Layers),尤其是在处理动画和复杂布局时,合成层的提升有助于确保这些操作在 GPU 上进行,从而减少 CPU 的负担。这种方法相比传统的渲染方式可以减少重绘(Repaint)和重排(Reflow)的次数,提高渲染效率。

3. 如何提升合成层?

要提升合成层的使用,这个过程与启用 GPU 加速操作非常类似,因此,您可以采用以下几种策略:

1)使用 will-change 属性

使用 CSS 的 will-change 属性可以告知浏览器即将对某一元素进行变化,这样浏览器可以提前创建合成层。

.div {
  will-change: transform, opacity; /* 提升元素的合成层 */
}
2)使用 CSS 转换和过渡
  • 应用 3D 转换: 使用 translateZ(0)translate3d(0, 0, 0),可以强制将元素推入合成层。
.div {
  transform: translateZ(0); /* 强制创建合成层 */
}
  • 过渡动画: 在进行动画时,使用 transformopacity 等属性,这些属性的操作会尽量在合成层中处理。
.box {
  transition: transform 0.3s, opacity 0.3s;
}
3)避免过大的合成层

虽然合成层能够提高性能,但过多或过大的合成层也会导致性能问题。不必要的合成层会增加 GPU 的内存使用,因此,在设计布局时,应合理安排需要提升的元素。

4)使用缓存的图像

图像常常在合成层中创建,确保图像的尺寸合适并尽量减少像素大小,以提升性能。例如,可以使用 image-rendering 属性来提高图像的渲染效果。

.img {
  image-rendering: crisp-edges; /* 优化图像渲染 */
}
5)简化图层结构

尽可能减少 DOM 层级和图层数量。复杂的布局可能会阻碍浏览器合成层的使用,简化 DOM 结构可以提高性能。

4. 隐式合成层

这个概念也许你听过,但并可能不清晰。隐式合成层,是相对显式合成层而言的,上面所描述的其实都是显式合成层,而所谓隐式合成层,就是指那些图层原本不是合成层,但浏览器基于性能优化自动创建的合成层。

什么意思呢?举个简单的场景来讲,那种 z-index 在合成层之上的图层。这种图层即使不是合成层,也会被转换成为合成层。

.div1 {
  transform: translateZ(0);
  z-index: 100;
}

.div2 {
  font-size: 12px;
  z-index: 1000;
}

上面例子当中,div1 是合成层,但 div2 为普通图层,但由于它位于 div1 之上,出于性能优化的目的,浏览器最终会将它提升为合成层,这种就是隐式合成层。

因此,我们在写 CSS 的时候,一定要注意不要过度地滥用z-index属性,认为值越大越好,过多地使用这种图层有时候会适得其反。

当然这只是隐式合成层其中的一种方式,还有动画与过渡、滚动优化、混合模式、遮罩、3D 变换等场景都会涉及到隐式合成层。

它的核心目标是减少重排和重绘的范围,提升渲染性能。

通过上面的分析,相信你对图形的渲染机制有了进一步的认识,码字不易,也欢迎大家一键三连和评论~