Skip to content

Canvas 性能优化

Canvas 应用的性能直接影响用户体验。在复杂场景下(如游戏、数据可视化、实时图表),不当的绘图方式可能导致帧率下降、卡顿。本节将介绍离屏 Canvas、分层渲染、状态管理优化、减少状态切换和硬件加速等关键技术,帮助你构建高性能的 Canvas 应用。

前置知识

阅读本节前,建议先了解:动画基础

基础概念

Canvas 2D 绑定的绘图操作本质上是 CPU 向 GPU 提交渲染指令的过程。性能瓶颈通常出现在:

  • 过多的绘图调用(draw calls)
  • 频繁的状态切换(fillStyle、transform 等)
  • 大面积的像素操作(getImageData/putImageData)
  • 重复绘制不变的静态内容

性能优化金字塔

       ┌──────────┐
       │ 硬件加速  │  ← 最后考虑
      ├───────────┤
      │  算法优化  │  ← 减少计算量
     ├────────────┤
     │  缓存策略  │  ← 避免重复计算
    ├─────────────┤
    │  绘图优化   │  ← 减少 draw calls
   ├──────────────┤
   │  架构设计   │  ← 合理的分层与渲染策略
  └───────────────┘

离屏 Canvas

什么是离屏 Canvas

离屏 Canvas(Offscreen Canvas)是不在 DOM 中的 <canvas> 元素。它常用于预渲染静态内容、缓存复杂的绘制结果,然后在动画循环中通过 drawImage() 快速绘制。

基本用法

javascript
// 创建离屏 Canvas
const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 200;
const offCtx = offscreen.getContext('2d');

// 在离屏 Canvas 上绘制复杂内容(只需执行一次)
offCtx.fillStyle = '#4285f4';
offCtx.beginPath();
// ... 复杂的路径绘制 ...
offCtx.fill();

// 在动画循环中直接使用 drawImage(非常快)
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(offscreen, 0, 0); // 快速绘制预渲染结果
  requestAnimationFrame(animate);
}

实战示例:预渲染复杂图形

javascript
/**
 * 预渲染复杂星形图案
 */
function preRenderStar(ctx, radius, color) {
  const offscreen = document.createElement('canvas');
  const size = radius * 2 + 10;
  offscreen.width = size;
  offscreen.height = size;
  const offCtx = offscreen.getContext('2d');

  const cx = size / 2, cy = size / 2;

  // 绘制复杂的星形(包含渐变、阴影等)
  offCtx.save();
  offCtx.shadowColor = 'rgba(0,0,0,0.3)';
  offCtx.shadowBlur = 8;

  const gradient = offCtx.createRadialGradient(cx, cy, 0, cx, cy, radius);
  gradient.addColorStop(0, '#ffffff');
  gradient.addColorStop(0.5, color);
  gradient.addColorStop(1, '#000000');

  offCtx.beginPath();
  for (let i = 0; i < 10; i++) {
    const r = i % 2 === 0 ? radius : radius * 0.4;
    const angle = (Math.PI / 5) * i - Math.PI / 2;
    const x = cx + r * Math.cos(angle);
    const y = cy + r * Math.sin(angle);
    if (i === 0) offCtx.moveTo(x, y);
    else offCtx.lineTo(x, y);
  }
  offCtx.closePath();

  offCtx.fillStyle = gradient;
  offCtx.fill();
  offCtx.restore();

  return offscreen;
}

// 预渲染(只执行一次)
const starImage = preRenderStar(ctx, 30, '#fbbc05');

// 动画中使用
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 直接 drawImage,无需每帧重新计算渐变和阴影
  ctx.drawImage(starImage, 100, 100);
  ctx.drawImage(starImage, 300, 200);
  requestAnimationFrame(animate);
}

分层渲染

什么是分层渲染

分层渲染将不同类型的图形绘制到不同的 Canvas 层上,通过 CSS 叠加显示。这样修改某一层时,不需要重新绘制其他层。

实现方式

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>分层渲染</title>
  <style>
    .canvas-container {
      position: relative;
      width: 400px;
      height: 300px;
      border: 1px solid #ccc;
    }
    .canvas-container canvas {
      position: absolute;
      top: 0;
      left: 0;
    }
    /* 不同层的 z-index */
    #bgLayer   { z-index: 1; }
    #staticLayer { z-index: 2; }
    #dynamicLayer { z-index: 3; }
    #uiLayer   { z-index: 4; }
  </style>
