mcpField
P
pixijs-performance

pixijs-performance

github.com/pixijs开发3586.1k 次安装4 周前更新
代码仓库

关于 pixijs-performance

pixijs-performance 是 PixiJS 性能调优技能,指导开发者先用浏览器性能与 GPU 分析定位瓶颈,再选择缓存、对象复用、批处理或剔除等方法。

主要功能

  1. 01建议先分析性能瓶颈
  2. 02使用缓存纹理减少绘制调用
  3. 03复用对象并优化批处理
  4. 04管理纹理清理与 GPU 上传
  5. 05按需启用视锥剔除

适用场景

  1. 01减少复杂静态界面的重复绘制
  2. 02优化频繁更新的文本和对象
  3. 03处理纹理资源释放及应用重建
  4. 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

API 参考

评论

支持 Markdown