Skip to main content

Canvas 与 SVG 的选型

两者都能画图,但本质不同:SVG 是保留模式的矢量 DOM,Canvas 是即时模式的位图画布。

1. 保留模式与即时模式​

根本差别在渲染模型:SVG 是保留模式,每个图形都是一个真实 DOM 节点,浏览器帮你记住它、可以单独绑事件、可以被 CSS 影响、缩放无损;Canvas 是即时模式,画完只剩像素,浏览器不记得画过什么,想交互就得自己做命中检测。后面所有的取舍——事件、可访问性、性能、导出——都是这一条推导出来的。能不能单独操作一个图形,是第一道分水岭。

SVG:保留模式每个图形都是一个 DOM 节点可单独绑事件,CSS 也能作用节点一多,重排重绘就贵Canvas:即时模式画完只剩像素,没有节点命中检测、重绘范围都得自己管元素再多,每帧成本也基本恒定分水岭不是「图形多不多」,而是「要不要单独操作每一个图形」。
图:保留模式与即时模式——一边是可寻址的节点,一边是不可寻址的像素

2. 元素数量决定分水岭​

判断标准不是「图形多不多」,而是是否每帧都在改大量元素。几百个节点、偶尔改一个属性,SVG 更简单更好维护;节点上万或者逐帧重绘,Canvas 明显更稳。中间地带可以用混合方案:Canvas 画主体,DOM 或 SVG 叠一层交互层(选中框、tooltip、标注),这样既扛得住数量,交互又不用自己算命中。

高清屏下 Canvas 必须处理 devicePixelRatio,否则直接模糊;导出高清图也要按倍数重设画布再绘制。

function setupCanvas(canvas, cssW, cssH) {
const dpr = window.devicePixelRatio || 1
canvas.width = cssW * dpr
canvas.height = cssH * dpr
canvas.style.width = cssW + 'px'
canvas.style.height = cssH + 'px'
const ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr) // 之后按 css 像素绘制即高清
return ctx
}

3. 各自的地盘​

3.1 SVG 更适合​

图标、图表、流程图、需要交互与导出的图形——节点可单独操作、可被读屏识别、矢量导出无损。

3.2 Canvas 更适合​

游戏、粒子动画、图片编辑、大规模可视化——数量大或逐帧重绘时更稳;

  • 判断的核心维度是「是否需要单独操作每一个图形」以及「是否要导出矢量」。

一个容易误判的场景是图表:几十个数据点的柱状图,SVG 完全够用且白拿交互与读屏;只有当数据点上万、还要做实时刷新的散点/热力图,才值得为性能切换到 Canvas。不要因为「听说 Canvas 快」就在几百个节点时就上 Canvas,那会白白丢掉可访问性与交互便利。

3.3 语义、SEO 与导出差异​

SVG 每个图形是真实 DOM 节点,天然能被读屏与搜索引擎解析;Canvas 只是一块像素,需要额外提供文本替代(如 aria-label 或旁边配说明)才能让读屏用户理解。导出上,SVG 直接拿矢量;Canvas 要按 devicePixelRatio 倍数重绘再 toDataURL,否则出来是糊的。可访问性是 SVG 的隐性优势,做数据可视化时尤其值得考虑。

4. 两个都要时的做法​

编辑器类产品常用的架构是 Canvas 主体 + DOM/SVG 交互层:

<div class="stage">
<canvas id="scene"></canvas>
<!-- 交互层用 DOM/SVG,命中检测交给浏览器 -->
<svg class="overlay">
<rect class="selection" />
<circle class="handle" />
</svg>
</div>

静态层用离屏 Canvas 缓存,避免每帧重画:

const off = document.createElement('canvas')
drawStatic(off.getContext('2d')) // 只画一次
function frame() {
ctx.clearRect(0, 0, w, h)
ctx.drawImage(off, 0, 0) // 直接贴缓存
drawDynamic(ctx)
requestAnimationFrame(frame)
}

更进一步的优化是把 Canvas 绘制搬出主线程:OffscreenCanvas 可在 Worker 里绘制,主线程只负责把结果贴回页面,重绘密集时用户输入不再卡。它适合「渲染循环长期占用数毫秒」的图表或编辑器;轻量场景留在主线程反而更简单。

// 主线程:把 canvas 的控制权转移给 Worker,绘制在 Worker 里完成
const off = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: off }, [off])
// worker.js:在 Worker 里拿到 OffscreenCanvas 并绘制
self.onmessage = ({ data: { canvas } }) => {
const ctx = canvas.getContext('2d')
draw(ctx) // 渲染逻辑与页面几乎一致,只是不再阻塞主线程
}

两个常踩的坑要顺手提一下:一是「每帧都重画整块画布」——只变了小块时用 clearRect 清掉脏区域即可,不必全量重绘;二是文本若能缓存就不要每帧重画;三是高清屏上 devicePixelRatio 可能到 3,超大面积画布按 3 倍开辟位图会很吃内存,可以按性能预算把 DPR 上限钳到 2,避免为了清晰度付出翻倍的显存。

混合方案是编辑器类产品的常规解:数量交给 Canvas,交互交给 DOM/SVG。

静态图形、数量不多的时候,SVG 反而更省——Canvas 的每一次改动都要重绘整块区域。快不快取决于改动的频率和范围,不取决于技术选型本身。

Canvas 也不是没法做交互,命中检测可以自己实现:

canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
for (const shape of shapes) {
ctx.beginPath()
shape.path(ctx)
if (ctx.isPointInPath(x, y)) { select(shape); break }
}
})

截图那件事也常被忽略:不处理 devicePixelRatio 导出的图一定是模糊的,导出前要按倍数重设画布再重新绘制。

选 Canvas 还是 SVG,本质是在问「你要不要单独操作每一个图形」。

Canvas 的 API 边界与绘制模型见 MDN:Canvas API,判断「能不能用 Canvas 做」之前先确认 API 是否覆盖。