pixijs-performance について
pixijs-performanceは、PixiJSの性能改善に取り組む開発者向けのスキルです。まずブラウザーとGPUのプロファイリングでボトルネックを特定し、キャッシュ、オブジェクト再利用、バッチ処理、カリングなどを選ぶ方法を示します。
主な機能
- 01最適化前のプロファイリングを推奨
- 02テクスチャキャッシュで描画呼び出しを削減
- 03オブジェクト再利用とバッチ処理の改善
- 04テクスチャの解放とGPUアップロードを管理
- 05状況に応じてカリングを適用
活用シーン
- 01複雑な静的UIの再描画を削減
- 02頻繁に更新されるテキストやオブジェクトを最適化
- 03テクスチャ解放やアプリ再作成に対応
- 04GPU負荷時に画面外オブジェクトを描画対象から除外
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pixijs/ pixijs-skills --skill pixijs-performanceインストール後、Claude Code で/pixijs-performanceを入力して読み込まれたか確認します。
詳細
最適化の前にプロファイリングしましょう。PixiJSは多くのコンテンツを標準で適切に処理できます。まずはブラウザーのDevTools PerformanceとGPUプロファイリングを利用してください。ボトルネックを特定したら、以下のパターン(破棄、プール、バッチ化、キャッシュ、カリング)を適用します。
クイックスタート
container.cacheAsTexture(true);
container.updateCacheTexture();
container.cacheAsTexture(false);
container.destroy({ children: true });
import { CullerPlugin, extensions } from "pixi.js";
extensions.add(CullerPlugin);
offscreenContainer.cullable = true;
offscreenContainer.cullArea = new Rectangle(0, 0, 256, 256);
// Tune GC via init options (ms). The `textureGC.*` properties are
// deprecated since 8.15.0 — use these on the Application init instead.
await app.init({ gcMaxUnusedTime: 60_000, gcFrequency: 30_000 });
関連スキル: pixijs-scene-container(破棄オプション)、pixijs-scene-core-concepts(レンダーグループ、レイヤー、カリング)、pixijs-scene-text(動的コンテンツ向けのBitmapText)、pixijs-assets(アトラス化)、pixijs-custom-rendering(カスタムバッチャー)。
コアパターン
適切なクリーンアップを伴う破棄
import { Sprite, Assets } from "pixi.js";
const texture = await Assets.load("character.png");
const sprite = new Sprite(texture);
// Destroy sprite only (preserve texture for reuse)
sprite.destroy();
// Destroy sprite AND its texture
sprite.destroy({ children: true, texture: true, textureSource: true });
読み込んだアセットを完全に使い終えたら:
Assets.unload("character.png");
これによりキャッシュから削除され、GPUリソースがアンロードされます。
カスタムレンダリング用に作成したオブジェクトも、同じ方法でクリーンアップします。作成したgeometryとrenderTargetに対してdestroy()を呼び出してください。コンテナーを破棄すると、そのレンダーグループ用にキャッシュされたバッチャーも破棄されます。
アプリケーションの破棄と再作成のサイクル
import { Application } from "pixi.js";
// Correct destroy that cleans global pools
app.destroy({ releaseGlobalResources: true });
const newApp = new Application();
await newApp.init({ width: 800, height: 600 });
releaseGlobalResources: trueを指定しないと、古いアプリのプール済みオブジェクト(バッチ、テクスチャ)が新しいアプリに残り、ちらつきや破損の原因になります。
テクスチャのガベージコレクション
PixiJSはGCSystemを介して、未使用のテクスチャとGPUリソース(WebGPUのバインドグループを含む)を自動収集します。デフォルトでは30秒ごとに確認し、60秒間使われていないリソースを削除します。これらは時間(ミリ秒)で指定します。
import { Application } from "pixi.js";
const app = new Application();
await app.init({
gcActive: true,
gcMaxUnusedTime: 120000, // idle time before cleanup in ms (default: 60000)
gcFrequency: 60000, // check interval in ms (default: 30000)
});
手動で制御するには:
texture.source.unload(); // immediate GPU memory release
GPUアップロード用のPrepareSystem
初回フレームの引っかかりを避けるため、レンダリング前にテクスチャとグラフィックスをGPUにアップロードします。
import "pixi.js/prepare";
import { Application, Assets } from "pixi.js";
const app = new Application();
await app.init();
// Don't render until assets are uploaded
app.stop();
const texture = await Assets.load("large-scene.png");
// Upload to GPU ahead of time
await app.renderer.prepare.upload(app.stage);
// Now rendering won't hitch on first frame
app.start();
prepare.upload()は、Container(サブツリー内のすべてのテクスチャ、テキスト、グラフィックスをアップロード)または個々のリソースを受け取ります。
パフォーマンス向上のためのcacheAsTexture
cacheAsTexture()はコンテナーのサブツリーを単一のテクスチャにレンダリングし、複雑な静的コンテンツのドローコールを減らします。内部ではレンダーグループを作成し、結果をキャッシュします。
使用するケース:
- 静的な子要素が多数ある場合(UIパネル、装飾的な背景、複雑なGraphics)
- コストの高いフィルターを使うコンテナー(フィルター適用後の結果をキャッシュ)
- 変更頻度の低い大規模なサブツリー
トレードオフ:
- キャッシュされたテクスチャにGPUメモリーを使用します(コンテナーが大きいほどメモリー使用量が増えます)
- 最大テクスチャサイズはGPUに依存します(通常は4096x4096以上)。PixiJSにはこれを公開するプロパティがないため、バックエンドに直接問い合わせます:
- 子要素を変更した後は
updateCacheTexture()を呼び出す必要があります - マスクとの併用は不安定です(マスキングのスキルを参照)
import type { WebGLRenderer, WebGPURenderer } from "pixi.js";
// Available after renderer/app init on both backends
const maxTextureSize = renderer.name === "webgpu"
? (renderer as WebGPURenderer).gpu.device.limits.maxTextureDimension2D
: (renderer as WebGLRenderer).gl.getParameter(WebGL2RenderingContext.MAX_TEXTURE_SIZE);
import { Container, Sprite } from "pixi.js";
const panel = new Container();
// ... add many static children ...
panel.cacheAsTexture(true);
// With options
panel.cacheAsTexture({ resolution: 2, antialias: true });
// Refresh after changes
panel.updateCacheTexture();
// MUST disable before destroying (see Common Mistakes below)
panel.cacheAsTexture(false);
panel.destroy();
避けること: 有効と無効を繰り返し切り替える(頻繁な再キャッシュで効果が相殺されます)、スパースなコンテナーをキャッシュする(効果がほとんどありません)、4096x4096を超えるコンテナーをキャッシュする。
オブジェクトの再利用
オブジェクトを破棄して再作成する代わりに、プロパティを変更して再利用します。
import { Sprite, Container, Texture } from "pixi.js";
class BulletPool {
private _pool: Sprite[] = [];
private _container: Container;
constructor(container: Container) {
this._container = container;
}
public get(texture: Texture): Sprite {
let bullet = this._pool.pop();
if (!bullet) {
bullet = new Sprite(texture);
this._container.addChild(bullet);
}
bullet.texture = texture;
bullet.position.set(0, 0);
bullet.rotation = 0;
bullet.scale.set(1);
bullet.alpha = 1;
bullet.tint = 0xffffff;
bullet.blendMode = "normal";
bullet.visible = true;
return bullet;
}
public release(bullet: Sprite): void {
bullet.visible = false;
this._pool.push(bullet);
}
}
破棄して再作成するよりも、visibleを切り替えてプロパティを更新するほうが大幅に低コストです。GPUリソースは割り当てられたままで、シーンのグラフ上の表示状態だけが変わります。
バッチ化の最適化
PixiJSは、連続する類似オブジェクトを単一のドローコールにバッチ化します。バッチが分割されるのは次の場合です。
- オブジェクトの種類が変わる(SpriteからGraphicsなど)
- テクスチャソースが変わる(バッチごとのテクスチャ上限を超える場合。通常は16)
- ブレンドモードが変わる
- トポロジーが変わる
描画順序を最適化します。
import { Sprite, Graphics, Container } from "pixi.js";
// 4 draw calls: type alternates
const bad = new Container();
bad.addChild(new Sprite(t1));
bad.addChild(new Graphics().rect(0, 0, 10, 10).fill(0xff0000));
bad.addChild(new Sprite(t2));
bad.addChild(new Graphics().rect(0, 0, 10, 10).fill(0x00ff00));
// 2 draw calls: types grouped
const good = new Container();
good.addChild(new Sprite(t1));
good.addChild(new Sprite(t2));
good.addChild(new Graphics().rect(0, 0, 10, 10).fill(0xff0000));
good.addChild(new Graphics().rect(0, 0, 10, 10).fill(0x00ff00));
ブレンドモードにも同じ原則が適用されます。screen/normal/screen/normalは4回の描画、screen/screen/normal/normalは2回の描画になります。
個別のテクスチャよりスプライトシート
import { Assets, Sprite } from "pixi.js";
// Load a spritesheet (single texture atlas)
const sheet = await Assets.load("game-atlas.json");
// All frames share one GPU texture; enables batching
const hero = new Sprite(sheet.textures["hero.png"]);
const enemy = new Sprite(sheet.textures["enemy.png"]);
const coin = new Sprite(sheet.textures["coin.png"]);
個別のテクスチャはそれぞれGPUへのアップロードが必要で、テクスチャ上限を超えるとバッチが分割されます。スプライトシートを使うと、多数のフレームを1つのアトラステクスチャにまとめられます。
半解像度のシートには、ファイル名の末尾に@0.5xを付けると、PixiJSが自動的にスケールします。
テキストのパフォーマンス
Text と HTMLText は変更のたびにキャンバスへ再描画し、GPU に再アップロードします。毎フレーム無条件に更新しないでください。
import { BitmapText, Text } from "pixi.js";
// Wrong: re-renders canvas + GPU upload every frame
app.ticker.add(() => {
scoreText.text = `Score: ${score}`;
});
// Correct: use BitmapText for frequently changing content
const scoreText = new BitmapText({
text: "Score: 0",
style: { fontFamily: "Arial", fontSize: 24, fill: 0xffffff },
});
app.ticker.add(() => {
scoreText.text = `Score: ${score}`;
});
BitmapText は事前生成されたグリフアトラスから描画します。更新時に行うのはクワッドの再配置だけで、キャンバスへの再描画や GPU へのアップロードは発生しません。スコア、タイマー、カウンターなど、頻繁に変化するものに使用してください。
キャンバス Text を使用する必要がある場合は、値が変化したときだけ更新されるようにしてください。
app.ticker.add(() => {
const next = `Score: ${score}`;
if (scoreText.text !== next) {
scoreText.text = next;
}
});
Text の解像度は、デフォルトでレンダラーの解像度に一致します。高 DPI ディスプレイで GPU メモリを削減するには、text.resolution = 1 として個別に下げてください。
Graphics のパフォーマンス
Graphics オブジェクトは、形状が変化しない場合に最も高速です(トランスフォーム、アルファ、ティントの変更は問題ありません)。小さな Graphics(約 100 ポイント未満)は Sprite と同様にバッチ処理されます。数百の図形を含む複雑な Graphics は低速なため、代わりにテクスチャに変換してください。
import { Graphics, Sprite } from "pixi.js";
const complex = new Graphics();
// ... draw complex shape ...
// Render once to texture, use as Sprite
const texture = app.renderer.generateTexture(complex);
const sprite = new Sprite(texture);
カリング
cullable が設定されている場合、PixiJS は表示領域外のオブジェクトの描画をスキップします。カリングは、CPU コスト(境界チェック)と引き換えに GPU の負荷を軽減するため、デフォルトでは無効です。カリングは CullerPlugin が登録されている場合にのみ実行されます。
import { extensions, CullerPlugin, Culler, Rectangle } from "pixi.js";
extensions.add(CullerPlugin); // before Application.init
// Enable on objects that may be off-screen
sprite.cullable = true;
// Optional: a pre-computed cull rectangle avoids per-frame bounds calculation.
// Without cullArea, the Culler uses the object's global bounds instead.
sprite.cullArea = new Rectangle(0, 0, 800, 600);
// Skip culling an entire subtree (static UI, always visible)
uiRoot.cullableChildren = false;
// Or cull manually without the plugin:
Culler.shared.cull(app.stage, app.renderer.screen);
コンテナの cullableChildren を設定すると、カラ―は子孫への再帰処理を停止します。これは、多数の子要素を持つ静的な UI パネルで大きな効果があります。Culler.shared.cull(container, rect) を使うと、カスタムレンダリングパイプラインで同じ処理を手動で実行できます。GPU がボトルネックの場合はカリングを使用してください。CPU がボトルネックの場合は、オブジェクトごとの境界チェックによるオーバーヘッドが加わるため避けてください。
解像度とアンチエイリアスのトレードオフ
import { Application } from "pixi.js";
const app = new Application();
// Mobile-friendly: lower resolution, no antialias
await app.init({
resolution: 1,
antialias: false,
backgroundAlpha: 1, // opaque background is faster
});
resolution: 2 にするとピクセル数は 4 倍になります。モバイルでは、フレームレートが半分になる場合があります。プロファイリングを行い、適切なバランスを見つけてください。
低レベルのレンダリング最適化
import { RenderTexture } from "pixi.js";
// single-pass antialiased render texture: also discard its MSAA depth/stencil at end of pass
const rt = RenderTexture.create({ width: 1024, height: 1024, antialias: true, transient: true });
// upload only the changed byte range of a large buffer (also works on WebGL)
buffer.update(changedBytes, offsetBytes);
// upload only a texel range of a data texture (end is exclusive)
bufferImageSource.update(100, 116);
タイルベースの WebGPU GPU(renderer.device.extensions.tileBased:すべてのスマートフォン GPU と Apple シリコン)では、アンチエイリアス処理された各ターゲットのマルチサンプルカラー バッファーは、書き戻されずにすでに破棄されています。また、ターゲットを再度開くパスでは、解決済みの画像から復元されます。renderer.device.extensions.transientAttachment が true の場合、バッファー自体が割り当てられないこともあります。transient: true を指定すると、マルチサンプルの深度/ステンシルバッファーと、その他の GPU でのカラーバッファーも破棄されます。単一パスで描画され、clear: false で再度処理されたり、フィルターやマスクでラップされたりしないテクスチャにのみ使用してください。WebGPU で静的なカスタム描画シーケンスを使用する場合は、レンダーバンドルを一度記録して毎フレーム再生してください。pixijs-custom-rendering スキルを参照してください。
大量のテクスチャの破棄を分散する
function staggerDestroy(textures: Texture[], perFrame: number = 5): void {
let index = 0;
const ticker = app.ticker;
const destroy = () => {
const end = Math.min(index + perFrame, textures.length);
for (let i = index; i < end; i++) {
textures[i].destroy(true);
}
index = end;
if (index >= textures.length) {
ticker.remove(destroy);
}
};
ticker.add(destroy);
}
1 フレーム内に多数のテクスチャを破棄すると、フリーズが発生します。フレームをまたいで負荷を分散してください。
フィルターとマスクのコスト
- 境界がわかっている場合は、
container.filterArea = new Rectangle(x, y, w, h)を設定してください。設定しない場合、PixiJS は毎フレーム境界を計測します。 - フィルターメモリを解放します:
container.filters = null。 - フルスクリーンフィルターテクスチャは画面サイズでプールされますが、パディング(
BlurFilterのデフォルト値)により要求サイズが画面を超え、次の 2 の累乗サイズになります。フルスクリーンのぼかしで画面サイズを維持するには、repeatEdgePixels: trueを設定してください。 - マスクのコスト(低い順):軸に沿った Rectangle マスク(シザー矩形) < Graphics マスク(ステンシルバッファー) < Sprite/アルファマスク(フィルターパイプライン)。種類にかかわらず、マスクを何百個も使うと処理が遅くなります。境界が軸に沿っている場合は、Rectangle マスクを優先してください。
- インタラクティブな子要素がないコンテナでは、
interactiveChildren = falseを設定してください。 - 大きなコンテナに
hitAreaを設定すると、子要素のヒットテストの再帰処理を省略できます。
安全な破棄の順序
破棄する前にシーンから削除してください。
parent.removeChild(sprite);
sprite.destroy();
レンダリングパイプラインがまだ参照を保持している間に破棄すると、null ポインターのクラッシュが発生します。フレームの途中で破棄する必要がある場合は、処理を遅らせてください。
app.ticker.addOnce(() => {
parent.removeChild(sprite);
sprite.destroy();
});
よくある間違い
[CRITICAL] releaseGlobalResources を指定せずに App を破棄する
誤り:
app.destroy();
const newApp = new Application();
正しい例:
app.destroy({ releaseGlobalResources: true });
const newApp = new Application();
このフラグがないと、古いアプリの使用済みバッチやテクスチャがグローバルプールに残り、新しいアプリで再利用されるため、ちらつきや表示の乱れが発生します。
[HIGH] シーングラフ内でオブジェクトタイプを交互に配置する
sprite / graphic / sprite / graphic = 4 回の描画呼び出し。
sprite / sprite / graphic / graphic = 2 回の描画呼び出し。
バッチの分割を最小限に抑えるため、子の順序で同じオブジェクトタイプをまとめてください。ブレンドモードの順序についても同様です。
[HIGH] オブジェクトを再利用せず、破棄して再作成する
破棄と再作成にはコストがかかります。GPU リソースが解放され、ガベージコレクションが発生し、新たに GPU へアップロードする必要があります。texture、position、visible などのプロパティを更新してオブジェクトを再利用してください。頻繁に生成・破棄されるエンティティには、オブジェクトプールパターンを使用してください。
[HIGH] 個別のテクスチャを大量に読み込む
個別のテクスチャはそれぞれ独自の GPU メモリスロットを消費し、バッチごとのテクスチャ上限に達するとバッチ処理が分割されます。スプライトシートを使うと、テクスチャをアトラスにまとめられます。また、一部のモバイル GPU では正常に動作しないため、いずれかの軸が 4096px を超えるテクスチャも避けてください。
[HIGH] 毎フレーム Text または HTMLText を更新する
更新のたびに文字列全体がキャンバスに再描画され、GPU にアップロードされます。60fps では大きなオーバーヘッドが発生します。動的なコンテンツ(スコア、タイマー、カウンター)には BitmapText を使用してください。キャンバスの Text が必要な場合は、値が実際に変わったときだけ更新してください。出典:src/docs/concepts/performance-tips.md
[HIGH] テクスチャの代わりに複雑な Graphics を使用する
複雑な Graphics オブジェクトを数百個描画すると、処理が遅くなります。小さな Graphics(約 100 点未満)は Sprite のように効率よくバッチ処理されますが、複雑なものはそうではありません。複雑な静的図形は renderer.generateTexture() でテクスチャにレンダリングし、Sprite として表示してください。出典:src/docs/concepts/performance-tips.md
[MEDIUM] 大量のテクスチャ破棄を分散しない
1 フレーム内に数十個のテクスチャを破棄すると、目に見えるフリーズが発生します。破棄処理を複数フレームに分散してください(例:ティッカーのコールバックで 1 フレームにつき 5 個)。出典:src/docs/concepts/garbage-collection.md
[MEDIUM] 大規模なシーンで PrepareSystem を使用しない
renderer.prepare.upload() を使用しない場合、最初のレンダリング時にテクスチャが GPU にアップロードされるため、フレームのカクつきが発生します。ロード画面やシーン遷移では、表示する前にアップロードしてください。import 'pixi.js/prepare' が必要です(デフォルトバンドルにも含まれていないため、必ず明示的にインポートしてください)。出典:src/prepare/PrepareSystem.ts
[MEDIUM] プロファイリングせずに高解像度やアンチエイリアスを使用する
resolution: 2 にするとピクセル数が 4 倍になります。antialias: true は GPU 負荷を増加させます。どちらもモバイルデバイスのパフォーマンスを低下させます。有効にする前に、必ず対象ハードウェアでプロファイリングしてください。出典:performance-tips.md