pixijs-performance

Par pixijs · pixijs-skills

Utilisez cette skill lors du profilage ou de l'optimisation d'une application PixiJS v8 pour les FPS, les draw calls ou la mémoire GPU. Couvre les patterns de destruction (`cacheAsTexture(false)`, `releaseGlobalResources`), `GCSystem` et `TextureGCSystem`, `PrepareSystem`, l'object pooling, les règles de batching, `BitmapText` pour le texte dynamique, le culling (`Culler`, `CullerPlugin`, `cullable`, `cullArea`), et les compromis résolution/antialias. Se déclenche sur : FPS, jank, draw calls, batching, object pool, `GCSystem`, `PrepareSystem`, `Culler`, `cacheAsTexture`, fuite mémoire, patterns de destruction.

npx skills add https://github.com/pixijs/pixijs-skills --skill pixijs-performance

Profile avant optimisation. PixiJS gère beaucoup de contenu correctement en standard ; les DevTools Performance du navigateur + le profiling GPU doivent être votre premier geste. Une fois le goulot identifié, appliquez le motif ciblé ci-dessous (destroy, pool, batch, cache, ou cull).

Quick Start

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

Related skills: pixijs-scene-container (options de destroy), pixijs-scene-core-concepts (render groups, layers, culling), pixijs-scene-text (BitmapText pour contenu dynamique), pixijs-assets (atlasing), pixijs-custom-rendering (custom batchers).

Core Patterns

Proper destroy avec nettoyage

import { Sprite, Assets } from "pixi.js";

const texture = await Assets.load("character.png");
const sprite = new Sprite(texture);

// Destroy sprite uniquement (préserver texture pour réutilisation)
sprite.destroy();

// Destroy sprite ET sa texture
sprite.destroy({ children: true, texture: true, textureSource: true });

Quand vous avez terminé avec un asset chargé entièrement :

Assets.unload("character.png");

Cela le retire du cache et décharge la ressource GPU.

Cycle Application destroy/recreate

import { Application } from "pixi.js";

// Destroy correct qui nettoie les pools globaux
app.destroy({ releaseGlobalResources: true });

const newApp = new Application();
await newApp.init({ width: 800, height: 600 });

Sans releaseGlobalResources: true, les objets poolés (batches, textures) de l'ancienne app s'échappent dans la nouvelle, causant du scintillement et de la corruption.

Garbage collection de textures

PixiJS collecte automatiquement les textures inutilisées et les ressources GPU via GCSystem. Défauts : vérification toutes les 30 secondes, suppression des ressources inactives pendant 60 secondes. Ceux-ci sont basés sur le temps (millisecondes).

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

Pour un contrôle manuel :

texture.source.unload(); // immediate GPU memory release

PrepareSystem pour upload GPU

Téléchargez les textures et graphics sur GPU avant rendu pour éviter les saccades à la première frame :

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() accepte un Container (upload toutes les textures, texte, et graphics dans le sous-arbre) ou des ressources individuelles.

cacheAsTexture pour la performance

cacheAsTexture() rend le sous-arbre d'un container en une seule texture, réduisant les appels de dessin pour le contenu statique complexe. Internellement, cela crée un groupe de rendu et cache le résultat.

Quand l'utiliser :

  • Beaucoup d'enfants statiques (panneaux UI, fonds décoratifs, Graphics complexes)
  • Containers avec des filtres coûteux (cache le résultat du filtre)
  • Grands sous-arbres qui changent rarement

Tradeoffs :

  • Utilise la mémoire GPU pour la texture en cache (plus gros containers = plus de mémoire)
  • La taille max de texture dépend du GPU (typiquement 4096x4096 ; vérifiez renderer.texture.maxTextureSize)
  • Doit appeler updateCacheTexture() après modification des enfants
  • Combiner avec des masks est fragile (voir la skill masking)
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();

