パフォーマンス最適化の実践
Webアプリケーションのパフォーマンスを最適化する実践的な方法を、実際のコード例とともに詳しく解説します。
パフォーマンス最適化の実践
Webアプリケーションのパフォーマンスを最適化する実践的な方法を、実際のコード例とともに詳しく解説します。React、Next.js、Canvas APIなど、様々な技術での最適化テクニックを紹介します。
パフォーマンス最適化の重要性
パフォーマンス最適化は、ユーザー体験を向上させ、サイトの成功に直結する重要な要素です。特に、物理シミュレーションやアニメーションを含むインタラクティブなWebアプリケーションでは、パフォーマンスがユーザー体験を左右します。
パフォーマンス最適化の目標
- 60FPSの維持: 滑らかなアニメーションには60FPSが必要
- 初回読み込み時間の短縮: 3秒以内での初回表示
- メモリ使用量の削減: メモリリークの防止
- バッテリー消費の削減: モバイル端末での動作を改善
Reactでのパフォーマンス最適化
1. React.memoによる再レンダリングの最適化
不要な再レンダリングを防ぐために、React.memoを使用します。
import { memo } from 'react';
interface ToolCardProps {
tool: ToolMetadata;
onClick: (id: string) => void;
}
/**
* React.memoで再レンダリングを最適化
*/
export const ToolCard = memo(function ToolCard({
tool,
onClick,
}: ToolCardProps) {
return (
<div onClick={() => onClick(tool.slug)}>
<h3>{tool.name}</h3>
<p>{tool.description}</p>
</div>
);
}, (prevProps, nextProps) => {
// カスタム比較関数(オプション)
return prevProps.tool.slug === nextProps.tool.slug;
});
2. useMemoによる計算結果のキャッシュ
重い計算をuseMemoでキャッシュします。
'use client';
import { useMemo } from 'react';
export function ExpensiveComponent({ items }: { items: Item[] }) {
// 重い計算をuseMemoでキャッシュ
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// フィルタリングもキャッシュ
const filteredItems = useMemo(() => {
return sortedItems.filter((item) => item.isActive);
}, [sortedItems]);
return (
<div>
{filteredItems.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
3. useCallbackによる関数のメモ化
コールバック関数をuseCallbackでメモ化します。
'use client';
import { useCallback, useState } from 'react';
export function ToolList({ tools }: { tools: ToolMetadata[] }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
// コールバック関数をメモ化
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
return (
<div>
{tools.map((tool) => (
<ToolCard
key={tool.slug}
tool={tool}
onClick={handleClick}
isSelected={selectedId === tool.slug}
/>
))}
</div>
);
}
4. コード分割(Code Splitting)
動的インポートを使って、コードを分割します。
import { lazy, Suspense } from 'react';
// 動的インポート
const HeavyComponent = lazy(() => import('./HeavyComponent'));
export function App() {
return (
<div>
<Suspense fallback={<div>読み込み中...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
Next.jsでのパフォーマンス最適化
1. 画像の最適化
Next.jsのImageコンポーネントを使って、画像を最適化します。
import Image from 'next/image';
export function OptimizedImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
2. フォントの最適化
Next.jsのnext/fontを使って、フォントを最適化します。
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja" className={inter.className}>
<body>{children}</body>
</html>
);
}
3. 静的生成とISR
静的生成やISR(Incremental Static Regeneration)を使って、パフォーマンスを向上させます。
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const articles = getAllArticles();
return articles.map((article) => ({
slug: article.slug,
}));
}
export const revalidate = 3600; // ISR: 1時間ごとに再生成
Canvas APIでのパフォーマンス最適化
1. 再描画範囲の最小化
変更された部分だけを再描画します。
export class OptimizedCanvas {
private ctx: CanvasRenderingContext2D;
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;
}
markDirty(x: number, y: number, width: number, height: number): void {
this.dirtyRegions.push({ x, y, width, height });
}
clearDirty(): void {
for (const region of this.dirtyRegions) {
this.ctx.clearRect(region.x, region.y, region.width, region.height);
}
this.dirtyRegions = [];
}
}
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);
}
}
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. 適応的質点数調整
FPSを監視して、質点数を自動調整します。
export class AdaptivePhysicsEngine {
private engine: Matter.Engine;
private targetFPS: number = 60;
private currentFPS: number = 60;
private pointCount: number = 30;
private minPointCount: number = 10;
private maxPointCount: number = 100;
constructor() {
this.engine = Matter.Engine.create();
}
updateFPS(fps: number): void {
this.currentFPS = fps;
// FPSが低い場合は質点数を減らす
if (fps < this.targetFPS * 0.9) {
this.pointCount = Math.max(this.minPointCount, this.pointCount - 5);
}
// FPSが高い場合は質点数を増やす
else if (fps > this.targetFPS * 1.1) {
this.pointCount = Math.min(this.maxPointCount, this.pointCount + 5);
}
}
getPointCount(): number {
return this.pointCount;
}
}
2. スリープモードの有効化
動いていないボディをスリープ状態にします。
export function enableSleeping(engine: Matter.Engine): void {
engine.enableSleeping = true;
// スリープ条件を調整
const engineAny = engine as any;
engineAny.sleepThreshold = 0.04; // 速度が0.04以下でスリープ
}
3. 反復回数の調整
物理エンジンの反復回数を調整します。
export function optimizeEngineIterations(engine: Matter.Engine): void {
const engineAny = engine as any;
// パフォーマンスに応じて反復回数を調整
engineAny.positionIterations = 6; // 位置の反復回数
engineAny.velocityIterations = 4; // 速度の反復回数
engineAny.constraintIterations = 2; // 制約の反復回数
}
メモリ管理の最適化
1. メモリリークの防止
イベントリスナーやタイマーを適切にクリーンアップします。
'use client';
import { useEffect, useRef } from 'react';
export function useAnimationFrame(callback: () => void) {
const requestRef = useRef<number>();
const previousTimeRef = useRef<number>();
useEffect(() => {
const animate = (time: number) => {
if (previousTimeRef.current !== undefined) {
callback();
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
};
requestRef.current = requestAnimationFrame(animate);
// クリーンアップ
return () => {
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}, [callback]);
}
2. オブジェクトプール
頻繁に作成・破棄されるオブジェクトをプールで管理します。
export class ObjectPool<T> {
private pool: T[] = [];
private createFn: () => T;
private resetFn: (obj: T) => void;
constructor(
createFn: () => T,
resetFn: (obj: T) => void,
initialSize: number = 10
) {
this.createFn = createFn;
this.resetFn = resetFn;
// 初期プールを作成
for (let i = 0; i < initialSize; i++) {
this.pool.push(createFn());
}
}
acquire(): T {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.createFn();
}
release(obj: T): void {
this.resetFn(obj);
this.pool.push(obj);
}
}
パフォーマンス測定
1. 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++;
}
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);
}
getMinFPS(): number {
if (this.frames.length === 0) return 0;
return Math.round(Math.min(...this.frames));
}
getMaxFPS(): number {
if (this.frames.length === 0) return 0;
return Math.round(Math.max(...this.frames));
}
}
2. パフォーマンスプロファイリング
パフォーマンスプロファイリングを使って、ボトルネックを特定します。
export class PerformanceProfiler {
private marks: Map<string, number> = new Map();
mark(name: string): void {
this.marks.set(name, performance.now());
}
measure(name: string): number {
const startTime = this.marks.get(name);
if (startTime === undefined) {
throw new Error(`Mark "${name}" not found`);
}
return performance.now() - startTime;
}
measureAndLog(name: string): void {
const duration = this.measure(name);
console.log(`${name}: ${duration.toFixed(2)}ms`);
}
}
実践的な最適化例
最適化された物理シミュレーションコンポーネント
'use client';
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import Matter from 'matter-js';
import { FPSMonitor } from './FPSMonitor';
import { AdaptivePhysicsEngine } from './AdaptivePhysicsEngine';
export function OptimizedPhysicsSimulation() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<AdaptivePhysicsEngine | null>(null);
const fpsMonitorRef = useRef<FPSMonitor>(new FPSMonitor());
const [bodies, setBodies] = useState<Matter.Body[]>([]);
// エンジンの初期化(useMemoで一度だけ実行)
const engine = useMemo(() => {
const adaptiveEngine = new AdaptivePhysicsEngine();
engineRef.current = adaptiveEngine;
return adaptiveEngine;
}, []);
// 描画関数(useCallbackでメモ化)
const draw = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const body of bodies) {
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();
}
}, [bodies]);
useEffect(() => {
if (!engine) return;
// アニメーションループ
const animate = () => {
fpsMonitorRef.current.recordFrame();
const fps = fpsMonitorRef.current.getFPS();
// FPSに応じて質点数を調整
if (engineRef.current) {
engineRef.current.updateFPS(fps);
}
// 物理エンジンの更新
engine.update();
setBodies([...engine.getBodies()]);
// 描画
draw();
requestAnimationFrame(animate);
};
animate();
}, [engine, draw]);
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
className="border border-gray-300 rounded"
/>
<div className="mt-4">
FPS: {fpsMonitorRef.current.getFPS()}
</div>
</div>
);
}
まとめ
パフォーマンス最適化は、以下のようなテクニックを組み合わせることで実現できます:
- Reactでの最適化: React.memo、useMemo、useCallback、コード分割
- Next.jsでの最適化: 画像最適化、フォント最適化、静的生成、ISR
- Canvas APIでの最適化: 再描画範囲の最小化、オフスクリーンキャンバス、視覚領域外の描画をスキップ
- 物理シミュレーションでの最適化: 適応的質点数調整、スリープモード、反復回数の調整
- メモリ管理: メモリリークの防止、オブジェクトプール
- パフォーマンス測定: FPSの監視、パフォーマンスプロファイリング
これらのテクニックを組み合わせることで、高性能なWebアプリケーションを実現できます。
関連記事
関連ツール
関連記事
TypeScriptの型安全性を活かした開発
TypeScriptの型安全性を活かした開発方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
React Hooksを使った物理シミュレーション
React Hooksを使って物理シミュレーションを実装する方法を、実際のコード例とともに詳しく解説します。
2026年1月8日
カラーピッカーツールの使い方・活用方法
カラーピッカーツールの基本的な使い方から、デザイン作業での活用方法まで詳しく解説します。
2026年1月8日