Canvas APIを使った描画の最適化
Canvas APIを使った描画を最適化する方法を、実際のコード例とともに詳しく解説します。
Canvas APIを使った描画の最適化
Canvas APIを使った描画を最適化する方法を、実際のコード例とともに詳しく解説します。パフォーマンス向上のためのテクニックから、メモリ管理まで、実践的な最適化手法を紹介します。
Canvas APIの描画最適化の重要性
Canvas APIは、2Dグラフィックスを描画するための強力なAPIですが、適切に最適化しないとパフォーマンスの問題が発生します。特に、物理シミュレーションやアニメーションなど、リアルタイムで描画を更新する場合、最適化が重要になります。
最適化が必要な理由
- 60FPSの維持: 滑らかなアニメーションには60FPS(1秒間に60フレーム)の描画が必要
- メモリ使用量の削減: 不適切な描画はメモリリークの原因になる
- バッテリー消費の削減: 最適化により、モバイル端末のバッテリー消費を削減
- ユーザー体験の向上: 最適化により、快適な操作感を提供
基本的な最適化テクニック
1. 再描画範囲の最小化
画面全体を再描画するのではなく、変更された部分だけを再描画することで、パフォーマンスを向上させます。
/**
* 再描画範囲を最小化する描画関数
*/
export class OptimizedCanvasRenderer {
private ctx: CanvasRenderingContext2D;
private width: number;
private height: number;
private dirtyRegions: Array<{ x: number; y: number; width: number; height: number }> = [];
constructor(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas context not available');
}
this.ctx = ctx;
this.width = canvas.width;
this.height = canvas.height;
}
/**
* 再描画が必要な領域をマーク
*/
markDirty(x: number, y: number, width: number, height: number): void {
this.dirtyRegions.push({ x, y, width, height });
}
/**
* マークされた領域だけをクリア
*/
clearDirtyRegions(): void {
for (const region of this.dirtyRegions) {
this.ctx.clearRect(region.x, region.y, region.width, region.height);
}
this.dirtyRegions = [];
}
/**
* 全体をクリア(必要な場合のみ)
*/
clearAll(): void {
this.ctx.clearRect(0, 0, this.width, this.height);
}
}
2. オフスクリーンキャンバスの活用
複雑な描画をオフスクリーンキャンバスで行い、メインキャンバスにコピーすることで、パフォーマンスを向上させます。
/**
* オフスクリーンキャンバスを使った描画
*/
export class OffscreenCanvasRenderer {
private mainCanvas: HTMLCanvasElement;
private mainCtx: CanvasRenderingContext2D;
private offscreenCanvas: HTMLCanvasElement;
private offscreenCtx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
this.mainCanvas = canvas;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas context not available');
}
this.mainCtx = ctx;
// オフスクリーンキャンバスの作成
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCanvas.width = canvas.width;
this.offscreenCanvas.height = canvas.height;
const offscreenCtx = this.offscreenCanvas.getContext('2d');
if (!offscreenCtx) {
throw new Error('Offscreen canvas context not available');
}
this.offscreenCtx = offscreenCtx;
}
/**
* オフスクリーンキャンバスに描画
*/
drawToOffscreen(drawFunction: (ctx: CanvasRenderingContext2D) => void): void {
this.offscreenCtx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
drawFunction(this.offscreenCtx);
}
/**
* オフスクリーンキャンバスをメインキャンバスにコピー
*/
copyToMain(): void {
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height);
this.mainCtx.drawImage(this.offscreenCanvas, 0, 0);
}
/**
* オフスクリーンキャンバスのコンテキストを取得
*/
getOffscreenContext(): CanvasRenderingContext2D {
return this.offscreenCtx;
}
}
3. 描画コンテキストの再利用
描画コンテキストの設定(色、線の太さなど)を頻繁に変更すると、パフォーマンスが低下します。同じ設定の描画をまとめて行うことで、最適化できます。
/**
* 描画コンテキストの設定を最適化
*/
export class OptimizedContext {
private ctx: CanvasRenderingContext2D;
private currentFillStyle: string | null = null;
private currentStrokeStyle: string | null = null;
private currentLineWidth: number | null = null;
constructor(ctx: CanvasRenderingContext2D) {
this.ctx = ctx;
}
/**
* フィルスタイルを設定(変更時のみ)
*/
setFillStyle(style: string): void {
if (this.currentFillStyle !== style) {
this.ctx.fillStyle = style;
this.currentFillStyle = style;
}
}
/**
* ストロークスタイルを設定(変更時のみ)
*/
setStrokeStyle(style: string): void {
if (this.currentStrokeStyle !== style) {
this.ctx.strokeStyle = style;
this.currentStrokeStyle = style;
}
}
/**
* 線の太さを設定(変更時のみ)
*/
setLineWidth(width: number): void {
if (this.currentLineWidth !== width) {
this.ctx.lineWidth = width;
this.currentLineWidth = width;
}
}
/**
* 描画を開始(設定をリセット)
*/
beginPath(): void {
this.ctx.beginPath();
}
/**
* 描画を終了
*/
fill(): void {
this.ctx.fill();
}
/**
* ストロークを描画
*/
stroke(): void {
this.ctx.stroke();
}
}
高度な最適化テクニック
1. レイヤー分離
背景と前景を別々のキャンバスに描画することで、背景の再描画を避けられます。
/**
* レイヤー分離による最適化
*/
export class LayeredCanvasRenderer {
private backgroundCanvas: HTMLCanvasElement;
private backgroundCtx: CanvasRenderingContext2D;
private foregroundCanvas: HTMLCanvasElement;
private foregroundCtx: CanvasRenderingContext2D;
private mainCanvas: HTMLCanvasElement;
private mainCtx: CanvasRenderingContext2D;
constructor(mainCanvas: HTMLCanvasElement) {
this.mainCanvas = mainCanvas;
const ctx = mainCanvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas context not available');
}
this.mainCtx = ctx;
// 背景キャンバスの作成
this.backgroundCanvas = document.createElement('canvas');
this.backgroundCanvas.width = mainCanvas.width;
this.backgroundCanvas.height = mainCanvas.height;
const bgCtx = this.backgroundCanvas.getContext('2d');
if (!bgCtx) {
throw new Error('Background canvas context not available');
}
this.backgroundCtx = bgCtx;
// 前景キャンバスの作成
this.foregroundCanvas = document.createElement('canvas');
this.foregroundCanvas.width = mainCanvas.width;
this.foregroundCanvas.height = mainCanvas.height;
const fgCtx = this.foregroundCanvas.getContext('2d');
if (!fgCtx) {
throw new Error('Foreground canvas context not available');
}
this.foregroundCtx = fgCtx;
}
/**
* 背景を描画(一度だけ)
*/
drawBackground(drawFunction: (ctx: CanvasRenderingContext2D) => void): void {
drawFunction(this.backgroundCtx);
}
/**
* 前景を描画(毎フレーム)
*/
drawForeground(drawFunction: (ctx: CanvasRenderingContext2D) => void): void {
this.foregroundCtx.clearRect(0, 0, this.foregroundCanvas.width, this.foregroundCanvas.height);
drawFunction(this.foregroundCtx);
}
/**
* レイヤーを合成してメインキャンバスに描画
*/
composite(): void {
this.mainCtx.clearRect(0, 0, this.mainCanvas.width, this.mainCanvas.height);
this.mainCtx.drawImage(this.backgroundCanvas, 0, 0);
this.mainCtx.drawImage(this.foregroundCanvas, 0, 0);
}
}
2. バッチ描画
複数の描画操作をまとめて実行することで、パフォーマンスを向上させます。
/**
* バッチ描画による最適化
*/
export class BatchRenderer {
private ctx: CanvasRenderingContext2D;
private drawOperations: Array<() => void> = [];
constructor(ctx: CanvasRenderingContext2D) {
this.ctx = ctx;
}
/**
* 描画操作を追加
*/
addDrawOperation(operation: () => void): void {
this.drawOperations.push(operation);
}
/**
* すべての描画操作を実行
*/
execute(): void {
// 同じ設定の描画をまとめて実行
this.ctx.save();
for (const operation of this.drawOperations) {
operation();
}
this.ctx.restore();
this.drawOperations = [];
}
}
3. 視覚領域外の描画をスキップ
画面に表示されない領域の描画をスキップすることで、パフォーマンスを向上させます。
/**
* 視覚領域外の描画をスキップ
*/
export class CullingRenderer {
private ctx: CanvasRenderingContext2D;
private viewport: { x: number; y: number; width: number; height: number };
constructor(
ctx: CanvasRenderingContext2D,
viewport: { x: number; y: number; width: number; height: number }
) {
this.ctx = ctx;
this.viewport = viewport;
}
/**
* オブジェクトが視覚領域内にあるかチェック
*/
isVisible(x: number, y: number, width: number, height: number): boolean {
return (
x + width >= this.viewport.x &&
x <= this.viewport.x + this.viewport.width &&
y + height >= this.viewport.y &&
y <= this.viewport.y + this.viewport.height
);
}
/**
* 視覚領域内の場合のみ描画
*/
drawIfVisible(
x: number,
y: number,
width: number,
height: number,
drawFunction: () => void
): void {
if (this.isVisible(x, y, width, height)) {
drawFunction();
}
}
}
メモリ管理の最適化
1. 画像のキャッシュ
頻繁に使用する画像をキャッシュすることで、メモリ使用量を削減し、パフォーマンスを向上させます。
/**
* 画像キャッシュ
*/
export class ImageCache {
private cache: Map<string, HTMLImageElement> = new Map();
private maxSize: number;
constructor(maxSize: number = 100) {
this.maxSize = maxSize;
}
/**
* 画像を取得(キャッシュから、または読み込み)
*/
async getImage(src: string): Promise<HTMLImageElement> {
if (this.cache.has(src)) {
return this.cache.get(src)!;
}
const img = new Image();
img.src = src;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
// キャッシュサイズを制限
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(src, img);
return img;
}
/**
* キャッシュをクリア
*/
clear(): void {
this.cache.clear();
}
}
2. パスの再利用
同じパスを複数回使用する場合、Path2Dオブジェクトを再利用します。
/**
* パスの再利用
*/
export class PathCache {
private cache: Map<string, Path2D> = new Map();
/**
* パスを取得(キャッシュから、または作成)
*/
getPath(key: string, createFunction: () => Path2D): Path2D {
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const path = createFunction();
this.cache.set(key, path);
return path;
}
/**
* キャッシュをクリア
*/
clear(): void {
this.cache.clear();
}
}
パフォーマンス測定
FPSの監視
FPSを監視して、パフォーマンスの問題を早期に発見します。
/**
* FPS監視クラス
*/
export class FPSMonitor {
private frames: number[] = [];
private lastTime: number = performance.now();
private frameCount: number = 0;
private maxSamples: number = 60;
/**
* フレームを記録
*/
recordFrame(): void {
const now = performance.now();
const delta = now - this.lastTime;
this.lastTime = now;
this.frames.push(1000 / delta);
if (this.frames.length > this.maxSamples) {
this.frames.shift();
}
this.frameCount++;
}
/**
* 現在のFPSを取得
*/
getFPS(): number {
if (this.frames.length === 0) {
return 0;
}
const sum = this.frames.reduce((a, b) => a + b, 0);
return Math.round(sum / this.frames.length);
}
/**
* 平均FPSを取得
*/
getAverageFPS(): number {
return this.getFPS();
}
/**
* 最小FPSを取得
*/
getMinFPS(): number {
if (this.frames.length === 0) {
return 0;
}
return Math.round(Math.min(...this.frames));
}
/**
* 最大FPSを取得
*/
getMaxFPS(): number {
if (this.frames.length === 0) {
return 0;
}
return Math.round(Math.max(...this.frames));
}
/**
* リセット
*/
reset(): void {
this.frames = [];
this.frameCount = 0;
this.lastTime = performance.now();
}
}
実践的な実装例
最適化された物理シミュレーションの描画
import { FPSMonitor } from './FPSMonitor';
import { OptimizedCanvasRenderer } from './OptimizedCanvasRenderer';
import { CullingRenderer } from './CullingRenderer';
import Matter from 'matter-js';
/**
* 最適化された物理シミュレーションの描画
*/
export class OptimizedPhysicsRenderer {
private canvas: HTMLCanvasElement;
private renderer: OptimizedCanvasRenderer;
private cullingRenderer: CullingRenderer;
private fpsMonitor: FPSMonitor;
private bodies: Matter.Body[] = [];
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.renderer = new OptimizedCanvasRenderer(canvas);
this.cullingRenderer = new CullingRenderer(
this.renderer['ctx'],
{ x: 0, y: 0, width: canvas.width, height: canvas.height }
);
this.fpsMonitor = new FPSMonitor();
}
/**
* ボディを追加
*/
addBody(body: Matter.Body): void {
this.bodies.push(body);
}
/**
* 描画を更新
*/
update(): void {
this.fpsMonitor.recordFrame();
// 変更された領域だけをクリア
for (const body of this.bodies) {
const bounds = body.bounds;
this.renderer.markDirty(
bounds.min.x - 5,
bounds.min.y - 5,
bounds.max.x - bounds.min.x + 10,
bounds.max.y - bounds.min.y + 10
);
}
this.renderer.clearDirtyRegions();
// 視覚領域内のボディだけを描画
for (const body of this.bodies) {
const bounds = body.bounds;
this.cullingRenderer.drawIfVisible(
bounds.min.x,
bounds.min.y,
bounds.max.x - bounds.min.x,
bounds.max.y - bounds.min.y,
() => {
this.drawBody(body);
}
);
}
}
/**
* ボディを描画
*/
private drawBody(body: Matter.Body): void {
const ctx = this.renderer['ctx'];
ctx.fillStyle = '#FF6B9D';
ctx.beginPath();
if (body.circleRadius) {
ctx.arc(body.position.x, body.position.y, body.circleRadius, 0, Math.PI * 2);
} else {
const vertices = body.vertices;
ctx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i++) {
ctx.lineTo(vertices[i].x, vertices[i].y);
}
ctx.closePath();
}
ctx.fill();
}
/**
* FPSを取得
*/
getFPS(): number {
return this.fpsMonitor.getFPS();
}
}
まとめ
Canvas APIを使った描画を最適化することで、以下のような効果が得られます:
- 60FPSの維持: 滑らかなアニメーションを実現
- メモリ使用量の削減: メモリリークを防止
- バッテリー消費の削減: モバイル端末での動作を改善
- ユーザー体験の向上: 快適な操作感を提供
主な最適化テクニック:
- 再描画範囲の最小化: 変更された部分だけを再描画
- オフスクリーンキャンバスの活用: 複雑な描画を効率化
- 描画コンテキストの再利用: 設定の変更を最小化
- レイヤー分離: 背景と前景を分離
- バッチ描画: 描画操作をまとめて実行
- 視覚領域外の描画をスキップ: 不要な描画を回避
- メモリ管理: 画像やパスのキャッシュ
- パフォーマンス測定: FPSの監視
これらの最適化テクニックを組み合わせることで、高性能なCanvas描画を実現できます。
関連記事
関連ツール
関連記事
TypeScriptで物理シミュレーションを実装する方法
TypeScriptを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
Next.js App Routerでの実装パターン
Next.js App Routerを使った実装パターンを、実際のコード例とともに詳しく解説します。
2026年1月8日
Matter.jsを使った物理シミュレーション入門
Matter.jsを使った物理シミュレーションの実装方法を、実際のコード例とともに解説します。
2026年1月7日