JEPA4Japan · チュートリアル

第15章:1万人を一人ずつ探さない

7,948文字 20分で読めます #Canvas#Frontend Engineering#Infinite Canvas#ELI5

再現可能なBenchmarkを作り、Invalidation、Culling、Spatial Index、Cache、LOD、Memory上限の効果を順に測ります。

コース進捗 コース目次 18レッスン中 18件を公開中

第I部:描く前に描画面を選ぶ——プロダクト、ピクセル、座標

  1. 01 第1章:まだ描かない——Canvasはプロダクト設計ではない 公開中
  2. 02 第2章:すぐに記憶を失うピクセルの紙 公開中
  3. 03 第3章:お絵描きを再現可能なレシピにする 公開中
  4. 04 第4章:4枚の地図と1台のカメラ 公開中

第II部:ピクセル世界に頭脳を与える——モデル、スケジューリング、入力、ツール

  1. 05 第5章:ピクセル世界に台帳を作る 公開中
  2. 06 第6章:ランプが点いたときだけ描き直す——Render SchedulerとReactの境界 公開中
  3. 07 第7章:マウス、指、ペンに同じ言葉を話してもらう 公開中
  4. 08 第8章:細い縁を調べる前に大きな箱を探す 公開中
  5. 09 第9章:ツールは信号機であり、Booleanの袋ではない 公開中

第III部:「ドラッグできる」から「信頼できる」へ——操作、文字、Asset、復旧

  1. 10 第10章:触って気持ちよいエディターにする 公開中
  2. 11 第11章:描かれた文字は編集できる文字ではない 公開中
  3. 12 第12章:借りた画像を勝手に箱へ詰めてはいけない 公開中
  4. 13 第13章:タイムマシンと古い箱 公開中
  5. 14 第14章:正しく見えることと、本当に正しいことは違う 公開中

第IV部:マスターの判断——Performance、Worker、GPU、SDK、共同編集、AI

  1. 15 第15章:1万人を一人ずつ探さない 現在のレッスン
  2. 16 第16章:受付を厨房へ入れない——WorkerとGPUへの更新 公開中
  3. 17 第17章:車を自作するか、実績あるシャーシを買うか 公開中
  4. 18 第18章:人とAIが同じ台帳を編集する 公開中

5歳児にもわかるゲームから始めよう

この章の 1 つの真実: パフォーマンスの最適化は、最適化のコツを覚えることではなく、どのステップが遅いかを見つけることから始まります。

10,000 枚の子供用カードを遊び場に置き、先生に「Xiaoyu」を見つけてもらいます。方法 A は、すべてのカードを最初から最後まで読み取ります。方法 B では、まず都市ボックス、次に学校ボックス、教室コンパートメント、最後に座席をチェックします。これを予測してください。現在教室で 10,000 枚のカードのうち 40 枚だけが表示されている場合、教師は座席表を描くときに残りの 9,960 枚に色を付ける必要がありますか?いいえ、すべてのカードに大きな写真、影、透明なシート、アニメーション名が必要な場合、2,000 枚のカードのほうが 100,000 個の単純なドットよりも高速である必要があるかどうかを予測してください。必ずしもそうとは限りません。

教師にセグメント化されたストップウォッチを渡します。質問の受信、名簿の更新、候補教室の検索、名前の確認、座席の描画、紙の表示などの各段階で 1 回押します。 p95 の「名前の確認」に 12 ミリ秒かかる場合、これ以上きれいなペイントブラシを使用しても役に立ちません。実際のボトルネックが大きな写真のデコードである場合、シート ディレクトリを深くしても役に立ちません。

  1. 各ステージの時間最初に最も遅いステップを見つけます
  2. 窓からだけ見てください目に見えないものは描かない
  3. まずは教室を探してください小さな候補セットを正確に検査する
  4. 離れたところからスケッチを描く詳細のみを近くで表示
  5. 古いカードを削除するキャッシュには制限が必要です
まず推測し、次に測定します。問題の原因は「人を見つけること」なのか、それとも「写真を描くこと」なのか。トレースはそれらを分離する必要があります。

