跳到主要内容

Canvas 与 SVG 的区别

概述

在前端开发中,CanvasSVG 是两种最常用的图形绘制技术。它们都可以在网页中绘制图形、动画和可视化内容,但底层实现原理完全不同,适用场景也有很大差异。理解它们之间的区别是前端开发者的必备知识。

简单来说:

  • 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>

核心区别对比

渲染模式对比

全面对比表

对比维度CanvasSVG
类型位图(栅格图形)矢量图形
绘制方式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 的关键因素之一。

性能关键点

场景CanvasSVG原因
1000+ 个图形同时显示✅ 快❌ 慢SVG 需要维护大量 DOM 节点
高频重绘(60fps 动画)✅ 快❌ 慢Canvas 直接操作像素,SVG 需要 DOM 更新+重排
复杂图形交互(拖拽、缩放)❌ 复杂✅ 简单SVG 有原生事件支持
静态图标、图表✅ 可用✅ 更优SVG 清晰度好,文件更小
像素级操作(滤镜、图像处理)✅ 支持❌ 不支持Canvas 可直接操作像素数据
性能陷阱

当 SVG 中包含上千个 DOM 节点时,浏览器的 DOM 操作和事件处理会变得非常慢。如果你的可视化场景需要展示大量数据点(如散点图包含 10000+ 个点),务必选择 Canvas。


实际应用场景

适合使用 Canvas 的场景

  1. 游戏开发:需要高频重绘和像素级控制
  2. 图像处理:裁剪、滤镜、合成等操作
  3. 粒子系统:大量粒子的运动和渲染
  4. 数据可视化(大数据量):如热力图、上万个数据点的散点图
  5. 视频处理:从视频帧中提取内容并绘制
  6. 签名板 / 画板:自由绘图应用
// 示例:简单的粒子效果(适合 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 的场景

  1. 图标系统:图标组件库(如 Ant Design Icons)
  2. 数据图表:如 D3.js、ECharts 的部分图表模式
  3. 交互式图形:流程图编辑器、思维导图
  4. Logo 和插画:需要无损缩放的品牌资产
  5. 地图:可交互的矢量地图
  6. 动画图标:加载动画、状态切换动画
<!-- 示例:可交互的 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 可以结合使用,发挥各自的优势:

混合使用示例

许多优秀的图表库都采用了混合方案:

渲染方式说明
EChartsCanvas(默认) + SVG(可选)默认使用 Canvas,小数据量可切换 SVG
D3.jsSVG(默认) + Canvas(可选)默认使用 SVG,大数据量可结合 Canvas
Chart.jsCanvas纯 Canvas 渲染
HighchartsSVG纯 SVG 渲染
AntV G2Canvas(默认) + 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 注意事项

Canvas 常见坑
  1. 高清屏模糊:不处理 devicePixelRatio 会导致在 Retina 屏幕上模糊
  2. 内存泄漏:频繁创建 Image 对象不释放会导致内存增长
  3. 跨域问题drawImage 绘制跨域图片后,toDataURL() 会报错
  4. 尺寸设置:不要用 CSS 设置 canvas 大小,应使用 width / height 属性
// ❌ 错误:用 CSS 设置尺寸(会导致图像拉伸变形)
// canvas { width: 400px; height: 300px; }

// ✅ 正确:用属性设置尺寸
const canvas = document.getElementById('myCanvas');
canvas.width = 400;
canvas.height = 300;

SVG 注意事项

SVG 常见坑
  1. 命名空间:动态创建 SVG 元素必须使用 createElementNS
  2. 性能瓶颈:DOM 节点过多(> 几千个)会导致页面卡顿
  3. 样式兼容性:部分 CSS 属性在 SVG 中的表现与 HTML 不同
  4. 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)。常用方法有:

  1. 数学计算法:维护图形数据列表,通过鼠标坐标计算判断是否在图形范围内(如判断点到圆心的距离)。
  2. isPointInPath() 方法:利用 Canvas API 提供的方法检测某个点是否在当前路径内。
  3. 离屏 Canvas 法:用一个不可见的 Canvas 为每个图形分配唯一颜色,通过获取点击位置的像素颜色来确定被点击的图形。

Q5:SVG 的 viewBox 是什么?有什么作用?

AviewBox 定义了 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)都支持双渲染器,可以根据场景灵活切换。