关于 pixijs-performance
pixijs-performance 是 PixiJS 性能调优技能,指导开发者先用浏览器性能与 GPU 分析定位瓶颈,再选择缓存、对象复用、批处理或剔除等方法。
主要功能
- 01建议先分析性能瓶颈
- 02使用缓存纹理减少绘制调用
- 03复用对象并优化批处理
- 04管理纹理清理与 GPU 上传
- 05按需启用视锥剔除
适用场景
- 01减少复杂静态界面的重复绘制
- 02优化频繁更新的文本和对象
- 03处理纹理资源释放及应用重建
- 04在 GPU 受限时跳过屏幕外对象
安装
复制下面的命令,在终端中运行。
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.destroy() 和 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
使用 PrepareSystem 上传到 GPU
在渲染前将纹理和图形上传到 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;当纹理数量超出上限时,还会中断批处理。精灵表会将许多帧整合到一张图集纹理中。
半分辨率图集的文件名使用 @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 的分辨率与渲染器的分辨率一致。可通过 text.resolution = 1 单独降低分辨率,以减少高 DPI 显示屏上的 GPU 内存占用。
Graphics 性能
Graphics 对象在形状不变时速度最快(变换、alpha 和 tint 不受影响)。小型 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 会使像素数量增加到四倍。在移动设备上,这可能会使帧率减半。请进行性能分析,找到合适的平衡点。
底层渲染优化
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);
}
在一帧内销毁大量纹理会导致卡顿。请将开销分摊到多帧中。
滤镜和遮罩的开销
- 知道边界时,设置
container.filterArea = new Rectangle(x, y, w, h)。否则,PixiJS 每帧都会测量边界。 - 释放滤镜内存:
container.filters = null。 - 全屏滤镜纹理会按屏幕尺寸进行池化,但填充(
BlurFilter的默认设置)会使所需尺寸超出屏幕,并扩展到下一个 2 的幂次方。对全屏模糊设置repeatEdgePixels: true,使其保持屏幕尺寸。 - 遮罩开销(从低到高):轴对齐矩形遮罩(scissor 矩形)< Graphics 遮罩(模板缓冲区)< Sprite/alpha 遮罩(滤镜管线)。无论类型如何,数百个遮罩都会降低性能;边界轴对齐时优先使用矩形遮罩。
- 对于没有可交互子对象的容器,设置
interactiveChildren = false。 - 对大型容器设置
hitArea,以跳过递归的子对象命中测试。
安全的销毁顺序
销毁前先从场景中移除:
parent.removeChild(sprite);
sprite.destroy();
渲染管线仍持有引用时销毁对象会导致空指针崩溃。如果必须在帧中销毁,请延迟处理:
app.ticker.addOnce(() => {
parent.removeChild(sprite);
sprite.destroy();
});
常见错误
[CRITICAL] 销毁 App 时未设置 releaseGlobalResources
错误:
app.destroy();
const newApp = new Application();
正确:
app.destroy({ releaseGlobalResources: true });
const newApp = new Application();
如果不设置此标志,旧 App 中过时的池化批次和纹理会留在全局池中,并被新 App 重用,从而导致闪烁和视觉失真。
[HIGH] 在场景图中交错排列对象类型
sprite / graphic / sprite / graphic = 4 次绘制调用。
sprite / sprite / graphic / graphic = 2 次绘制调用。
按子级顺序将相同对象类型分组,以尽量减少批次中断。混合模式的排列顺序也一样。
[HIGH] 销毁并重新创建对象,而不是循环利用
销毁并重新创建的开销很大:这会释放 GPU 资源、触发垃圾回收,还需要重新上传到 GPU。通过更新 texture、position、visible 和其他属性来复用对象。对于频繁生成和销毁的实体,请使用对象池模式。
[HIGH] 加载许多独立纹理,而不是精灵表
每个独立纹理都会占用自己的 GPU 内存槽,并会在达到每批次纹理数量上限时中断批处理。精灵表会将纹理整合到图集内。此外,应避免使用任一边长超过 4096px 的纹理,因为它们在某些移动 GPU 上会失效。
[HIGH] 每帧更新 Text 或 HTMLText
每次更新都会将完整字符串重新渲染到画布并上传到 GPU。在 60fps 下,这会造成巨大的开销。对于动态内容(分数、计时器、计数器),请使用 BitmapText。如果必须使用画布 Text,则仅在数值实际发生变化时更新。来源:src/docs/concepts/performance-tips.md
[HIGH] 使用复杂的 Graphics,而不是纹理
渲染数百个复杂的 Graphics 对象会很慢。小型 Graphics(少于约 100 个点)可以像 Sprites 一样高效地进行批处理,但复杂的 Graphics 不行。使用 renderer.generateTexture() 将复杂的静态形状渲染到纹理,并将其显示为 Sprite。来源:src/docs/concepts/performance-tips.md
[MEDIUM] 不分批销毁大量纹理
在单帧中销毁数十个纹理会导致明显卡顿。请将销毁操作分散到多帧中(例如,通过 ticker 回调每帧销毁 5 个)。来源:src/docs/concepts/garbage-collection.md
[MEDIUM] 大型场景未使用 PrepareSystem
如果不调用 renderer.prepare.upload(),纹理会在首次渲染时上传到 GPU,导致帧卡顿。对于加载画面或场景切换,请在显示前完成上传。需要使用 import 'pixi.js/prepare'(默认包中也不包含;请始终显式导入)。来源:src/prepare/PrepareSystem.ts
[MEDIUM] 未经性能分析就使用高分辨率或抗锯齿
resolution: 2 会使像素数量增加四倍。antialias: true 会增加 GPU 开销。两者都会降低移动设备上的性能。启用前,请务必在目标硬件上进行性能分析。来源:performance-tips.md