平均的なものには、少数のひどい経験が隠れています。 100 回のドラッグのうち 95 回のドラッグに 4 ミリ秒かかり、5 回のドラッグに 80 ミリ秒かかる場合、平均値は許容範囲内に見えるかもしれませんが、インターフェイスには目に見えて頻繁に問題が発生します。 p50、p95、および p99 を一緒に検査し、代表的なデバイスからの生のサンプルとトレースを保持します。

おもちゃを Canvas の概念に置き換える

子ども探しゲームCanvas Lab目的
セグメント化されたストップウォッチ性能マーク・尺度個別の入力、更新、クエリ、レンダリング、および表示
車を押してから目に見える反応が現れるまでの時間Interaction Latency入力タイムスタンプから次に表示されるフレームまで
最も遅い少数派p95 / p99GC、キャッシュ スラッシング、ラージ オブジェクトを公開する
教師は 1 つの仕事全体に専念しているLong Taskメインスレッドがビジー状態になりすぎて入力を処理できない
ウィンドウの境界Viewport Culling表示されている候補のみを保持する
市、学校、教室のディレクトリSpatial IndexBroad Phase でクエリ セットをすばやく絞り込む
顔で確認する狭位相ヒットテスト候補に対して正確な Geometry を実行する
離れたところに小さなカラーブロックのみを描画しますLevel of Detail (LOD)画面上のサイズから複雑さを選択します
カードの束を一度に持ち歩くBatching状態の切り替えと Draw Calls を削減します。
よく使うカードをコピーするBitmap / パス Cache高価な結果を再利用する
期限切れのカードを削除するEvictionメモリバジェットを尊重する
いくつかの透明なレイヤーLayer Separation静的な背景と高周波オーバーレイを個別に無効にする

類推はここで止まります。実際の空間インデックスは、無限にネストされたファイル キャビネットではありません。更新コスト、境界の重複、メモリ使用、および縮退のケースがあります。 Culling は Document から何も削除しません。現在の Render Pass の非表示の Shape へのアクセスを回避するだけです。 LOD は、セマンティックまたはヒット テストの真実を変更してはならず、視覚的な詳細のみを変更してはなりません。 GPU の動作は、メインスレッドのフレーム チャートだけからは推測できません。ブラウザ/GPU ツールと測定が必要です。

まず誤った思い込みを捨てる

  • **「Canvasのオブジェクトが多いので描画が遅い。」**オブジェクトの総数、表示オブジェクト、パスの複雑さ、ピクセル領域、透明レイヤー、テキスト、画像、変更頻度はすべてコストに影響します。
  • **「まず WebGL/WebGPU に切り替えてください。」**p95 が Document クローン作成、Geometry クエリ、または同期リードバックによって支配されている場合、別の Renderer の方が悪化する可能性があります。
  • **「Spatial Index は常に高速です。」**Shape が各フレームを移動するたびにインデックスを維持すると、スキャンよりもコストがかかる可能性があります。クエリ/更新率を測定します。
  • **「Cache が大きいほど、常に速度が向上します。」**無効化を行わないと、古いコンテンツが描画されます。エビクションなしではビットマップが保持され、メモリ負荷が生じ、GC がトリガーされます。 Cache のヒット率とバイト数が表示される必要があります。
  • **「開発マシンでは 60 FPS に達したので、これで終わりです。」**ハイエンド デスクトップ上の空の Document は、代表デバイス/シーンではありません。予算は、ローエンドのハードウェア、高額の DPR、大型ディスプレイ、および実際のコンテンツの配信をカバーする必要があります。
  • **「平均 8 ミリ秒で合格します。」**p99 は 140ms になる可能性があります。 Interaction Latency には入力キューイングが含まれており、レンダリング時間だけとは異なります。
  • **「getImageData() は 1 つの関数呼び出しにすぎません。」**同期 Readback では、パイプラインが強制的に待機する可能性があります。高温の通路に置かないでください。
  • **「毎回いくつかのオブジェクトを割り当てることは問題ではありません。」**高頻度の Allocation が蓄積すると、周期的なドラッグ ヒッチとして見える GC スパイクが発生します。

本番環境に必要な知識

前提条件

