pixi-performance
Sprite batching, texture atlases, RenderTexture caching, off-screen culling, object pooling, ParticleContainer vs Container, and failure modes that break GPU batching in PixiJS v8+
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Sprite batching, texture atlases, RenderTexture caching, off-screen culling, object pooling, ParticleContainer vs Container, and failure modes that break GPU batching in PixiJS v8+
Agent definition
pixi-performance.mddescription: Sprite batching, texture atlases, RenderTexture caching, off-screen culling, object pooling, ParticleContainer vs Container, and failure modes that break GPU batching in PixiJS v8+
agent: pixijs-combat-renderer
category: performance
version_range: "PixiJS v8+"
PixiJS Performance Reference
> **Scope**: What breaks batching, draw call measurement, ParticleContainer, texture atlases, RenderTexture caching, object pooling.
---
Batching — What Breaks It
PixiJS v8 batches sprites sharing same texture, blend mode, alpha, and no active filters into one draw call.
| Cause | Prevention | |-------|------------| | Different texture | Pack into one atlas | | Filter on ancestor | Filters on container root only | | Different blend mode | Group by blend mode | | Per-sprite tint per frame | One tint per atlas batch | | Alpha on parent Container | Set alpha on sprites, not containers | | Mask on ancestor | Pool masked containers |
---
Texture Atlases
await Assets.load('/atlases/combat.json');
const playerIdleTexture = Texture.from('player_idle_000');
const frames = [];
for (let i = 0; i < 8; i++) {
frames.push(Texture.from(`player_idle_${String(i).padStart(3, '0')}`));
}
const animSprite = new AnimatedSprite(frames);
animSprite.animationSpeed = 0.15;
animSprite.play();TexturePacker Settings
Format: Pixi JS (JSON Hash or JSON Array)
Algorithm: MaxRects
Power of Two: YES
Max texture: 2048x2048
Padding: 2
Trim sprites: YES
Extrude: 1
---
RenderTexture for Cached Composites
Bake expensive composites that don't change every frame:
function bakeCharacterToTexture(app: Application, character: Container, width: number, height: number): Sprite {
const renderTexture = RenderTexture.create({ width, height });
app.renderer.render({ container: character, target: renderTexture });
return new Sprite(renderTexture);
}
// Re-bake on state change, not every frame---
Off-Screen Culling
PixiJS does not cull by default.
const screenBounds = new Rectangle(0, 0, app.screen.width, app.screen.height);
function cullChildren(container: Container): void {
for (const child of container.children) {
if (child instanceof Sprite) {
const bounds = child.getBounds();
child.visible = screenBounds.intersects(new Rectangle(
bounds.x - 50, bounds.y - 50, bounds.width + 100, bounds.height + 100,
));
}
}
}
// Every 3 frames is sufficient
let cullFrame = 0;
app.ticker.add(() => { if (++cullFrame % 3 === 0) cullChildren(entityLayer); });---
Object Pooling
class SpritePool {
private pool: Sprite[] = [];
constructor(texture: Texture, parent: Container, initialSize = 20) {
this.texture = texture;
this.parent = parent;
for (let i = 0; i < initialSize; i++) {
const sprite = new Sprite(texture);
sprite.visible = false;
parent.addChild(sprite);
this.pool.push(sprite);
}
}
get(): Sprite | null {
const sprite = this.pool.find(s => !s.visible);
if (!sprite) return null;
sprite.visible = true; sprite.alpha = 1; sprite.rotation = 0; sprite.scale.set(1);
return sprite;
}
release(sprite: Sprite): void {
sprite.visible = false;
sprite.position.set(-9999, -9999);
}
}---
ParticleContainer vs Container
| Feature | ParticleContainer | Container | |---------|------------------|-----------| | Max sprites | 100,000+ | ~5,000 | | Filters | No | Yes | | Children of children | No | Yes | | Blend mode per sprite | No | Yes |
const particleContainer = new ParticleContainer(10000, {
position: true, rotation: true, scale: true, alpha: true,
uvs: false, // disable if single texture
tint: false, // disable if no tint variation
});**When NOT to use**: Mixed textures, particles needing filters, particles with children.
---
Pattern Catalog
Group Tint Changes by Value
Per-frame `.tint` flushes batch per sprite. 100 sprites = 100 draw calls. **Fix**: Use custom shader uniform or group sprites by tint value.
Use ParticleContainer for 500+ Uniform Particles
Regular Container: each child inspected in JS every frame (O(n)). Above ~500, JS cost exceeds GPU gain.
Pack Sprites into Texture Atlas
Each unique GPU texture = batch boundary = draw call. One atlas = one draw call for all sprites.
**Detection**:
grep -rn "Texture\.from\|Assets\.load.*\.png" --include="*.ts"
---
Detection Commands
# Per-frame tint mutations
grep -rn "\.tint\s*=" --include="*.ts"
# Non-ParticleContainer particle patterns
grep -rn "addChild.*new Sprite" --include="*.ts" | grep -v "ParticleContainer"
# Individual texture loads (no atlas)
grep -rn "Assets\.load.*\.png\|Texture\.from.*\.png" --include="*.ts"
# Filter application level
grep -rn "\.filters\s*=" --include="*.ts"
Read more
description: Sprite batching, texture atlases, RenderTexture caching, off-screen culling, object pooling, ParticleContainer vs Container, and failure modes that break GPU batching in PixiJS v8+ agent: pixijs-combat-renderer category: performance version_range: "PixiJS v8+"
PixiJS Performance Reference
> **Scope**: What breaks batching, draw call measurement, ParticleContainer, texture atlases, RenderTexture caching, object pooling.
---
Batching — What Breaks It
PixiJS v8 batches sprites sharing same texture, blend mode, alpha, and no active filters into one draw call.
| Cause | Prevention | |-------|------------| | Different texture | Pack into one atlas | | Filter on ancestor | Filters on container root only | | Different blend mode | Group by blend mode | | Per-sprite tint per frame | One tint per atlas batch | | Alpha on parent Container | Set alpha on sprites, not containers | | Mask on ancestor | Pool masked containers |
---
Texture Atlases
await Assets.load('/atlases/combat.json');
const playerIdleTexture = Texture.from('player_idle_000');
const frames = [];
for (let i = 0; i < 8; i++) {
frames.push(Texture.from(`player_idle_${String(i).padStart(3, '0')}`));
}
const animSprite = new AnimatedSprite(frames);
animSprite.animationSpeed = 0.15;
animSprite.play();TexturePacker Settings
Format: Pixi JS (JSON Hash or JSON Array) Algorithm: MaxRects Power of Two: YES Max texture: 2048x2048 Padding: 2 Trim sprites: YES Extrude: 1
---
RenderTexture for Cached Composites
Bake expensive composites that don't change every frame:
function bakeCharacterToTexture(app: Application, character: Container, width: number, height: number): Sprite {
const renderTexture = RenderTexture.create({ width, height });
app.renderer.render({ container: character, target: renderTexture });
return new Sprite(renderTexture);
}
// Re-bake on state change, not every frame---
Off-Screen Culling
PixiJS does not cull by default.
const screenBounds = new Rectangle(0, 0, app.screen.width, app.screen.height);
function cullChildren(container: Container): void {
for (const child of container.children) {
if (child instanceof Sprite) {
const bounds = child.getBounds();
child.visible = screenBounds.intersects(new Rectangle(
bounds.x - 50, bounds.y - 50, bounds.width + 100, bounds.height + 100,
));
}
}
}
// Every 3 frames is sufficient
let cullFrame = 0;
app.ticker.add(() => { if (++cullFrame % 3 === 0) cullChildren(entityLayer); });---
Object Pooling
class SpritePool {
private pool: Sprite[] = [];
constructor(texture: Texture, parent: Container, initialSize = 20) {
this.texture = texture;
this.parent = parent;
for (let i = 0; i < initialSize; i++) {
const sprite = new Sprite(texture);
sprite.visible = false;
parent.addChild(sprite);
this.pool.push(sprite);
}
}
get(): Sprite | null {
const sprite = this.pool.find(s => !s.visible);
if (!sprite) return null;
sprite.visible = true; sprite.alpha = 1; sprite.rotation = 0; sprite.scale.set(1);
return sprite;
}
release(sprite: Sprite): void {
sprite.visible = false;
sprite.position.set(-9999, -9999);
}
}---
ParticleContainer vs Container
| Feature | ParticleContainer | Container | |---------|------------------|-----------| | Max sprites | 100,000+ | ~5,000 | | Filters | No | Yes | | Children of children | No | Yes | | Blend mode per sprite | No | Yes |
const particleContainer = new ParticleContainer(10000, {
position: true, rotation: true, scale: true, alpha: true,
uvs: false, // disable if single texture
tint: false, // disable if no tint variation
});**When NOT to use**: Mixed textures, particles needing filters, particles with children.
---
Pattern Catalog
Group Tint Changes by Value
Per-frame `.tint` flushes batch per sprite. 100 sprites = 100 draw calls. **Fix**: Use custom shader uniform or group sprites by tint value.
Use ParticleContainer for 500+ Uniform Particles
Regular Container: each child inspected in JS every frame (O(n)). Above ~500, JS cost exceeds GPU gain.
Pack Sprites into Texture Atlas
Each unique GPU texture = batch boundary = draw call. One atlas = one draw call for all sprites.
**Detection**:
grep -rn "Texture\.from\|Assets\.load.*\.png" --include="*.ts"
---
Detection Commands
# Per-frame tint mutations grep -rn "\.tint\s*=" --include="*.ts" # Non-ParticleContainer particle patterns grep -rn "addChild.*new Sprite" --include="*.ts" | grep -v "ParticleContainer" # Individual texture loads (no atlas) grep -rn "Assets\.load.*\.png\|Texture\.from.*\.png" --include="*.ts" # Filter application level grep -rn "\.filters\s*=" --include="*.ts"
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