Évitez : toggler on/off à répétition (re-caching constant annule les bénéfices), cacher des containers épars (gain négligeable), cacher des containers plus gros que 4096x4096.

Object recycling

Réutilisez les objets en changeant leurs propriétés au lieu de destroy/recreate :

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

Détruire et recréer est significativement plus coûteux que toggler visible et mettre à jour les propriétés. Les ressources GPU restent allouées ; seule la visibilité du scene graph change.

Batching optimization

PixiJS regroupe les objets consécutifs similaires en appels de dessin uniques. Les batch breaks se produisent sur :

  • Changement de type d'objet (Sprite vs Graphics)
  • Changement de source de texture (au-delà de la limite par batch, typiquement 16)
  • Changement de blend mode
  • Changement de topologie

Optimisez l'ordre de dessin :

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

Le même principe s'applique aux blend modes : screen/normal/screen/normal = 4 draws ; screen/screen/normal/normal = 2 draws.

Spritesheets plutôt que textures individuelles

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"]);

Les textures individuelles requièrent chacune leur propre upload GPU et cassent les batches quand la limite de texture par batch est dépassée. Les spritesheets consolident plusieurs frames en une texture atlas.

Utilisez le suffixe @0.5x sur les feuilles demi-résolution pour que PixiJS les redimensionne automatiquement.

Text performance

Text et HTMLText réaffichent vers un canvas et se re-uploadent sur le GPU à chaque changement. Ne les mettez jamais à jour par frame sans condition :

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 rend depuis un atlas de glyphes pré-généré. Les mises à jour repositionnent uniquement les quads ; pas de réaffichage canvas ni d'upload GPU. Utilisez-le pour les scores, timers, compteurs, et tout ce qui change fréquemment.

Si vous devez utiliser canvas Text, gardez les mises à jour pour qu'elles ne se produisent que quand la valeur change :

app.ticker.add(() => {
  const next = `Score: ${score}`;
  if (scoreText.text !== next) {
    scoreText.text = next;
  }
});

La résolution Text correspond à la résolution du renderer par défaut. Réduisez-la indépendamment via text.resolution = 1 pour diminuer la mémoire GPU sur les écrans haute DPI.

Graphics performance

Les objets Graphics sont les plus rapides quand leur forme ne change pas (les transforms, alpha, et tint vont bien). Les petits Graphics (moins de ~100 points) sont groupés comme les Sprites. Les Graphics complexes avec des centaines de formes sont lents ; convertissez-les en textures à la place :

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

Culling

PixiJS saute le rendu des objets en dehors de la zone visible quand cullable est défini. Désactivé par défaut car il échange un coût CPU (vérification de bounds) pour des économies GPU. Le culling ne s'exécute que quand CullerPlugin est enregistré :

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 sur un container arrête le culler de recurser dans ses descendants ; une large victoire pour les panneaux UI statiques avec beaucoup d'enfants. Culler.shared.cull(container, rect) exécute la même logique manuellement pour les pipelines de rendu custom. Utilisez le culling quand vous êtes GPU-bound ; évitez-le quand CPU-bound, puisque la vérification de bounds par objet ajoute de l'overhead.

Resolution et antialias tradeoffs

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 multiplie par quatre le nombre de pixels. Sur mobile, cela peut diviser par deux le frame rate. Profile pour trouver le bon équilibre.

Stagger bulk texture destruction

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

Détruire beaucoup de textures en une frame cause un freeze. Étalez le coût sur plusieurs frames.

Filters et masks cost

  • Définissez container.filterArea = new Rectangle(x, y, w, h) quand vous connaissez les bounds. Sans cela, PixiJS mesure les bounds chaque frame.
  • Libérez la mémoire filtre : container.filters = null.
  • Coût mask (moins cher au plus cher) : Rectangle masks axis-aligned (scissor rect) < Graphics masks (stencil buffer) < Sprite/alpha masks (filter pipeline). Des centaines de masks ralentiront les choses peu importe le type ; préférez les rectangle masks quand les bounds sont axis-aligned.
  • Définissez interactiveChildren = false sur les containers sans enfants interactifs.
  • Définissez hitArea sur les gros containers pour éviter la hit test récursive des enfants.