第 5 章の Document は、安定したレコードと変更セットを提供します。第 6 章のスケジューラは、常に再描画する代わりに無効化をサポートします。第 8 章の GeometryKernel は、境界、正確なヒット テスト、および空間クエリ インターフェイスを提供します。これらの境界がなければ、プロファイルには巨大な pointermove が 1 つだけ表示されます。モデル、クエリ、または描画が遅いかどうかを明らかにすることはできません。

体系的な知識

測定。Frame Time は、フレームのすべてのステージで費やされる合計時間です。 Interaction Latency は、入力イベントから次の関連ピクセルが表示されるまで実行されます。 3 つのクロックを区別します。同期 performance.now() はラップされた CPU 関数のみを測定します。ユーザー タイミングはカスタム ステージを測定します。ブラウザのイベント タイミング/トレースには、入力キュー、ハンドラー、レンダリング、および次のペイントをインタラクション定義に含めることができます。 requestAnimationFrame のコールバックは paint の前に実行されるため、その到着時刻を「ピクセルが表示されるようになった」ように表示することはできません。平均だけを報告するのではなく、p50/p95/p99 を収集します。 Long Task は、メインスレッドのブロックを示します。 DevTools は、スクリプト作成、レンダリング、ペイント、および合成を分離します。 GPU ツールは、テクスチャのアップロードとフィルレートの証拠を追加します。 Memory の場合、Allocation レート、GC 一時停止、Heap、推定 ArrayBuffer/Bitmap バイト、および Retained Objects を区別します。ベンチマークのブラウザー、デバイス、電源モード、DPR、ビューポート、フォント/アセット キャッシュ状態、およびデータ シードを修正します。 「代表的なデバイスで、ドラッグ p95 ≤ 16ms、ヒット テスト p95 ≤ 4ms、安定した 10 分間後のヒープ増加率 ≤ 10%」などの Performance Budget を記述します。

**最適化の順序。**まず無効な Document アップデートを避けてください。次に、変更セットで無効化を実行します。次に、Viewport Culling を追加して、画面外のオブジェクトが細かいジオメトリと描画をスキップできるようにします。 Broad Phase スキャンがまだ遅い場合は、Spatial Index を導入してください。候補者のみを生成します。 GeometryKernel の Narrow Phase と Z オーダーが最終的なヒット結果を決定します。その後、画面上のサイズに基づいて LOD を、スタイル/マテリアルに基づいて Batching を、静的な背景と動的なオーバーレイの間に Layer Separation を追加します。

キャッシュキーはキャッシュされたものと一致する必要があります。ワールド空間ジオメトリのみを含む Path2D は、通常、{shapeId, geometryRevision} を使用します。ラスター化されたピクセルのキャッシュには、{styleRevision, scaleBucket, dpr, colorSpace} も含まれている必要があります。変更セットから両方を正確に無効にします。 1 つの記憶されたキー式をすべてのキャッシュに強制しないでください。 Image Atlas は、多くの小さなイメージのテクスチャ スイッチを減らしますが、パディング、更新、およびテクスチャの最大寸法を処理する必要があります。 Dirty Rectangles は、オーバーラップ、透明度、合成を正しく導出できる場合にのみ使用します。決して古い位置を省略しないでください。配列と一時ベクトルを再利用して、Allocation を削減します。有効な DPR とバッキング ピクセル領域を制限して、大きなディスプレイがピクセルコストを二乗で増やさないようにします。同期 Readback をホット パスから禁止します。

メモリ バジェットは、Document、Spatial Index、パス/Bitmap キャッシュ、イメージ デコード、および Export タイルをカバーする必要があります。 LRU/Eviction をエントリ数ではなく推定バイト数で制御します。 Bitmap のサイズは、幅 × 高さ × ピクセルあたりのバイト数から推定できます。 Browser 内部 Path2D メモリには正確に標準化された読み取り値がないため、制御された推定値、エントリ制限、および代表デバイスのソーク/ヒープ データを一緒に使用して制限します。例の 96 を実際の測定値として決して扱わないでください。ページが非表示になった場合、またはシステムが Memory 圧力を報告した場合は、プロアクティブに機能を低下させます。 Cache Invalidation の正確性はヒット率よりも重要です。古い Shape を表示するのではなく再描画します。

