Canvas 与 SVG 的区别
概述
在前端开发中,Canvas 和 SVG 是两种最常用的图形绘制技术。它们都可以在网页中绘制图形、动画和可视化内容,但底层实现原理完全不同,适用场景也有很大差异。理解它们之间的区别是前端开发者的必备知识。
简单来说:
- Canvas:像在画布上"画画",画完之后画布只记住像素点,不记得你画了什么形状。
- SVG:像用"积木"搭建图形,每一个形状都是独立的对象,随时可以拿出来修改。
Canvas 基础
什么是 Canvas?
<canvas> 是 HTML5 引入的一个标签,它提供了一个位图绘制区域。我们通过 JavaScript 调用 Canvas API,在这个区域上逐像素地绘制图形。
Canvas 是即时模式(Immediate Mode)图形系统——你告诉它"画什么",它画完就忘了,只保留最终的像素结果。
基本用法
<!-- 1. 在 HTML 中创建 canvas 元素 -->
<canvas id="myCanvas" width="400" height="300"></canvas>
// 2. 获取绘图上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d'); // 获取 2D 绘图上下文
// 3. 绘制一个蓝色矩形
ctx.fillStyle = '#3498db';
ctx.fillRect(50, 50, 200, 100);
// 4. 绘制一个红色圆形
ctx.beginPath();
ctx.arc(300, 150, 50, 0, Math.PI * 2);
ctx.fillStyle = '#e74c3c';
ctx.fill();
// 5. 绘制文字
ctx.font = '20px Arial';
ctx.fillStyle = '#2c3e50';
ctx.fillText('Hello Canvas!', 80, 250);
Canvas 绘制流程
常用 Canvas API
| API | 功能 | 示例 |
|---|---|---|
fillRect(x, y, w, h) | 绘制填充矩形 | ctx.fillRect(0, 0, 100, 50) |
strokeRect(x, y, w, h) | 绘制矩形边框 | ctx.strokeRect(0, 0, 100, 50) |
clearRect(x, y, w, h) | 清除矩形区域 | ctx.clearRect(0, 0, canvas.width, canvas.height) |
beginPath() | 开始新路径 | ctx.beginPath() |
arc(x, y, r, start, end) | 绘制圆弧 | ctx.arc(50, 50, 30, 0, Math.PI * 2) |
moveTo(x, y) | 移动画笔 | ctx.moveTo(0, 0) |
lineTo(x, y) | 画线到指定点 | ctx.lineTo(100, 100) |
fill() | 填充路径 | ctx.fill() |
stroke() | 描边路径 | ctx.stroke() |
drawImage() | 绘制图像 | ctx.drawImage(img, 0, 0) |
SVG 基础
什么是 SVG?
SVG(Scalable Vector Graphics,可缩放矢量图形)是一种基于 XML 的图形描述语言。每个图形元素都是 DOM 节点,可以像 HTML 元素一样被选中、修改和绑定事件。
SVG 是保留模式(Retained Mode)图形系统——浏览器会维护一个完整的图形对象模型,你可以随时访问和修改其中的任何元素。
基本用法
<!-- 直接在 HTML 中编写 SVG -->
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
<!-- 蓝色矩形 -->
<rect x="50" y="50" width="200" height="100" fill="#3498db" />
<!-- 红色圆形 -->
<circle cx="300" cy="150" r="50" fill="#e74c3c" />
<!-- 文字 -->
<text x="80" y="250" font-size="20" fill="#2c3e50">Hello SVG!</text>
</svg>
SVG 渲染原理
常用 SVG 元素
| 元素 | 功能 | 示例 |
|---|---|---|
<rect> | 矩形 | <rect x="10" y="10" width="100" height="50" /> |
<circle> | 圆形 | <circle cx="50" cy="50" r="30" /> |
<ellipse> | 椭圆 | <ellipse cx="50" cy="50" rx="40" ry="20" /> |
<line> | 直线 | <line x1="0" y1="0" x2="100" y2="100" /> |
<polyline> | 折线 | <polyline points="0,0 50,50 100,0" /> |
<polygon> | 多边形 | <polygon points="50,0 100,100 0,100" /> |
<path> | 路径(最强大) | <path d="M10 10 L90 90" /> |
<text> | 文字 | <text x="10" y="50">Hello</text> |
<g> | 分组 | <g transform="translate(10,10)">...</g> |
<defs> | 定义可复用元素 | <defs><pattern>...</pattern></defs> |
核心区别对比
渲染模式对比
全面对比表
| 对比维度 | Canvas | SVG |
|---|---|---|
| 类型 | 位图(栅格图形) | 矢量图形 |
| 绘制方式 | JavaScript 脚本绘制 | XML 标签声明式描述 |
| DOM 节点 | 只有一个 <canvas> 元素 | 每个图形都是 DOM 节点 |
| 事件绑定 | 只能绑定在 canvas 元素上,需手动计算点击位置 | 可以给每个图形元素绑定事件 |
| 缩放表现 | 放大后会模糊(像素化) | 无限放大不失真 |
| 动画方式 | requestAnimationFrame + 手动重绘 | CSS 动画 / SMIL / JavaScript |
| 文字处理 | 绘制后变成像素,不可选中 | 真实 DOM 文本,可选中、可搜索 |
| 可访问性 | 差,屏幕阅读器无法读取内容 | 好,支持 <title> 和 <desc> 描述 |
| 性能(少量元素) | 较好 | 很好 |
| 性能(大量元素) | 好(不受元素数量影响) | 差(DOM 节点过多会卡顿) |
| 文件大小 | 取决于画布尺寸 | 取决于图形复杂度 |
| SEO | 不利于 SEO | 利于 SEO |
| 导出 | 可导出为 PNG / JPEG | 可导出为 SVG / PNG / PDF |
深入理解:事件处理
事件处理是 Canvas 和 SVG 最显著的使用体验差异之一。
SVG 的事件处理(简单直观)
SVG 中每个图形都是 DOM 节点,事件处理和普通 HTML 元素一样:
<svg width="300" height="200">
<!-- 给圆形绑定点击事件 -->
<circle
cx="100" cy="100" r="40"
fill="#3498db"
onclick="alert('你点击了蓝色圆形!')"
style="cursor: pointer;"
/>
<!-- 给矩形绑定悬停效果 -->
<rect
x="180" y="60" width="80" height="80"
fill="#e74c3c"
onmouseover="this.setAttribute('fill', '#c0392b')"
onmouseout="this.setAttribute('fill', '#e74c3c')"
style="cursor: pointer;"
/>
</svg>
也可以使用 JavaScript:
const circle = document.querySelector('circle');
circle.addEventListener('click', (e) => {
console.log('点击的元素:', e.target); // 直接获取到 circle 元素
e.target.setAttribute('fill', '#2ecc71');
});
Canvas 的事件处理(需要手动计算)
Canvas 只有一个 DOM 元素,无法直接知道点击了哪个"图形":
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 存储图形信息用于碰撞检测
const shapes = [
{ type: 'circle', x: 100, y: 100, r: 40, color: '#3498db' },
{ type: 'rect', x: 180, y: 60, w: 80, h: 80, color: '#e74c3c' },
];
// 绘制所有图形
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
shapes.forEach(shape => {
ctx.fillStyle = shape.color;
if (shape.type === 'circle') {
ctx.beginPath();
ctx.arc(shape.x, shape.y, shape.r, 0, Math.PI * 2);
ctx.fill();
} else if (shape.type === 'rect') {
ctx.fillRect(shape.x, shape.y, shape.w, shape.h);
}
});
}
// 手动实现点击检测
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 遍历所有图形,判断点击位置是否在某个图形内
shapes.forEach(shape => {
if (shape.type === 'circle') {
const dist = Math.sqrt((mouseX - shape.x) ** 2 + (mouseY - shape.y) ** 2);
if (dist <= shape.r) {
console.log('点击了圆形!');
}
} else if (shape.type === 'rect') {
if (mouseX >= shape.x && mouseX <= shape.x + shape.w &&
mouseY >= shape.y && mouseY <= shape.y + shape.h) {
console.log('点击了矩形!');
}
}
});
});
draw();
SVG 的事件处理就像点击网页上的按钮一样简单;Canvas 的事件处理则需要你自己实现"碰撞检测"逻辑,代码量多出很多。
深入理解:动画实现
Canvas 动画
Canvas 动画的核心思路是:清除画布 → 更新状态 → 重新绘制,循环往复。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let x = 0;
const speed = 2;
function animate() {
// 1. 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 更新状态
x += speed;
if (x > canvas.width) x = -50;
// 3. 重新绘制
ctx.fillStyle = '#3498db';
ctx.beginPath();
ctx.arc(x, 150, 25, 0, Math.PI * 2);
ctx.fill();
// 4. 循环
requestAnimationFrame(animate);
}
animate();
SVG 动画
SVG 支持多种动画方式:
方式一:CSS 动画
<style>
.moving-circle {
animation: moveRight 3s ease-in-out infinite alternate;
}
@keyframes moveRight {
from { cx: 50; }
to { cx: 350; }
}
</style>
<svg width="400" height="200">
<circle class="moving-circle" cx="50" cy="100" r="25" fill="#3498db" />
</svg>
方式二:SMIL 动画(SVG 内置)
<svg width="400" height="200">
<circle cx="50" cy="100" r="25" fill="#e74c3c">
<!-- animate 标签定义动画 -->
<animate
attributeName="cx"
from="50" to="350"
dur="3s"
repeatCount="indefinite"
fill="freeze"
/>
</circle>
</svg>
方式三:JavaScript 操作 DOM
const circle = document.querySelector('circle');
let cx = 50;
function animate() {
cx += 2;
if (cx > 350) cx = 50;
circle.setAttribute('cx', cx);
requestAnimationFrame(animate);
}
animate();
Canvas 动画适合需要大量像素级操作的复杂场景(如粒子效果、游戏画面);SVG 动画适合简单图形的过渡、变换,配合 CSS 使用非常方便。
深入理解:缩放与分辨率
Canvas 的缩放问题
Canvas 本质上是一张位图,放大后会出现锯齿和模糊:
// Canvas 默认不会处理高清屏(Retina)问题
// 需要手动适配
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
// 设置实际像素大小
canvas.width = 400 * dpr;
canvas.height = 300 * dpr;
// 设置 CSS 显示大小
canvas.style.width = '400px';
canvas.style.height = '300px';
// 缩放绑定上下文
ctx.scale(dpr, dpr);
SVG 的无损缩放
SVG 是矢量图形,无论放大多少倍都不会失真:
<!-- SVG 天然支持缩放 -->
<svg viewBox="0 0 100 100" width="400" height="400">
<circle cx="50" cy="50" r="40" fill="#3498db" />
</svg>
<!-- 同样的 SVG,不同尺寸显示,始终清晰 -->
<svg viewBox="0 0 100 100" width="50" height="50">
<circle cx="50" cy="50" r="40" fill="#3498db" />
</svg>
性能对比分析
性能是选择 Canvas 还是 SVG 的关键因素之一。
性能关键点
| 场景 | Canvas | SVG | 原因 |
|---|---|---|---|
| 1000+ 个图形同时显示 | ✅ 快 | ❌ 慢 | SVG 需要维护大量 DOM 节点 |
| 高频重绘(60fps 动画) | ✅ 快 | ❌ 慢 | Canvas 直接操作像素,SVG 需要 DOM 更新+重排 |
| 复杂图形交互(拖拽、缩放) | ❌ 复杂 | ✅ 简单 | SVG 有原生事件支持 |
| 静态图标、图表 | ✅ 可用 | ✅ 更优 | SVG 清晰度好,文件更小 |
| 像素级操作(滤镜、图像处理) | ✅ 支持 | ❌ 不支持 | Canvas 可直接操作像素数据 |
当 SVG 中包含上千个 DOM 节点时,浏览器的 DOM 操作和事件处理会变得非常慢。如果你的可视化场景需要展示大量数据点(如散点图包含 10000+ 个点),务必选择 Canvas。
实际应用场景
适合使用 Canvas 的场景
- 游戏开发:需要高频重绘和像素级控制
- 图像处理:裁剪、滤镜、合成等操作
- 粒子系统:大量粒子的运动和渲染
- 数据可视化(大数据量):如热力图、上万个数据点的散点图
- 视频处理:从视频帧中提取内容并绘制
- 签名板 / 画板:自由绘图应用
// 示例:简单的粒子效果(适合 Canvas)
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
const particles = Array.from({ length: 500 }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
r: Math.random() * 3 + 1,
}));
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
// 边界反弹
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(52, 152, 219, 0.7)';
ctx.fill();
});
requestAnimationFrame(animate);
}
animate();
适合使用 SVG 的场景
- 图标系统:图标组件库(如 Ant Design Icons)
- 数据图表:如 D3.js、ECharts 的部分图表模式
- 交互式图形:流程图编辑器、思维导图
- Logo 和插画:需要无损缩放的品牌资产
- 地图:可交互的矢量地图
- 动画图标:加载动画、状态切换动画
<!-- 示例:可交互的 SVG 按钮组 -->
<svg width="300" height="80" xmlns="http://www.w3.org/2000/svg">
<style>
.btn { cursor: pointer; transition: all 0.3s; }
.btn:hover rect { fill: #2980b9; }
.btn:hover text { fill: white; }
</style>
<g class="btn" onclick="alert('确认')">
<rect x="10" y="15" width="120" height="50" rx="8" fill="#3498db" />
<text x="70" y="48" text-anchor="middle" fill="white" font-size="16">确认</text>
</g>
<g class="btn" onclick="alert('取消')">
<rect x="160" y="15" width="120" height="50" rx="8" fill="#ecf0f1" stroke="#bdc3c7" />
<text x="220" y="48" text-anchor="middle" fill="#2c3e50" font-size="16">取消</text>
</g>
</svg>
混合使用方案
在实际项目中,Canvas 和 SVG 可以结合使用,发挥各自的优势:
混合使用示例
许多优秀的图表库都采用了混合方案:
| 库 | 渲染方式 | 说明 |
|---|---|---|
| ECharts | Canvas(默认) + SVG(可选) | 默认使用 Canvas,小数据量可切换 SVG |
| D3.js | SVG(默认) + Canvas(可选) | 默认使用 SVG,大数据量可结合 Canvas |
| Chart.js | Canvas | 纯 Canvas 渲染 |
| Highcharts | SVG | 纯 SVG 渲染 |
| AntV G2 | Canvas(默认) + SVG | 支持双渲染器 |
// ECharts 切换渲染器示例
import * as echarts from 'echarts';
// 使用 Canvas 渲染(默认)
const canvasChart = echarts.init(document.getElementById('chart'), null, {
renderer: 'canvas',
});
// 使用 SVG 渲染
const svgChart = echarts.init(document.getElementById('chart'), null, {
renderer: 'svg',
});
开发中的注意事项
Canvas 注意事项
- 高清屏模糊:不处理
devicePixelRatio会导致在 Retina 屏幕上模糊 - 内存泄漏:频繁创建
Image对象不释放会导致内存增长 - 跨域问题:
drawImage绘制跨域图片后,toDataURL()会报错 - 尺寸设置:不要用 CSS 设置 canvas 大小,应使用
width/height属性
// ❌ 错误:用 CSS 设置尺寸(会导致图像拉伸变形)
// canvas { width: 400px; height: 300px; }
// ✅ 正确:用属性设置尺寸
const canvas = document.getElementById('myCanvas');
canvas.width = 400;
canvas.height = 300;
SVG 注意事项
- 命名空间:动态创建 SVG 元素必须使用
createElementNS - 性能瓶颈:DOM 节点过多(> 几千个)会导致页面卡顿
- 样式兼容性:部分 CSS 属性在 SVG 中的表现与 HTML 不同
- viewBox:不理解 viewBox 会导致图形被裁切或比例失调
// ❌ 错误:用 createElement 创建 SVG 元素
const circle = document.createElement('circle'); // 不会工作!
// ✅ 正确:用 createElementNS 创建
const svgNS = 'http://www.w3.org/2000/svg';
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttribute('cx', '50');
circle.setAttribute('cy', '50');
circle.setAttribute('r', '30');
circle.setAttribute('fill', '#3498db');
document.querySelector('svg').appendChild(circle);
速查决策指南
当你不确定该选 Canvas 还是 SVG 时,可以参考以下决策流程:
面试高频问答
Q1:Canvas 和 SVG 的本质区别是什么?
A:Canvas 是位图,基于像素的即时模式渲染,通过 JavaScript API 在画布上绘制,绘制完成后不保留图形对象信息。SVG 是矢量图,基于 XML 的保留模式渲染,每个图形都是 DOM 节点,可以被独立操作和绑定事件。
Q2:什么场景下应该选择 Canvas?什么场景选择 SVG?
A:
- 选 Canvas:大量图形元素渲染(如上万个数据点)、游戏开发、图像处理、粒子效果等需要高性能像素操作的场景。
- 选 SVG:图标系统、交互式图表、流程图编辑器、需要无损缩放的图形、对 SEO 和可访问性有要求的场景。
Q3:为什么 Canvas 在大量图形时性能更好?
A:Canvas 整个画面只是一个 <canvas> DOM 元素,不管绘制多少图形,DOM 节点数量不变。而 SVG 中每个图形都是一个 DOM 节点,上千个节点意味着浏览器需要维护大量的 DOM 树,DOM 操作和事件分发都会成为性能瓶颈。Canvas 的绘制复杂度只与画布大小(像素数量)相关,与图形数量关系不大。
Q4:Canvas 如何实现点击某个图形的事件处理?
A:由于 Canvas 上只有一个 DOM 元素,需要手动实现碰撞检测(Hit Test)。常用方法有:
- 数学计算法:维护图形数据列表,通过鼠标坐标计算判断是否在图形范围内(如判断点到圆心的距离)。
isPointInPath()方法:利用 Canvas API 提供的方法检测某个点是否在当前路径内。- 离屏 Canvas 法:用一个不可见的 Canvas 为每个图形分配唯一颜色,通过获取点击位置的像素颜色来确定被点击的图形。
Q5:SVG 的 viewBox 是什么?有什么作用?
A:viewBox 定义了 SVG 的内部坐标系统,格式为 viewBox="minX minY width height"。它的作用是建立一个虚拟的坐标空间,然后 SVG 的内容会按比例映射到实际显示区域。通过 viewBox,可以实现图形的等比缩放和自适应布局,无论外部容器尺寸如何变化,图形都能正确显示。
Q6:Canvas 在 Retina(高清)屏上为什么会模糊?怎么解决?
A:Retina 屏的设备像素比(devicePixelRatio)大于 1(通常为 2 或 3),如果 Canvas 的 width/height 属性值等于 CSS 像素值,实际渲染时会用少量的 Canvas 像素覆盖更多的物理像素,导致模糊。解决方法是将 Canvas 的属性尺寸设为 CSS 尺寸乘以 devicePixelRatio,然后通过 ctx.scale(dpr, dpr) 缩放绑定上下文,使绘图代码中使用的坐标保持不变。
Q7:主流图表库(ECharts、D3.js 等)使用的是 Canvas 还是 SVG?
A:不同的图表库有不同的选择:
- ECharts:默认使用 Canvas 渲染,也支持切换为 SVG 渲染器。在处理大数据量图表时推荐 Canvas,在需要交互性和导出矢量图时可用 SVG。
- D3.js:主要操作 SVG DOM,但也可以搭配 Canvas 使用,许多基于 D3 的可视化项目在大数据场景中会改用 Canvas。
- Chart.js:纯 Canvas 渲染。
- Highcharts:纯 SVG 渲染。
Q8:Canvas 和 SVG 可以混合使用吗?
A:可以。实际项目中经常将两者结合:用 Canvas 渲染大量数据点(性能好),用 SVG 渲染交互控件和标签文字(事件处理方便、文字清晰可选中)。两者可以通过绝对定位叠加在同一区域,各自处理各自的层。许多现代可视化库(如 ECharts、AntV)都支持双渲染器,可以根据场景灵活切换。