プログラミング学習

パフォーマンス最適化の実践

Webアプリケーションのパフォーマンスを最適化する実践的な方法を、実際のコード例とともに詳しく解説します。

2400文字
パフォーマンス最適化ReactNext.jsWeb開発

パフォーマンス最適化の実践

Webアプリケーションのパフォーマンスを最適化する実践的な方法を、実際のコード例とともに詳しく解説します。React、Next.js、Canvas APIなど、様々な技術での最適化テクニックを紹介します。

パフォーマンス最適化の重要性

パフォーマンス最適化は、ユーザー体験を向上させ、サイトの成功に直結する重要な要素です。特に、物理シミュレーションやアニメーションを含むインタラクティブなWebアプリケーションでは、パフォーマンスがユーザー体験を左右します。

パフォーマンス最適化の目標

  1. 60FPSの維持: 滑らかなアニメーションには60FPSが必要
  2. 初回読み込み時間の短縮: 3秒以内での初回表示
  3. メモリ使用量の削減: メモリリークの防止
  4. バッテリー消費の削減: モバイル端末での動作を改善

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>
  );
}

まとめ

パフォーマンス最適化は、以下のようなテクニックを組み合わせることで実現できます:

  1. Reactでの最適化: React.memo、useMemo、useCallback、コード分割
  2. Next.jsでの最適化: 画像最適化、フォント最適化、静的生成、ISR
  3. Canvas APIでの最適化: 再描画範囲の最小化、オフスクリーンキャンバス、視覚領域外の描画をスキップ
  4. 物理シミュレーションでの最適化: 適応的質点数調整、スリープモード、反復回数の調整
  5. メモリ管理: メモリリークの防止、オブジェクトプール
  6. パフォーマンス測定: FPSの監視、パフォーマンスプロファイリング

これらのテクニックを組み合わせることで、高性能なWebアプリケーションを実現できます。

関連記事

関連ツール

関連記事