100,000 個の単純なシェイプがあるが、表示されているのは 200 個だけであるシーンは、インデックス/クエリとカリングによって大部分が占められています。シャドウ、テキスト、アルファ、イメージ、アニメーションを含む 2,000 の完全に表示されたシェイプを含むシーンは、ペイント、ピクセル塗りつぶし、フォント、およびイメージのアップロードによって占められる場合があります。 1 つの「オブジェクト数」グラフで両方の結論を正当化することはできません。

根拠と互換性(確認日:2026-08-29)

高解像度のマークとメジャーには User Timing API を使用します。イベントから次のペイントまでの標準化されたエントリは、W3C Event Timing によって記述されます。ブラウザとイベント タイプによる機能検出も可能です。特徴検出を備えた Long Tasks API を介して長いタスクを監視します。コールバックのタイミングとバックグラウンドのスロットリングについては、MDN requestAnimationFrame を参照してください。 Memory API の可用性とセマンティクスは異なります。 performance.measureUserAgentSpecificMemory() では依然として安全なコンテキストと互換性チェックが必要です。 MDN を参照してください。サポートされていない場合は、Heap スナップショットを使用し、メトリクスを処理し、自己報告バイトをキャッシュします。決して捏造されたゼロではありません。

本章の実装ステップ

**出発点:**第 14 章では、正確性をテストして Render Time を収集できますが、それでもすべてのフレームがすべてスキャンされます。**フィニッシュライン:**同じ決定論的シーンには、完全、無効化、Culling、Spatial Index + Cache + LOD の 4 つの実装と 1 つのベンチマークがあります。レポートには、Document/可視カウント、フレーム、ヒット、ドラッグ、Heap、Export Memory が含まれます。

次のファイルとインターフェイスを追加します。

  • src/engine/perf/scenes.ts: 固定シードから 100,000 個の単純なオブジェクトと 2,000 個の複雑なオブジェクトを生成します。
  • src/engine/spatial/GridIndex.ts: ブロードフェーズインデックス。
  • src/engine/render/strategies.ts: 1 つの Renderer コントラクトを共有する 4 つの戦略。
  • src/engine/cache/LruByteCache.ts: バイト制限のあるパス/Bitmap キャッシュ。
  • bench/canvas.bench.ts: ウォームアップ、サンプル、パーセンタイル、および環境メタデータ。
  • src/engine/perf/__tests__/index-equivalence.test.ts: インデックス付きクエリの結果はフルスキャンの結果と同じです。

以下の完全な最小限の実装は、インデックス作成、カリング、LOD、キャッシュの無効化、および測定セマンティクスを示しています。グリッドインデックスを使用します。本番環境では、クエリプロトコルを変えずに、データ分布に適した R-tree へ置き換えられます。

type Bounds = Readonly<{ minX: number; minY: number; maxX: number; maxY: number }>;
type Shape = Readonly<{
  id: string;
  rev: number;
  x: number;
  y: number;
  w: number;
  h: number;
  label: string;
}>;
const boundsOf = (s: Shape): Bounds => ({ minX: s.x, minY: s.y, maxX: s.x + s.w, maxY: s.y + s.h });
const overlaps = (a: Bounds, b: Bounds) =>
  a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY;

export class GridIndex {
  private cells = new Map<string, Set<string>>();
  private entries = new Map<string, Bounds>();
  constructor(private readonly cellSize = 512) {}
  private keys(b: Bounds) {
    const keys: string[] = [];
    for (let y = Math.floor(b.minY / this.cellSize); y <= Math.floor(b.maxY / this.cellSize); y++)
      for (let x = Math.floor(b.minX / this.cellSize); x <= Math.floor(b.maxX / this.cellSize); x++)
        keys.push(`${x}:${y}`);
    return keys;
  }
  upsert(id: string, bounds: Bounds) {
    this.remove(id);
    this.entries.set(id, bounds);
    for (const key of this.keys(bounds)) {
      const cell = this.cells.get(key) ?? new Set<string>();
      cell.add(id);
      this.cells.set(key, cell);
    }
  }
  remove(id: string) {
    const old = this.entries.get(id);
    if (!old) return;
    for (const key of this.keys(old)) {
      const cell = this.cells.get(key);
      cell?.delete(id);
      if (cell?.size === 0) this.cells.delete(key);
    }
    this.entries.delete(id);
  }
  query(area: Bounds) {
    const ids = new Set<string>();
    for (const key of this.keys(area))
      for (const id of this.cells.get(key) ?? [])
        if (overlaps(this.entries.get(id)!, area)) ids.add(id);
    return [...ids];
  }
}

