做可视化时经常会卡在第一个问题:到底用 Canvas 还是 SVG?

我的经验是:别从“哪个更高级”开始比,先从你的需求开始问——图形数量多不多?交互复杂不复杂?需不需要无损缩放?要不要 DOM 事件和 CSS 控制?

下面就按这些问题,把 Canvas/SVG 的差异掰开揉碎聊一遍。

一、技术原理与渲染方式

Canvas:位图渲染

  • 原理:基于像素的位图渲染,通过 JavaScript API 在 2D 上下文上绘制图形。
  • 渲染流程:CPU 计算 → GPU 光栅化 → 像素填充 → 屏幕显示。
  • 特点:一次性绘制,绘制完成后图形失去”对象”属性,无法单独操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
// Canvas 绘制示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 绘制矩形
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(10, 10, 100, 50);

// 绘制圆形
ctx.beginPath();
ctx.arc(200, 35, 25, 0, 2 * Math.PI);
ctx.fillStyle = '#4ecdc4';
ctx.fill();

SVG:矢量图形

  • 原理:基于 XML 的矢量图形,通过 DOM 元素描述图形结构。
  • 渲染流程:DOM 解析 → 矢量计算 → GPU 渲染 → 屏幕显示。
  • 特点:保留图形对象,支持事件绑定、样式修改、动画等。
1
2
3
4
5
<!-- SVG 绘制示例 -->
<svg width="300" height="100">
<rect x="10" y="10" width="100" height="50" fill="#ff6b6b" />
<circle cx="200" cy="35" r="25" fill="#4ecdc4" />
</svg>

二、操作方式与交互能力

Canvas:命令式操作

  • 绘制方式:通过 API 命令绘制,如 fillRect()strokeText()
  • 交互处理:需要手动计算鼠标位置与图形的关系,实现点击检测。
  • 动画实现:通过 requestAnimationFrame 循环重绘实现动画。
1
2
3
4
5
6
7
8
9
10
11
// Canvas 交互检测示例
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// 手动检测点击区域
if (x >= 10 && x <= 110 && y >= 10 && y <= 60) {
console.log('点击了矩形');
}
});

SVG:声明式操作

  • 绘制方式:通过 XML 标签声明图形结构。
  • 交互处理:天然支持事件绑定,每个图形元素都可以独立响应事件。
  • 动画实现:支持 CSS 动画、SMIL 动画,或通过 JavaScript 修改属性。
1
2
3
4
5
6
7
<!-- SVG 交互示例 -->
<svg width="300" height="100">
<rect x="10" y="10" width="100" height="50" fill="#ff6b6b"
onclick="handleClick()"
onmouseover="this.style.fill='#ff8e8e'"
onmouseout="this.style.fill='#ff6b6b'" />
</svg>

三、内存消耗与性能表现

Canvas:内存友好

  • 内存占用:固定内存占用,与图形复杂度无关,只与画布尺寸相关。
  • 大数据量:适合处理大量图形(数万到数十万个),内存占用稳定。
  • 垃圾回收:绘制完成后对象可被回收,内存压力小。
1
2
3
4
5
6
7
8
9
10
11
12
13
// Canvas 大数据量绘制
function drawManyCircles() {
for (let i = 0; i < 10000; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const r = Math.random() * 5 + 1;

ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fillStyle = `hsl(${Math.random() * 360}, 70%, 50%)`;
ctx.fill();
}
}

SVG:DOM 开销

  • 内存占用:每个图形对应一个 DOM 节点,内存占用与图形数量成正比。
  • 大数据量:当图形数量超过数千个时,DOM 操作性能急剧下降。
  • 垃圾回收:DOM 节点需要手动清理,否则容易造成内存泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
// SVG 大数据量问题示例
function createManySVGCircles() {
const svg = document.getElementById('svg');
for (let i = 0; i < 1000; i++) { // 超过1000个性能开始下降
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', Math.random() * 300);
circle.setAttribute('cy', Math.random() * 100);
circle.setAttribute('r', Math.random() * 5 + 1);
circle.setAttribute('fill', `hsl(${Math.random() * 360}, 70%, 50%)`);
svg.appendChild(circle);
}
}

四、响应时间与渲染性能

Canvas:渲染速度快

  • 初始渲染:一次性绘制,渲染速度快。
  • 更新性能:局部更新需要重绘整个画布或使用离屏 Canvas。
  • 缩放性能:缩放时性能稳定,但可能出现像素化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Canvas 高性能更新策略
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');

// 在离屏 Canvas 上绘制
function drawToOffscreen() {
// 绘制逻辑...
}

// 主线程只负责复制
function updateDisplay() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(offscreenCanvas, 0, 0);
}

SVG:DOM 操作慢

  • 初始渲染:DOM 解析和渲染相对较慢。
  • 更新性能:修改属性时触发重排重绘,性能较差。
  • 缩放性能:矢量缩放性能优秀,无像素化问题。

五、事件处理与交互体验

