千个座位如何丝滑渲染?
一、问题场景:被 DOM 击溃的选座系统
你在接手重构某影院 Web 选座系统时,遭遇了性能噩梦:
- 座位数量:单厅 300+ 座位,用 div + CSS 布局
- 用户痛点:缩放卡顿、选择延迟、移动端崩溃
- 性能检测:Chrome Performance 显示每秒 5 帧,重绘耗时 200ms+
核心矛盾:DOM 的盒模型计算成本 vs 座位图的密集渲染需求。
二、解决方案:Canvas 的降维打击
1. 架构草图
javascript
// 初始化画布
const canvas = document.getElementById('seat-map');
const ctx = canvas.getContext('2d');
const seats = generateSeatGrid(15, 20); // 15行20列
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
seats.forEach(row => {
row.forEach(seat => {
drawSeat(seat.x, seat.y, seat.status); // 🔍 动态绘制座位状态
});
});
}
关键决策点:
ctx.clearRect
替代 DOM 的逐元素删除- 状态驱动绘制(
seat.status
控制颜色) - 脱离 DOM 树,规避回流重绘
2. 交互处理(坐标映射)
javascript
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 🔍 数学映射:像素坐标 → 座位索引
const col = Math.floor(x / SEAT_SIZE);
const row = Math.floor(y / SEAT_SIZE);
if (seats[row]?.[col]) {
seats[row][col].status =
seats[row][col].status === 'selected' ? 'free' : 'selected';
render(); // 状态更新后重绘
}
});
坐标转换公式 :
索引 = floor( (点击坐标 - canvas偏移量) / 单个座位像素尺寸 )
三、原理剖析:Canvas 的高性能本质
1. 分层渲染架构
graph TD
A[用户交互] --> B[计算座位状态]
B --> C[清空画布]
C --> D[遍历座位数据]
D --> E[根据状态绘制图形]
E --> F[完成渲染]
2. 性能对比表(Canvas vs DOM)
指标 | DOM 方案 | Canvas 方案 |
---|---|---|
300座位渲染耗时 | 120-200ms | 5-15ms |
内存占用 | 高(每个元素独立对象) | 低(共享上下文) |
事件处理 | 原生事件代理 | 需手动坐标映射 |
缩放支持 | 依赖CSS Transform | 内建scale()变换 |
3. 设计哲学
Canvas 是命令式绘图 :开发者描述"如何画"而非"画什么"。这与 DOM 的声明式本质背道而驰,却完美契合高频重绘场景。
四、实战优化:从可用到极致
1. 双缓存策略(解决闪屏)
javascript
// 创建离屏Canvas作为缓存
const offscreen = document.createElement('canvas');
const offCtx = offscreen.getContext('2d');
function cacheSeats() {
// 在离屏Canvas绘制静态座位图
seats.forEach(seat => drawSeatOn(offCtx, seat));
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 🔍 直接绘制缓存内容(避免重复计算)
ctx.drawImage(offscreen, 0, 0);
// 再绘制动态选中的座位(黄色)
drawSelectedSeats(ctx);
}
2. 按需渲染(性能救星)
javascript
let isRendering = false;
function lazyRender() {
if (isRendering) return;
isRendering = true;
requestAnimationFrame(() => {
render();
isRendering = false;
});
}
// 在交互事件中调用 lazyRender 而非直接 render
五、举一反三:Canvas 的工程化变体
-
演唱会选座方案
- 挑战:异形舞台 + 座位区环形分布
- 解法 :极坐标映射
(r, θ) → (x, y)
-
飞机座位图
- 特性:舱位等级隔离 + 过道标识
- 优化:分层绘制(背景层 + 动态层)
-
体育馆3D视角
- 进阶:WebGL + Three.js 实现视角旋转
- 降级:Canvas 模拟透视(近大远小变换)
小结
当你的界面是空间密集型 (座位图、地图、拓扑图)而非内容密集型(表单、列表)时,拥抱 Canvas 就是选择性能的自由。它用最原始的绘图指令,构建了最流畅的视觉宇宙。