class LruPathCache {
  private map = new Map<string, { path: Path2D; bytes: number }>();
  private used = 0;
  constructor(private readonly budgetBytes: number) {}
  get(key: string) {
    const item = this.map.get(key);
    if (!item) return;
    this.map.delete(key);
    this.map.set(key, item);
    return item.path;
  }
  set(key: string, path: Path2D, bytes: number) {
    const old = this.map.get(key);
    if (old) {
      this.used -= old.bytes;
      this.map.delete(key);
    }
    this.map.set(key, { path, bytes });
    this.used += bytes;
    while (this.used > this.budgetBytes) {
      const first = this.map.keys().next().value as string | undefined;
      if (!first) break;
      this.used -= this.map.get(first)!.bytes;
      this.map.delete(first);
    }
  }
  invalidateShape(id: string) {
    for (const [key, item] of this.map)
      if (key.startsWith(`${id}:`)) {
        this.used -= item.bytes;
        this.map.delete(key);
      }
  }
  bytes() {
    return this.used;
  }
}

type Strategy = 'full' | 'invalidated' | 'culled' | 'indexed-cached-lod';
export class SceneRenderer {
  private index = new GridIndex();
  private cache = new LruPathCache(16 * 1024 * 1024);
  private dirty = new Set<string>();
  private shapes = new Map<string, Shape>();
  upsert(shape: Shape) {
    this.shapes.set(shape.id, shape);
    this.index.upsert(shape.id, boundsOf(shape));
    this.cache.invalidateShape(shape.id);
    this.dirty.add(shape.id);
  }
  render(ctx: CanvasRenderingContext2D, viewport: Bounds, zoom: number, strategy: Strategy) {
    if (strategy === 'invalidated' && this.dirty.size === 0)
      return { candidates: 0, drawn: 0, cacheBytes: this.cache.bytes() };
    // この関数に入る前に、Host は Camera に従って bitmap 全体を消去済みである。この例の invalidation はフレームを予定するかどうかだけを最適化し、Dirty Rect ではない。
    const candidates =
      strategy === 'full' || strategy === 'invalidated'
        ? [...this.shapes.keys()]
        : strategy === 'culled'
          ? [...this.shapes.values()]
              .filter((s) => overlaps(boundsOf(s), viewport))
              .map((s) => s.id)
          : this.index.query(viewport);
    let drawn = 0;
    for (const id of candidates) {
      const s = this.shapes.get(id)!;
      if (!overlaps(boundsOf(s), viewport)) continue;
      const simple = strategy === 'indexed-cached-lod' && Math.max(s.w, s.h) * zoom < 12;
      const key = `${s.id}:${s.rev}:${simple ? 'low' : 'high'}`;
      let path = strategy === 'indexed-cached-lod' ? this.cache.get(key) : undefined;
      if (!path) {
        path = new Path2D();
        path.rect(s.x, s.y, s.w, s.h);
        if (strategy === 'indexed-cached-lod') this.cache.set(key, path, 96);
      }
      ctx.fillStyle = '#e2e8f0';
      ctx.fill(path);
      ctx.strokeStyle = '#334155';
      ctx.stroke(path);
      if (!simple) {
        ctx.fillStyle = '#0f172a';
        ctx.fillText(s.label, s.x + 4, s.y + 14);
      }
      drawn++;
    }
    this.dirty.clear();
    return { candidates: candidates.length, drawn, cacheBytes: this.cache.bytes() };
  }
}

export function percentile(samples: readonly number[], p: number) {
  if (!samples.length) throw new Error('NO_SAMPLES');
  const sorted = [...samples].sort((a, b) => a - b);
  return sorted[Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1)];
}
export async function benchmarkCpuStage(name: string, rounds: number, task: () => void) {
  for (let i = 0; i < 30; i++) task();
  const samples: number[] = [];
  for (let i = 0; i < rounds; i++) {
    const start = performance.now();
    task();
    samples.push(performance.now() - start);
    await Promise.resolve();
  }
  return {
    name,
    samples,
    p50: percentile(samples, 0.5),
    p95: percentile(samples, 0.95),
    p99: percentile(samples, 0.99),
  };
}

