Loading Texture Atlases in PixiJS v8 with the Assets API
PixiJS v8 replaces the old Loader with the Assets API for texture atlases. This guide shows the exact JSON format, loading code, sprite creation, batch‑rendering verification, and memory cleanup — plus the common mistakes that silently break batching or leak GPU memory.
31 Jan 2026, 17:46 UTC

Why the Assets API matters for atlases
PixiJS v8 removes the legacy Loader class. The new Assets API is the only supported way to load a texture atlas (spritesheet) and have its frames populate the global texture cache so that Texture.from('frameName') works. When you load an atlas correctly, every sprite created from its frames shares the same BaseTexture, which lets the WebGL batch renderer draw dozens or hundreds of sprites in a single GPU draw call — provided you don’t break the batch with filter or shader changes.
Atlas JSON format expectations
The Assets.load() expects a JSON file that follows the TexturePacker “JSON (PixiJS)” preset (also called JSON Hash or JSON Array with meta.scale). The minimal required fields are:
frames: an object (hash) or array mapping frame names to{ frame: {x,y,w,h}, rotated, trimmed, spriteSourceSize, sourceSize }.meta.image: the filename of the packed PNG/JPG (relative to the JSON file).meta.scale: a number (usually1) that tells PixiJS the atlas was packed at 1×; without it, high‑DPI displays render frames blurry.
Example atlas.json snippet:
{
"frames": {
"hero_idle_001": {
"frame": {"x":0,"y":0,"w":64,"h":64},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
}
},
"meta": {
"image": "atlas.png",
"scale": 1
}
}
Caution: TexturePacker’s generic “JSON Array” preset often omits meta.scale. Use the “JSON (PixiJS)” preset or add "scale": 1 manually.
Loading and using an atlas
In an ES module (or any async context), load the atlas once during initialization:
import { Assets, Sprite, Texture } from 'pixi.js';
async function init() {
// Load atlas.json (and its referenced atlas.png) under alias 'game'
await Assets.load({ alias: 'game', src: 'assets/atlas.json' });
// Verify frames were parsed
const atlas = Assets.get('game');
console.log('Available frames:', Object.keys(atlas.textures));
// Create sprites from frames — they all share the same BaseTexture
const sprites = [];
for (let i = 0; i < 100; i++) {
const tex = Texture.from('hero_idle_001'); // resolves from global cache
const s = new Sprite(tex);
s.x = Math.random() * 800;
s.y = Math.random() * 600;
sprites.push(s);
}
// Add to stage (assumes `app.stage` exists)
sprites.forEach(s => app.stage.addChild(s));
}
init();
Run this in a browser with a local dev server (e.g., npx serve or Vite). The Assets.load() call returns a Promise that resolves with the parsed atlas object containing a textures map. After the await, Texture.from('frameName') will find the frame because the atlas load populated the global cache.
Confirming single‑draw‑call batching
Open the browser’s GPU profiler (Chrome DevTools → Performance → GPU) or use SpectorJS to capture a frame. With the 100 sprites above you should see exactly one draw call for the sprite batch (plus any background/UI draws). If you see multiple draw calls for the same atlas, check for:
- Sprites using textures from a different atlas (different
BaseTexture). - A
Filter(e.g.,BlurFilter) applied directly to a sprite — filters force a batch break. Move the filter to a parentContainerinstead. - Custom shaders or
mixBlendModechanges between consecutive children.
Dynamic atlases at runtime
If you generate a packed texture on a <canvas> (e.g., baking UI icons), create a BaseTexture from the canvas and manually add Texture regions to the cache:
import { BaseTexture, Texture } from 'pixi.js';
const canvas = document.createElement('canvas');
canvas.width = 512; canvas.height = 512;
// ... draw packed frames onto canvas ...
const base = BaseTexture.from(canvas);
base.update(); // upload to GPU
const frameRect = { x: 0, y: 0, width: 64, height: 64 };
const tex = new Texture(base, frameRect);
Texture.addToCache(tex, 'dynamicIcon');
// Now Texture.from('dynamicIcon') works anywhere
const icon = Sprite.from('dynamicIcon');
This avoids a JSON file entirely but requires you to manage frame coordinates yourself.
Memory management and scene switching
Each loaded atlas keeps its packed image in GPU memory. Assets.cache holds them globally, so switching levels without unloading leaks memory. When a scene is done:
// Free the GPU texture and remove frames from cache
Assets.unload('game');
// Optional: force a GC cycle in development (not for production)
if (window.gc) window.gc();
After unload, Texture.from('hero_idle_001') will return a 1×1 white texture (the missing‑texture fallback). Reload the atlas for the next scene if needed.
Common mistakes checklist
| Mistake | Symptom | Fix |
|---|---|---|
Using legacy PIXI.Loader.shared.add() | No textures in cache, Texture.from() returns white square | Migrate to Assets.load() |
Missing meta.scale in JSON | Blurry sprites on Retina/high‑DPI | Add "scale": 1 or re‑export with PixiJS preset |
Calling Texture.from('missing') without verifying | Silent 1×1 white texture, no error | Check Object.keys(Assets.get('alias').textures) after load |
| Mixing atlases in same container without z‑order grouping | Extra draw calls, batch breaks | Group children by atlas; use ParticleContainer for single‑atlas particles |
| Applying filter to individual sprite | Batch break per filtered sprite | Apply filter to parent Container |
| Loading one huge atlas for entire game | High GPU memory, long initial load | Split atlases per scene/level; load/unload as needed |
Quick verification steps
- Create a minimal project:
npm init -y && npm i pixi@latest. - Add a TexturePacker‑generated
atlas.json+atlas.pngtopublic/assets/. - Write the
init()example above insrc/main.js(type="module"). - Run dev server (
npx viteornpx serve) and open in Chrome. - Open DevTools → Performance, record a frame, check GPU draw calls.
- Call
Assets.unload('game')in console, force GC, verify memory drops by ~atlas image size.
Limitations
- Single image per atlas: The JSON
meta.imagepoints to one file. Multi‑image atlases require separateAssets.load()calls and will not batch across different base textures. - No automatic frame‑name collision handling: If two atlases define the same frame name, the last loaded wins. Use unique prefixes (e.g.,
level1_hero,level2_hero). - Async only:
Assets.load()returns a Promise; you cannot synchronously access frames immediately after the call. - Background loading: Use
Assets.backgroundLoad('atlas.json')for non‑blocking preload with progress viaAssets.onProgress, but you still mustawait Assets.load('atlas.json')(orAssets.get()) before using frames.
With the Assets API, texture atlas loading becomes a one‑liner that feeds the batch renderer exactly what it needs — one GPU texture, many frame wrappers, and predictable draw‑call counts. Keep atlases scoped to scenes, unload when done, and verify with a GPU profiler rather than guessing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.