Canvas-vs-SVG-vs-WebGL
1. 问题先分类,再选表面
| 你真正要的 | 优先 | 不要先上 |
|---|---|---|
| 可命中、可选择、可给读屏、参与 CSS 布局的图形 UI | DOM / SVG | Canvas 2D |
| 少量矢量、缩放不失真、CSS 动画、CSS 滤镜 | SVG | Canvas |
| 像素效果、笔刷、滤镜烘焙、视频帧处理 | Canvas 2D | SVG |
| 粒子、大量精灵、自定义着色、三维 | WebGL / WebGPU | 用 2D 硬撑 |
| 一个图标、一条进度、圆角卡片 | CSS | 任何 canvas |
Canvas 2D 适用,当 帧的真相是像素,且对象语义由你自己的场景图提供。SVG 适用,当 帧的真相是文档节点。WebGL 适用,当 帧的真相是 GPU 程序。
2. Canvas 2D:强项和死线
强项
- 笔刷、涂抹、模糊、混合模式、把多层烘焙成一张图
- 每帧对象数量中等(百~千级 path),但每个对象很「像素」
- 和视频/相机帧、
ImageBitmap、OffscreenCanvas Worker 同一套位图模型 - 导出就是读 backing store,模型简单
死线(碰到就该换或叠一层)
- 文本要能选中、复制、IME、读屏:叠 DOM,或整体不要 Canvas
- 无限画布 + 矢量缩放还要保持几何真值:场景用矢量存,Canvas 只做视口光栅;或主编辑器 SVG
- 对象 10⁴ 以上还要每帧独立样式:2D 同步光栅打不过;上 WebGL instancing
- 需要光照、深度、自定义 shader:不是 2D 的活
3. SVG:不是「慢的 Canvas」
SVG 是保留模式。节点还在,命中走 DOM hit-testing,变换是 attribute/CTM,无障碍可以挂 title / role。
代价:
- 节点多时 layout + paint 比一张 canvas 清屏更痛(路径数量到几千就要认真测)
- 像素级笔刷、每帧全屏滤镜,SVG 会逼你把效果烘到
<image>,最后还是位图 - 跨源 SVG 当
img画进 canvas 仍有 taint 规则
混用是生产常态:SVG 做可交互结构,Canvas 做预览层 / 笔迹层。不要强迫一种表面干两种语义。
4. CSS:先问能不能不用图 API
transform、opacity、filter、mix-blend-mode、clip-path 走合成器,动画不进 JS 光栅。一张会动的卡片、一个粒子感的背景(有限个数),CSS 往往更便宜、更好维护。
Canvas 背景动画的典型失败:为了「更有质感」开一张全屏 2x DPR canvas 跑噪声,结果滚动时主线程 8ms 全给了 fillRect。同样效果用 CSS + 一张静态 PNG/WebGL 全屏 shader(真要噪声)更干净。
5. WebGL / WebGPU:只在这里出现
上 GPU API 的理由是 2D context 的同步 CPU/GPU 光栅模型不够,不是「看起来更高级」。
| 信号 | 含义 |
|---|---|
| 粒子/精灵过万,2D 分桶仍然掉帧 | 需要 instancing / 点精灵 |
| 每像素自定义公式(LUT、扭曲、光照) | 需要 fragment shader |
| 要与 Three.js / 地图引擎同一 GL 上下文 | 2D 插不进去,除非 texImage2D 上传 canvas,多一次拷 |
| 只要一个 blur | 先 2D filter 或预烘焙;不够再 shader |
WebGPU 比 WebGL 更接近现代 GPU,但能力探测、降级、移动端驱动,都是另一条产品线。本体系不展开。从 2D 迁出时,场景图可以留,光栅器换掉——这是 00 章自建 scene 的回报。
CanvasRenderingContext2D 和 WebGLRenderingContext 不能同时绑同一元素。要 2D 画 UI、GL 画场景,用两张 canvas 叠,或 GL 里自己画 UI。
6. 一个可执行的判定流程
type SurfaceKind = "css" | "svg" | "canvas2d" | "webgl";
export function chooseSurface(input: {
objectSemantics: "document" | "pixels" | "gpu-program";
interactiveText: boolean;
pixelEffects: boolean;
orderOfMagnitude: number;
}): SurfaceKind {
if (input.interactiveText && input.objectSemantics === "document") {
return "svg";
}
if (input.objectSemantics === "document" && !input.pixelEffects) {
return input.orderOfMagnitude < 2_000 ? "svg" : "canvas2d";
}
if (input.objectSemantics === "gpu-program" || input.orderOfMagnitude > 10_000) {
return "webgl";
}
if (input.pixelEffects) {
return "canvas2d";
}
return "css";
}
orderOfMagnitude 是每帧要独立更新的图元数,不是场景里存在的总节点数。静态一万个 SVG path 仍然可能比每帧重绘一千个 canvas path 更合适——测过再翻案。
7. 失败形态
| 症状 | 选错了什么 |
|---|---|
| 编辑器做不出选择/对齐/读屏 | 用 Canvas 替代了文档 |
| Retina 矢量发糊还要对齐像素 | 把 SVG 问题当成了 Canvas 清晰度问题 |
| 粒子 2D 分桶后仍 20fps | 该 WebGL 还在 2D |
| 全屏 canvas 背景拖垮滚动 | 该 CSS/静态图 |
| 同一元素又要 2d 又要 webgl | 违反 getContext 单例 |
权威资料
- MDN — Canvas vs SVG(性能侧;选型还需结合 SVG 文档模型)
- MDN — SVG
- Khronos — WebGL
- W3C — WebGPU
核对日期:2026-08-26