検索ゲームに戻ります。query(viewport) は、窓と交差する教室ボックスのみを開きます。 overlaps が最終確認です。キャッシュはコピーされたカードのセットです。リビジョンが変更されると、古いコピーはすぐに破棄されます。 LRU が 16MB を超えると、最も最近使用されていないカードが最初に削除されます。 benchmarkCpuStage は、「この同期作業にはどのくらい時間がかかりましたか?」 のみ答えることができます。その結果に Frame/Drag/Present というラベルを付けることはできません。これらには、実際のブラウザ入力、イベント タイミング、およびトレースが必要です。チェーン内のすべてのリンクにはカウントがあるため、「速く感じられる」ということが証拠になることはありません。

等価性テストにより、最適化によって結果が変更されるのを防ぎます。予算テストは、パフォーマンスがサイレントに低下するのを防ぎます。

import { expect, test } from 'vitest';
import { GridIndex, percentile } from '../performance';

test('Spatial Index のクエリ結果は Broad Phase の全件走査と完全に等価である', () => {
  const index = new GridIndex(64);
  const items = Array.from({ length: 10_000 }, (_, i) => ({
    id: `s-${i}`,
    minX: (i % 100) * 20,
    minY: Math.floor(i / 100) * 20,
    maxX: (i % 100) * 20 + 12,
    maxY: Math.floor(i / 100) * 20 + 12,
  }));
  for (const { id, ...bounds } of items) index.upsert(id, bounds);
  const area = { minX: 180, minY: 240, maxX: 820, maxY: 760 };
  const scan = items
    .filter(
      (s) =>
        s.minX <= area.maxX && s.maxX >= area.minX && s.minY <= area.maxY && s.maxY >= area.minY,
    )
    .map((s) => s.id)
    .sort();
  expect(index.query(area).sort()).toEqual(scan);
});

test('記録済み fixture に対する nearest-rank percentile gate の計算が正しい', () => {
  const samples = [7.2, 8.1, 8.4, 9.0, 9.2, 10.1, 11.4, 12.0, 12.8, 14.9];
  expect(percentile(samples, 0.95)).toBeLessThanOrEqual(16);
});

上記の 10 個の数値は、パーセンタイルの実装のみを検証します。これらは代表的なデバイスのパフォーマンスを示すものではありません。 npm run benchmark -- --scene=100k-simple --device=representative を実行する場合、実際のハーネスは documentSize、visibleShapes、cpuStage、frame.p50/p95/p99、hitTest、drag、heap、cacheEstimatedBytes、exportPeakBytes、およびブラウザ/DPR/viewport/seed をレポートする必要があります。各メトリクスがマイクロベンチマーク、イベント タイミング、またはトレースから取得されたものであるかどうかをラベル付けする必要があります。 4 つの戦略はすべて同じシーンを使用する必要があります。期待されるのは、「戦略 4 が常に勝つ」ということではなく、利益とコストを明確に記載するレポート (たとえば、full p95 42ms → indexed p95 9ms) でありながら、インデックス更新 p95 が予算内にとどまることです。 npm exec vitest run src/engine/perf を実行します。インデックス付けされたクエリとスキャンされたクエリは同等であること、キャッシュ リビジョンの更新では古いパスが返されることはなく、LRU の推定バジェットとエントリ制限の両方が保持されることが期待されます。

意図的に壊してみる