Canvas:手动事件处理

  • 事件检测:需要手动实现点击、悬停等事件检测。
  • 交互复杂度:复杂交互需要大量计算,但可以实现更精细的控制。
  • 事件委托:所有事件都绑定在 Canvas 元素上,通过坐标计算分发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Canvas 复杂交互示例
class CanvasInteraction {
constructor(canvas) {
this.canvas = canvas;
this.shapes = [];
this.selectedShape = null;

this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
}

handleMouseDown(e) {
const pos = this.getMousePos(e);
this.selectedShape = this.findShapeAt(pos);
}

findShapeAt(pos) {
// 手动检测图形位置
return this.shapes.find(shape => {
return pos.x >= shape.x && pos.x <= shape.x + shape.width &&
pos.y >= shape.y && pos.y <= shape.y + shape.height;
});
}
}

SVG:原生事件支持

  • 事件绑定:每个图形元素都可以直接绑定事件。
  • 事件冒泡:支持标准 DOM 事件冒泡机制。
  • 交互简单:简单交互实现容易,但复杂交互可能受 DOM 性能限制。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- SVG 原生事件支持 -->
<svg width="300" height="200">
<g id="chart">
<rect x="10" y="10" width="50" height="30" fill="blue"
onclick="selectBar(this)"
onmouseover="highlight(this)"
onmouseout="unhighlight(this)" />
<rect x="70" y="20" width="50" height="20" fill="red"
onclick="selectBar(this)"
onmouseover="highlight(this)"
onmouseout="unhighlight(this)" />
</g>
</svg>

六、缩放与分辨率适配

Canvas:像素化问题

  • 高 DPI 支持:需要手动处理设备像素比,否则在高分辨率屏幕上模糊。
  • 缩放质量:放大时会出现像素化,影响视觉效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
// Canvas 高 DPI 支持
function setupHighDPICanvas(canvas) {
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();

canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);

canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}

SVG:完美缩放

  • 矢量特性:任意缩放都保持清晰,无像素化问题。
  • 响应式:天然支持响应式设计,配合 CSS 媒体查询效果更佳。
1
2
3
4
<!-- SVG 响应式示例 -->
<svg viewBox="0 0 300 200" preserveAspectRatio="xMidYMid meet">
<!-- 图形内容 -->
</svg>

七、为什么主流可视化库选择 Canvas?

1. 性能优势

  • 大数据量处理:现代数据可视化经常需要处理数万甚至数十万个数据点,Canvas 的内存占用和渲染性能明显优于 SVG。
  • 实时更新:数据可视化需要频繁更新,Canvas 的重绘性能更适合实时场景。

2. 技术生态

  • WebGL 支持:Canvas 可以切到 WebGL 上下文,做 3D 或 GPU 加速渲染(比如 three.js 这类就是直接跑 WebGL)。
  • 工程可控:如果你把渲染和交互都做成“自己的对象模型”,Canvas 会更自然;SVG 走 DOM,则更依赖浏览器对 DOM 的性能表现。

3. 跨平台一致性

  • 渲染一致性:Canvas 在不同平台和浏览器上的渲染结果更一致。
  • 性能可预测:Canvas 的性能表现更可预测,便于优化。

4. 开发效率

  • 批量操作:Canvas 支持批量绘制操作,减少 API 调用次数。
  • 状态管理:Canvas 的状态管理更简单,不需要维护复杂的 DOM 树。

八、技术选型建议

选择 Canvas 的场景

  • 大数据量可视化(>1000 个图形元素)
  • 需要频繁更新的实时图表
  • 复杂的交互和动画效果
  • 3D 或 WebGL 渲染需求
  • 性能要求较高的场景

选择 SVG 的场景

  • 小到中等数据量的静态图表
  • 需要精确的矢量缩放
  • 简单的交互需求
  • 需要 SEO 友好的场景
  • 需要 CSS 样式控制的场景

九、技术对比总结

特性 Canvas SVG
渲染方式 位图渲染,像素级操作 矢量图形,DOM 元素
内存占用 固定,与图形数量无关 与图形数量成正比
大数据量性能 优秀,适合数万个元素 较差,超过千个性能下降
交互处理 需要手动实现事件检测 原生支持 DOM 事件
动画性能 优秀,适合复杂动画 一般,DOM 操作开销大
缩放质量 可能出现像素化 完美缩放,无像素化
开发复杂度 较高,需要手动处理细节 较低,声明式语法
SEO 友好性 差,无法被搜索引擎解析 好,XML 结构可被解析
CSS 样式支持 不支持 完全支持
3D 渲染能力 支持 WebGL 不支持
实时更新性能 优秀 一般
文件大小 小,纯 JavaScript 较大,XML 结构

十、结语

最后给一个更“工程化”的总结:

  • 如果你需要画很多点/很多图形,并且要频繁更新(实时图表、拖拽缩放、动画),优先 Canvas/WebGL。
  • 如果你的图形数量不大,但你想要“每个元素都是 DOM”,方便绑事件、改样式、做无损缩放,那 SVG 会更舒服。

另外,混用也很常见:大背景/大数据量用 Canvas,少量需要精确交互的元素用 SVG/DOM 做覆盖层。

Happy Visualizing!