</head>
<body>
  <h1>Canvas 分层渲染</h1>
  <div class="canvas-container">
    <canvas id="bgLayer" width="400" height="300"></canvas>
    <canvas id="staticLayer" width="400" height="300"></canvas>
    <canvas id="dynamicLayer" width="400" height="300"></canvas>
    <canvas id="uiLayer" width="400" height="300"></canvas>
  </div>

  <script>
    // 各层获取上下文
    const bgCtx = document.getElementById('bgLayer').getContext('2d');
    const staticCtx = document.getElementById('staticLayer').getContext('2d');
    const dynamicCtx = document.getElementById('dynamicLayer').getContext('2d');
    const uiCtx = document.getElementById('uiLayer').getContext('2d');

    // 背景层:只绘制一次
    const bgGrad = bgCtx.createLinearGradient(0, 0, 400, 300);
    bgGrad.addColorStop(0, '#e3f2fd');
    bgGrad.addColorStop(1, '#e8f5e9');
    bgCtx.fillStyle = bgGrad;
    bgCtx.fillRect(0, 0, 400, 300);

    // 静态层:只绘制一次
    staticCtx.fillStyle = '#4285f4';
    staticCtx.fillRect(50, 200, 100, 60);
    staticCtx.fillStyle = '#34a853';
    staticCtx.fillRect(250, 200, 100, 60);

    // 动态层:每帧更新
    let ballX = 0;
    function animateDynamic() {
      dynamicCtx.clearRect(0, 0, 400, 300);
      dynamicCtx.beginPath();
      dynamicCtx.arc(ballX, 150, 15, 0, Math.PI * 2);
      dynamicCtx.fillStyle = '#ea4335';
      dynamicCtx.fill();

      ballX = (ballX + 2) % 400;
      requestAnimationFrame(animateDynamic);
    }
    animateDynamic();

    // UI 层:只在需要时更新
    function updateScore(score) {
      uiCtx.clearRect(0, 0, 400, 300);
      uiCtx.fillStyle = '#333';
      uiCtx.font = 'bold 16px sans-serif';
      uiCtx.fillText(`得分: ${score}`, 10, 25);
    }
    updateScore(0);
  </script>
</body>
</html>

分层策略

层级内容更新频率
背景层天空、地面、背景纹理从不更新
静态层建筑物、固定元素偶尔更新
动态层角色、粒子、动态对象每帧更新
UI 层HUD、文字、菜单按需更新

减少 save/restore

过度使用的问题

javascript
// 不好的做法:过度使用 save/restore
function drawParticles(particles) {
  particles.forEach(p => {
    ctx.save();          // 每个粒子都 save
    ctx.translate(p.x, p.y);
    ctx.rotate(p.angle);
    ctx.fillStyle = p.color;
    ctx.fillRect(-5, -5, 10, 10);
    ctx.restore();       // 每个粒子都 restore
  });
}

优化方案

javascript
// 好的做法:手动管理状态
function drawParticles(particles) {
  // 批量设置相同的属性
  ctx.fillStyle = '#4285f4'; // 如果颜色相同

  particles.forEach(p => {
    // 只有需要 save/restore 时才使用
    if (p.needsTransform) {
      ctx.save();
      ctx.translate(p.x, p.y);
      ctx.rotate(p.angle);
      ctx.fillRect(-5, -5, 10, 10);
      ctx.restore();
    } else {
      ctx.fillRect(p.x - 5, p.y - 5, 10, 10);
    }
  });
}

减少状态切换

批量绘制

javascript
// 不好的做法:频繁切换颜色
function drawBad() {
  ctx.fillStyle = 'red';
  ctx.fillRect(0, 0, 50, 50);
  ctx.fillStyle = 'blue';
  ctx.fillRect(100, 0, 50, 50);
  ctx.fillStyle = 'green';
  ctx.fillRect(200, 0, 50, 50);
  // ... 更多颜色切换
}

// 好的做法:按颜色分组绘制
function drawGood() {
  // 先绘制所有红色
  ctx.fillStyle = 'red';
  redRects.forEach(r => ctx.fillRect(r.x, r.y, r.w, r.h));

  // 再绘制所有蓝色
  ctx.fillStyle = 'blue';
  blueRects.forEach(r => ctx.fillRect(r.x, r.y, r.w, r.h));

  // 再绘制所有绿色
  ctx.fillStyle = 'green';
  greenRects.forEach(r => ctx.fillRect(r.x, r.y, r.w, r.h));
}

Path2D 批量绘制

javascript
// 使用 Path2D 预定义路径
const rectPaths = {
  red: new Path2D(),
  blue: new Path2D(),
  green: new Path2D()
};

// 一次性构建路径
redRects.forEach(r => {
  rectPaths.red.rect(r.x, r.y, r.w, r.h);
});
blueRects.forEach(r => {
  rectPaths.blue.rect(r.x, r.y, r.w, r.h);
});

// 批量绘制
ctx.fillStyle = 'red';
ctx.fill(rectPaths.red);
ctx.fillStyle = 'blue';
ctx.fill(rectPaths.blue);

硬件加速

CSS 硬件加速

css
/* 通过 CSS 触发 GPU 加速层 */
canvas {
  will-change: contents;
  /* 或 */
  transform: translateZ(0);
}

OffscreenCanvas(多线程)

OffscreenCanvas 允许在 Web Worker 中进行 Canvas 绘制,不阻塞主线程:

javascript
// 主线程
const canvas = document.getElementById('myCanvas');
const offscreen = canvas.transferControlToOffscreen();

// 传递给 Worker
const worker = new Worker('canvas-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// canvas-worker.js
self.onmessage = (e) => {
  const canvas = e.data.canvas;
  const ctx = canvas.getContext('2d');

  function animate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // ... 绘制逻辑 ...
    requestAnimationFrame(animate);
  }
  animate();
};

OffscreenCanvas 兼容性

javascript
// 检测 OffscreenCanvas 支持
if (canvas.transferControlToOffscreen) {
  // 支持,使用 Worker
} else {
  // 不支持,回退到主线程渲染
}

其他优化技巧

1. 避免频繁创建对象

javascript
// 不好的做法:每帧创建新对象
function animate() {
  const point = { x: 0, y: 0 }; // 每帧创建
  // ...
}

// 好的做法:复用对象
const reusablePoint = { x: 0, y: 0 };
function animate() {
  reusablePoint.x = newX;
  reusablePoint.y = newY;
  // ...
}

2. 使用整数坐标

javascript
// 不好的做法:浮点数坐标
ctx.fillRect(10.5, 20.3, 50.7, 50.1);

// 好的做法:使用整数坐标(避免亚像素渲染)
ctx.fillRect(10, 20, 50, 50);

3. 避免在动画中使用阴影

javascript
// 不好的做法:动画中每帧使用阴影
function animate() {
  ctx.shadowColor = 'rgba(0,0,0,0.3)';
  ctx.shadowBlur = 10;
  ctx.fillRect(x, y, 100, 100);
  requestAnimationFrame(animate);
}

// 好的做法:预渲染阴影
// 在离屏 Canvas 上预渲染带阴影的图形
const shadowImage = preRenderWithShadow();
function animate() {
  ctx.drawImage(shadowImage, x, y);
  requestAnimationFrame(animate);
}

4. 大画布处理策略

javascript
// 对于超大画布(如 4K),考虑分块渲染
const TILE_SIZE = 512;

function renderLargeCanvas() {
  for (let y = 0; y < totalHeight; y += TILE_SIZE) {
    for (let x = 0; x < totalWidth; x += TILE_SIZE) {
      renderTile(x, y, TILE_SIZE, TILE_SIZE);
    }
  }
}

性能检测工具

javascript
/**
 * Canvas 性能分析器
 */
class CanvasProfiler {
  constructor(ctx) {
    this.ctx = ctx;
    this.metrics = {
      frameCount: 0,
      drawCalls: 0,
      totalDrawTime: 0,
      avgDrawTime: 0,
      fps: 0
    };
    this.frameStart = 0;
    this.lastFPSUpdate = 0;
    this.framesSinceFPSUpdate = 0;
  }

  beginFrame(timestamp) {
    this.frameStart = performance.now();
    this.drawCalls = 0;
  }

  recordDraw(callName, duration) {
    this.drawCalls++;
    this.totalDrawTime += duration;
  }

  endFrame(timestamp) {
    const frameTime = performance.now() - this.frameStart;
    this.metrics.frameCount++;
    this.framesSinceFPSUpdate++;

    // 每秒更新一次 FPS
    if (timestamp - this.lastFPSUpdate >= 1000) {
      this.metrics.fps = this.framesSinceFPSUpdate;
      this.framesSinceFPSUpdate = 0;
      this.lastFPSUpdate = timestamp;
    }

    this.metrics.avgDrawTime = this.totalDrawTime / Math.max(1, this.drawCalls);
  }

  getReport() {
    return {
      ...this.metrics,
      drawCalls: this.drawCalls
    };
  }

  drawOverlay(x, y) {
    const report = this.getReport();
    this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
    this.ctx.fillRect(x, y, 200, 80);

    this.ctx.fillStyle = '#0f0';
    this.ctx.font = '12px monospace';
    this.ctx.fillText(`FPS: ${report.fps}`, x + 10, y + 18);
    this.ctx.fillText(`Draw Calls: ${report.drawCalls}`, x + 10, y + 36);
    this.ctx.fillText(`Avg Draw: ${report.avgDrawTime.toFixed(2)}ms`, x + 10, y + 54);
    this.ctx.fillText(`Total Frames: ${report.frameCount}`, x + 10, y + 72);
  }
}

性能优化检查清单

优化项说明影响
使用离屏 Canvas 预渲染缓存静态或复杂图形
分层渲染分离静态和动态内容
批量绘制按属性分组,减少状态切换
减少 save/restore只在必要时使用
避免动画中使用阴影预渲染到离屏 Canvas
使用整数坐标避免亚像素渲染
复用对象避免每帧创建对象
Path2D预定义路径对象
OffscreenCanvas多线程渲染
CSS will-change提示浏览器创建 GPU 层
限制像素操作减少 getImageData/putImageData
requestAnimationFrame使用标准动画 API

下一节

继续学习:SVG 基本图形

参考链接