注入された失敗症状証拠修正回帰テスト回復
100,000 個のオブジェクト、ほとんどが画面外フルスキャンスクリプトの負荷が高い候補 100,000、描画 200カリング + インデックスまばらな大場面ベンチ正しいフルスキャン パスにフォールバックします
10 個の巨大なビットマップHeap/GPU アップロードのピークAsset バイトとアップロード トレースサムネイル/LOD とデコード バジェット大型画像治具元の画像キャッシュを削除する
大量のテキストレイアウト/ペイントが高いテキストステージマークメトリックキャッシュとスクリーンスペース LOD多言語フォントシーンFallbackラベル
非常に長いフリーハンドパスの構築が高いポイント/パス期間チャンク化、簡略化、スケール LOD100kポイント Shape簡略化されたPreviewを描画します
連続ズームCache のスラッシュヒット率/スケールバケット個別スケールのバケットと制限ズームスイープ一時的なバケットをクリアする
Cache は無効化に失敗しますShape は旧色を示しますリビジョン/キーの不一致変更セットからの正確な無効化スタイル更新のスクリーンショットキャッシュを無効にして再描画する
Cache は無制限に成長しますGC が 10 分後にヒッチするキャッシュバイト/保持LRUバイトバジェット浸漬試験Document ではなくキャッシュをクリアします
高DPR大型ディスプレイ背景ピクセルが爆発する幅×高さ×DPR²有効 DPR/ピクセル キャップ4K DPR3シーン解像度を下げて通知する
頻繁なgetImageDataCPU/GPU 同期待機Readbackスパンホットパスから削除します。幾何学的ピッキングを使用するReadback オン/オフ A/BCPU Geometry を使用する
Move ごとに配列を割り当てる周期的 GC スパイクAllocation フレームチャートバッファ/配列を再利用する60秒のドラッグソークデータを失わずに次のフレームを続行

根拠を示して合格する

自動化された証拠手動による証拠合格条件
固定シード 4 戦略ベンチマーク、インデックス/フル等価性、キャッシュ バジェット、60 秒ソーク代表的なデバイスの DevTools トレースとドラッグ フィールのレビュー自動化された予算は通過します。トレースの主要なステージは JSON レポートと一致します
DPR/viewport/large-image/long-Path シーン マトリックスローエンドの統合グラフィックスと高 DPR 大型ディスプレイで 1 回ずつ実行します隠れた劣化エラーはありません。 p95 とピークメモリは説明可能です
必須の質問受け入れられる証拠
p95 ドラッグが遅いのはどこですか?Input / Model / Query / Render / Present の各段階を示す Trace
最適化は本当に役に立ちますか?同じシード、デバイス、ビューポートを使用した A/B ディストリビューション
正しさは損なわれましたか?インデックス/完全等価性と視覚的および相互作用回帰
Cache は健康ですか?ヒット率、バイト数、エビクション、リビジョンの無効化
記憶は増え続けるのでしょうか?定常状態ソーク、Retained Objects、および予算
高い DPR はどのように処理されますか?裏打ち画素数と劣化閾値
  • 完全なデータ、無効化されたデータ、カリングされたデータ、およびインデックス付き + キャッシュ + LOD のデータは、同じシーンで比較できます。
  • レポートには、「オブジェクト数」だけでなく、Document サイズと表示形状が含まれます。
  • フレーム、ヒット、ドラッグには p50/p95/p99 が含まれます。 Heap と Export ピークは別々に記録されます。
  • すべての最適化には、正確性同等性テストと無効化スイッチがあります。
  • Cache には、キー、無効化、バイト バジェット、エビクション、およびヒット メトリックがあります。
  • 結論は、トレース ステージと番号を特定します。 「Canvasの描画が遅い」は禁止です。

5歳児に説明する

「p95」、「Culling」、「Spatial Index」、「Cache」、または「LOD」とは言わないでください。なぜ教師が世界中の子供たちを検査すべきではないのか説明してください。 2,000 枚のカードだけでもまだ遅いのはなぜですか?コピーされたカードは役立つのに、キャビネットから溢れてしまうのはなぜでしょうか?

専門用語を使わない適切な回答

教師はまずストップウォッチを使い、遅いのが「教室を見つける」「名前を確認する」「大きな写真を描く」のどこかを調べます。人を見つけるときは、まず窓から見える学校や教室を調べ、少数の候補だけを丁寧に確認します。遠くでは単純なブロックだけを描き、近づいたときだけ名前を書きます。よく使うカードはコピーできますが、子どもが着替えたら古いコピーを破棄し、キャビネットがいっぱいになったら、最も長く使われていないコピーから先に破棄します。大きな写真、影、アニメーションを含む2,000枚のカードは、画面外にある10万個の小さな点より多くの処理を必要とするかもしれません。だから、何かを変える前に測定します。