本文面向有 HTML / CSS / JavaScript 基础、想系统掌握 Canvas 的开发者。读完你将理解:Canvas 究竟是一块什么样的“画布”、一条绘图命令如何一步步变成屏幕上的像素,并能写出高性能的动画与可视化代码。
前置知识:会用浏览器开发者工具、能看懂基础 JS;文中所有示例都可在任一现代浏览器里直接运行。
全文主线:先破除误区、建立“位图画布”的心智模型,再沿着“JS 调用 → 绘图指令 → 渲染后端 → 光栅化 → 合成上屏”这条链路把原理讲透,最后落到可复用的代码实践与选型判断。
Canvas 是什么:命令式绘制的“位图画布”
Canvas 是 HTML5 提供的位图绘图技术:用<canvas>标签在页面里划出一块矩形区域,再用 JavaScript 通过一套 2D 绘图 API,把图形、文字、图片逐条“画”进去。网页游戏、图表库(ECharts / Chart.js)、动态海报、图像滤镜,底层几乎都是它。
一个关键心智:Canvas 是命令式的——你写下一系列绘图命令,浏览器执行它们,把结果定格成一张像素位图。它不像 HTML / CSS 那样“描述”页面结构,而是直接“绘制”最终画面。
<canvasid="demo"width="400"height="300"></canvas><script>constcanvas=document.getElementById('demo');constctx=canvas.getContext('2d');// 拿到“2D 画笔”</script>常见误区:以为 <canvas> 像 <div> 一样能放子元素、能逐个操作图形。实际上 canvas 内部只有像素,没有 DOM 节点;画上去的内容也无法像 DOM 那样随时读取、修改——想改一个圆,只能清掉重画。这是它与 SVG 最本质的区别。
另外注意:标签上的width / height属性定义的是绘图缓冲区的实际像素尺寸;CSS 里的 width / height 只决定它在页面上的显示大小。两者不一致时,浏览器会把位图拉伸显示,导致画面模糊(高 DPI 适配那一节会专门处理)。
三个核心概念,建立正确心智模型
把 Canvas 想成“一块黑板 + 一支带记忆的画笔 + 一串指令”,后面所有 API 都好理解了。
画布:一块像素矩形
<canvas> 背后是一块width × height的像素缓冲区(bitmap)。它有三个特性:尺寸固定(想变要先改 width/height)、内容是一次性绘制的位图、放大后会失真。没有 GPU 加速时这块缓冲区由 CPU 维护,启用加速后则由 GPU 上的纹理承载。
上下文:携带状态的“画笔”
getContext(‘2d’)返回一个 CanvasRenderingContext2D 对象。它不只提供绘图方法,更是一个状态机:当前填充色、描边色、线宽、字体、阴影、变换矩阵等,都作为“画笔当前状态”存在上下文里。后续所有绘图命令都依据这套状态执行。
命令:逐条执行的绘图指令
fillRect、arc、drawImage、fillText……每一条都是立即加入“绘制流水线”的指令。命令的顺序就是绘制顺序,后画的盖在先画的上面。要修改某一笔,只能清屏重画——所以“最小化重绘区域”是 Canvas 优化的第一原则。
坐标系:左上角原点
Canvas 的 2D 坐标系以画布左上角为原点 (0,0),x 轴向右、y 轴向下,单位是像素。这与数学坐标系 y 轴方向相反,第一次写代码时最容易画反。
ctx.save();// 记住当前状态ctx.translate(200,150);// 平移坐标系:把原点从左上角 (0,0) 挪到 (200,150)ctx.rotate(Math.PI/4);// 旋转坐标系:以当前原点(已平移后的点)为中心,顺时针旋转 π/4,即 45°(Canvas 角度用弧度制)ctx.fillRect(-50,-50,100,100);// 填充矩形:矩形左上角在 (-50,-50)、宽高 100。因为坐标系已经被平移 + 旋转,所以它不再画在画布原点,而是落在 (200,150) 且被转成菱形ctx.restore();// 恢复,不影响后续绘制从 API 到像素:完整渲染链路
这是全文理解最难、也最值得看的部分。Canvas 的渲染并不在 JS 线程里直接“画像素”,而是像工厂流水线一样分层协作。下面这张图把从ctx.fillRect()到屏幕像素的完整链路画了出来:
绘制命令先记录、后重放
你的 JS 调用并不会立刻落像素。Chromium / WebKit 里,Canvas 2D API 通过 JSBinding 进入浏览器内核,底层由Skia(Google 的开源 2D 图形库)承接:每条命令先被记录进一个SkPicture 显示列表,等浏览器收到垂直同步(Vsync)信号后,再统一“重放”给渲染后端。这一机制让浏览器可以批量、异步地处理绘制,而不是被 JS 的每一次调用拖住。
硬件加速与软件回退
重放时,Skia 会选择一个渲染后端:支持 GPU 时走Ganesh(Skia 的 GL 后端),把绘制转成 OpenGL / Vulkan / Metal / Direct3D 命令交给 GPU 光栅化;没有 GPU(远程桌面、WebGL 被禁用等)则回退到CPU 软件光栅化(如 SwiftShader)。现代 Chromium 中,GPU 命令由独立、被沙箱隔离的 GPU 进程(Viz)执行,驱动崩溃不会拖垮页面,这也是稳定性设计的核心。
为什么 Canvas 比 DOM 图形更快
普通 DOM 图形要经过样式计算 → 布局 → 绘制 → 合成完整流程,每个节点都有开销;而 Canvas 不参与 DOM 树和布局,绘制完成后整张位图就是一个独立合成层,后续动画(位移、透明、缩放)可以只在合成阶段完成,绕开昂贵的重排与重绘。这正是它适合高帧率动画、海量图元的根本原因。
30 秒上手:最小可运行示例
把下面代码存成 .html 双击打开,你就能看到一块浅蓝画布上有一个蓝圆和一行文字——这已经是 Canvas 应用的完整骨架:取上下文 → 设置状态 → 发指令。
<canvasid="demo"width="400"height="300"></canvas><script>constctx=document.getElementById('demo').getContext('2d');// 1) 背景ctx.fillStyle='#f0f4ff';ctx.fillRect(0,0,400,300);// 2) 圆形ctx.fillStyle='#3b82f6';ctx.beginPath();ctx.arc(120,150,60,0,Math.PI*2);ctx.fill();// 3) 文字ctx.fillStyle='#0f172a';ctx.font='20px sans-serif';ctx.fillText('Hello Canvas',210,160);</script>高频绘制能力速查
| 能力 | 常用 API | 说明 |
|---|---|---|
| 基本图形 | fillRect / strokeRect / clearRect | 矩形是性能最好的基元,大量绘制时优先用它 |
| 路径 | beginPath / moveTo / lineTo / arc / bezierCurveTo | 任意形状;记得每次 beginPath 后重新描述 |
| 文字 | fillText / strokeText / measureText | fillText 带多行需手动处理换行;measureText 可量宽度做居中 |
| 图像 | drawImage(img, x, y, w, h) | 接受 Image / Canvas / Video / ImageBitmap,可裁切缩放 |
| 变换 | translate / rotate / scale / transform | 配合 save/restore 使用,适合做局部旋转缩放 |
| 像素级 | getImageData / putImageData / createImageData | 逐像素处理,滤镜、抠图的基础;性能开销大,谨慎用 |
| 状态 | save / restore | 状态栈:颜色、线宽、变换等统一入栈出栈 |
动画循环与高性能实践
静态绘制只是开始,Canvas 的真正价值在动画与实时可视化。下面几条是从“能画”到“画得好”的关键。
用 requestAnimationFrame 驱动动画
永远不要用 setInterval 做动画。requestAnimationFrame 由浏览器在下一帧绘制前回调,自动与屏幕刷新率对齐,页面不可见时自动暂停,省电省资源。
functionframe(t){// 1) 清屏(或只清“脏”区域)ctx.clearRect(0,0,w,h);// 2) 根据时间 t 计算并绘制本帧constx=100+Math.sin(t/500)*80;ctx.beginPath();ctx.arc(x,150,30,0,Math.PI*2);ctx.fill();// 3) 请求下一帧requestAnimationFrame(frame);}requestAnimationFrame(frame);高 DPI 屏适配(清晰度第一坑)
Retina 等高分屏的物理像素密度是 CSS 像素的 2~3 倍。如果不处理,Canvas 会按 CSS 尺寸绘制再拉伸,文字和线条发虚。标准做法:把绘图缓冲区设成“CSS 尺寸 × devicePixelRatio”,再整体放大坐标系。
constdpr=window.devicePixelRatio||1;canvas.width=cssWidth*dpr;canvas.height=cssHeight*dpr;canvas.style.width=cssWidth+'px';canvas.style.height=cssHeight+'px';ctx.scale(dpr,dpr);// 之后按 CSS 像素坐标绘制即可离屏画布预渲染(烘焙)
如果每帧都在重复画同一批复杂图形(比如背景地图、角色精灵),就用一个不可见的离屏 canvas先把它们画成一张位图,帧循环里只 drawImage 一张图。绘制负担从“每帧重画几百个图元”降到“每帧贴一张图”,性能提升非常明显——纹理常驻 GPU,连拷贝都能省。
脏矩形与分层
脏矩形:不要每帧 clearRect 整块画布再全量重画。对象只移动了一小块,就只清那一小块、只重画那一小块,减少无效像素写入。分层:把“静止背景”和“动态前景”拆成两个叠加的 canvas(或一个离屏静态层 + 一个动态层),静止层只画一次,动态层每帧只重画自己的小区域。
减少状态切换与批量化
每改一次 fillStyle / strokeStyle 都可能触发内部状态切换。尽量按状态分组:先画完所有红色图形,再切一次颜色画蓝色图形;能用 fillRect 代替 stroke 的地方用 fillRect;避免在循环体内重复设置不变的属性。这是最便宜也最容易被忽视的优化。
如何定位性能瓶颈
优化前先量化,别凭感觉。打开 DevTools → Performance,录一段动画,重点看三处:JS 计算(节点位置、碰撞检测是否拖慢帧)、Canvas 绘制指令(fillRect / drawImage 等调用开销)、光栅化与合成(GPU 侧是否打满)。哪一层耗时最长,就用对应手段:计算层优化算法与缓存;绘制层做离屏烘焙、脏矩形、批量化;合成层控制图层数量与尺寸、避免每帧触发整块重绘。
技术选型:Canvas vs SVG vs WebGL
Canvas 不是唯一答案。做技术分享和方案设计时,下面这张对照表能帮你快速决策:
| 维度 | Canvas 2D | SVG | WebGL | DOM + CSS |
|---|---|---|---|---|
| 渲染模型 | 位图(像素) | 矢量(几何描述) | GPU 着色器 | DOM / 样式 |
| 缩放质量 | 放大失真 | 无限缩放 | 无失真 | 矢量级 |
| 交互 | 手动命中检测 | 原生 DOM 事件 | 手动命中检测 | 原生事件 |
| 海量图元 | 强(数千~数万) | 弱(千个节点就卡) | 极强(十万级) | 弱 |
| 工程成本 | 低 | 低 | 高(着色器、缓冲) | 低 |
| 典型场景 | 游戏、动画、大数据可视化 | 图表、图标、地图、Logo | 3D、粒子、复杂特效 | 基础布局、简单图形 |
选型口诀:静态、少量、要缩放 → SVG;动态、海量、要高帧率 → Canvas 2D;3D 或十万级以上图元 → WebGL / WebGPU。实际项目常混用——比如 SVG 做可交互的 UI 层,Canvas 做底层大数据图层。
跨平台:一份 Canvas 代码,多端运行
Canvas 的渲染抽象早已跨出浏览器,这也是它生命力极强的原因:
浏览器:全部现代浏览器原生支持,2D 走 Skia、可 GPU 加速。
Electron / 桌面端:Chromium 内核,与浏览器行为一致;还可通过 offscreenCanvas 在 Worker 线程渲染。
Node.js:node-canvas / Skia Canvas 在服务端用同一套 2D API 生成图片(如海报、验证码、图表),再交给前端展示。
小程序 / 移动端:各平台提供同名的 Canvas 2D API(如 canvas type=“2d”),底层映射到平台 2D 渲染库(Android 走 Skia、iOS 走 CoreGraphics)。
Flutter / HarmonyOS 等:新一代跨端渲染引擎普遍以 Skia 为底,Canvas 2D 语义(fill、stroke、clip、transform)在其上几乎是通用的。
限制与适用边界
把边界讲清楚,才不会在错误场景里硬用 Canvas:
无 DOM 语义:内容对屏幕阅读器不友好,可访问性差;需要可访问性时优先 SVG 或补充 aria。
不可直接交互:没有事件模型,点击/悬停都要自己实现坐标命中检测。
位图放大失真:分辨率固定,放大后模糊;需要无损缩放请用 SVG 或矢量方案。
内存敏感:画布越大、图层越多,GPU 纹理内存占用越高;大画布在低端移动设备上可能明显掉帧。
文本排版弱:无自动换行、无富文本排版,长文本、复杂排版不适合在 Canvas 里做。
进阶方向与延伸阅读
掌握了本文的主线后,可以按兴趣继续深入:WebGL / WebGPU 着色器编程;CanvasKit(Skia 的 WASM 版本)在 Web 端的应用;离屏渲染与 Worker 中的 OffscreenCanvas;用 ECharts / PixiJS / Konva 等成熟库站在巨人肩膀上。以下资源可作延伸:
web.dev:提升 HTML5 画布性能
掘金:Canvas 渲染初探(硬件加速与烘焙)
Chromium GPU Accelerated Compositing(Canvas 合成层与 Ganesh)
MDN:Canvas API 官方文档