做可视化时经常会卡在第一个问题:到底用 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 样式控制的场景

九、技术对比总结

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

十、结语

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

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

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

Happy Visualizing!