Safe destroy order

Retirez de la scène avant de détruire :

parent.removeChild(sprite);
sprite.destroy();

Détruire tandis que le pipeline de rendu tient encore une référence cause des crashes null-pointer. Si la destruction doit arriver mid-frame, déférez-la :

app.ticker.addOnce(() => {
  parent.removeChild(sprite);
  sprite.destroy();
});

Common Mistakes

[CRITICAL] App destroy without releaseGlobalResources

Wrong:

app.destroy();
const newApp = new Application();

Correct:

app.destroy({ releaseGlobalResources: true });
const newApp = new Application();

Sans ce flag, les stale batches poolés et textures de l'ancienne app persistent dans les pools globaux et sont réutilisés par la nouvelle app, causant du scintillement et de la corruption visuelle.

[HIGH] Interleaving object types in scene graph

sprite / graphic / sprite / graphic = 4 draw calls. sprite / sprite / graphic / graphic = 2 draw calls.

Groupez les mêmes types d'objets ensemble dans l'ordre des enfants pour minimiser les batch breaks. Le même s'applique à l'ordre des blend modes.

[HIGH] Destroying and recreating objects instead of recycling

Destroy/recreate est coûteux : cela désalloue les ressources GPU, déclenche la garbage collection, et requiert des uploads GPU frais. Réutilisez les objets en mettant à jour texture, position, visible, et autres propriétés. Utilisez un pattern object pool pour les entités fréquemment spawned/despawned.

[HIGH] Loading many individual textures instead of spritesheets

Chaque texture séparée consomme son propre slot mémoire GPU et casse le batching quand la limite de texture par batch est atteinte. Les spritesheets consolident les textures en atlases. Évitez aussi les textures dépassant 4096px sur un axe, car elles échouent sur certains GPUs mobiles.

[HIGH] Updating Text or HTMLText every frame

Chaque mise à jour réaffiche la chaîne complète vers un canvas et upload sur le GPU. À 60fps cela crée un énorme overhead. Utilisez BitmapText pour le contenu dynamique (scores, timers, compteurs). Si canvas Text est requis, mettez à jour uniquement quand la valeur change réellement. Source: src/docs/concepts/performance-tips.md

[HIGH] Using complex Graphics instead of textures

Des centaines d'objets Graphics complexes sont lents à rendre. Les petits Graphics (moins de ~100 points) se groupent efficacement comme les Sprites, mais les complexes non. Rendez les formes statiques complexes en texture avec renderer.generateTexture() et affichez comme un Sprite. Source: src/docs/concepts/performance-tips.md

[MEDIUM] Not staggering bulk texture destruction

Détruire des dizaines de textures en une seule frame cause un freeze visible. Étalez la destruction sur plusieurs frames (ex., 5 par frame via un callback ticker). Source: src/docs/concepts/garbage-collection.md

[MEDIUM] Not using PrepareSystem for large scenes

Sans renderer.prepare.upload(), les textures uploadent sur le GPU au premier rendu, causant des saccades de frame. Pour les loading screens ou les transitions de scène, uploadez avant d'afficher. Requiert import 'pixi.js/prepare' (non inclus même dans le bundle par défaut ; importez-le toujours explicitement). Source: src/prepare/PrepareSystem.ts

[MEDIUM] Using high resolution or antialias without profiling

resolution: 2 multiplie par quatre le nombre de pixels. antialias: true ajoute un coût GPU. Les deux dégradent la performance sur les appareils mobiles. Profilez toujours sur le hardware cible avant d'activer. Source: performance-tips.md

API Reference

Skills similaires