第14章:性能优化实践
14.1 章节概述
性能优化是游戏和应用开发中的关键环节。PixiJS 虽然已经很快,但在复杂场景中仍需要优化才能保持流畅。
本章将深入讲解:
- 性能指标:FPS、Draw Call、内存
- 渲染优化:批处理、纹理图集、视口剔除
- 对象优化:对象池、缓存、懒加载
- 内存管理:纹理管理、垃圾回收
- 调试工具:性能分析、问题定位
- 最佳实践:优化清单、常见陷阱
14.2 性能指标
14.2.1 关键指标
关键性能指标
┌─────────────────────────────────────────────────────────────┐
│ FPS (帧率) │
├─────────────────────────────────────────────────────────────┤
│ - 目标:60 FPS │
│ - 可接受:30 FPS │
│ - 低于 30 FPS:明显卡顿 │
│ │
│ 帧时间: │
│ - 60 FPS = 16.67ms/帧 │
│ - 30 FPS = 33.33ms/帧 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Draw Call │
├─────────────────────────────────────────────────────────────┤
│ - 每次 GPU 绘制调用 │
│ - 目标:< 100 │
│ - 可接受:< 500 │
│ - 过多会导致 CPU 瓶颈 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 内存使用 │
├─────────────────────────────────────────────────────────────┤
│ - 纹理内存(GPU) │
│ - JavaScript 堆内存 │
│ - 目标:稳定,无持续增长 │
└─────────────────────────────────────────────────────────────┘14.2.2 性能监控
typescript
/**
* 性能监控
*/
// FPS 监控
class FPSMonitor {
private frames: number = 0;
private lastTime: number = performance.now();
private fps: number = 0;
private text: PIXI.Text;
constructor() {
this.text = new PIXI.Text('FPS: 0', {
fontSize: 14,
fill: 0x00FF00
});
}
update() {
this.frames++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
this.fps = this.frames;
this.frames = 0;
this.lastTime = now;
this.text.text = `FPS: ${this.fps}`;
}
}
getDisplay(): PIXI.Text {
return this.text;
}
getFPS(): number {
return this.fps;
}
}
// 完整性能面板
class PerformancePanel extends PIXI.Container {
private fpsText: PIXI.Text;
private drawCallText: PIXI.Text;
private memoryText: PIXI.Text;
private frames: number = 0;
private lastTime: number = performance.now();
constructor() {
super();
const style = { fontSize: 12, fill: 0x00FF00 };
this.fpsText = new PIXI.Text('FPS: 0', style);
this.drawCallText = new PIXI.Text('Draw Calls: 0', style);
this.drawCallText.y = 15;
this.memoryText = new PIXI.Text('Memory: 0 MB', style);
this.memoryText.y = 30;
const bg = new PIXI.Graphics();
bg.beginFill(0x000000, 0.7);
bg.drawRect(0, 0, 150, 50);
bg.endFill();
this.addChild(bg, this.fpsText, this.drawCallText, this.memoryText);
}
update(renderer: PIXI.Renderer) {
this.frames++;
const now = performance.now();
if (now - this.lastTime >= 1000) {
this.fpsText.text = `FPS: ${this.frames}`;
this.frames = 0;
this.lastTime = now;
// 内存(如果可用)
if (performance.memory) {
const mb = (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1);
this.memoryText.text = `Memory: ${mb} MB`;
}
}
// Draw Calls(需要开启调试)
// this.drawCallText.text = `Draw Calls: ${renderer.renderingToScreen}`;
}
}
// 使用
const perfPanel = new PerformancePanel();
perfPanel.position.set(10, 10);
app.stage.addChild(perfPanel);
app.ticker.add(() => {
perfPanel.update(app.renderer);
});14.3 渲染优化
14.3.1 批处理
typescript
/**
* 批处理优化
*/
/*
批处理原理:
PixiJS 会自动将相同纹理、相同混合模式的对象合并为一次 Draw Call
中断批处理的情况:
1. 不同纹理
2. 不同混合模式
3. 滤镜
4. 遮罩
5. 不同着色器
*/
// 优化前:多次 Draw Call
const sprites = [];
for (let i = 0; i < 100; i++) {
const sprite = new PIXI.Sprite(textures[i % 10]); // 10 种不同纹理
sprites.push(sprite);
container.addChild(sprite);
}
// 结果:可能 10+ Draw Calls
// 优化后:使用纹理图集
const sheet = await PIXI.Assets.load('spritesheet.json');
for (let i = 0; i < 100; i++) {
const sprite = new PIXI.Sprite(sheet.textures[`frame${i % 10}`]);
sprites.push(sprite);
container.addChild(sprite);
}
// 结果:1 Draw Call
// 按纹理排序
function sortByTexture(container: PIXI.Container) {
container.children.sort((a, b) => {
const texA = (a as PIXI.Sprite).texture?.baseTexture?.uid || 0;
const texB = (b as PIXI.Sprite).texture?.baseTexture?.uid || 0;
return texA - texB;
});
}14.3.2 纹理图集
typescript
/**
* 纹理图集最佳实践
*/
// 1. 使用工具生成图集
// TexturePacker, ShoeBox, Free Texture Packer
// 2. 图集大小建议
// - 移动设备:最大 2048x2048
// - 桌面设备:最大 4096x4096
// 3. 按用途分组
// - UI 图集
// - 角色图集
// - 特效图集
// - 地图图集
// 4. 加载策略
async function loadAssets() {
// 预加载核心图集
await PIXI.Assets.load('core-sprites.json');
// 按需加载其他图集
// PIXI.Assets.backgroundLoad('level1-sprites.json');
}14.3.3 视口剔除
typescript
/**
* 视口剔除
*/
class ViewportCuller {
private viewport: PIXI.Rectangle;
private padding: number;
constructor(viewport: PIXI.Rectangle, padding: number = 50) {
this.viewport = viewport;
this.padding = padding;
}
updateViewport(x: number, y: number, width: number, height: number) {
this.viewport.x = x - this.padding;
this.viewport.y = y - this.padding;
this.viewport.width = width + this.padding * 2;
this.viewport.height = height + this.padding * 2;
}
cull(objects: PIXI.DisplayObject[]) {
for (const obj of objects) {
const bounds = obj.getBounds();
obj.visible = this.viewport.intersects(bounds);
}
}
// 更高效的版本(不使用 getBounds)
cullFast(sprites: PIXI.Sprite[]) {
for (const sprite of sprites) {
const x = sprite.x;
const y = sprite.y;
const w = sprite.width;
const h = sprite.height;
sprite.visible =
x + w > this.viewport.x &&
x < this.viewport.right &&
y + h > this.viewport.y &&
y < this.viewport.bottom;
}
}
}
// 使用
const culler = new ViewportCuller(app.screen);
app.ticker.add(() => {
culler.updateViewport(camera.x, camera.y, app.screen.width, app.screen.height);
culler.cullFast(gameObjects);
});14.3.4 层级优化
typescript
/**
* 层级优化
*/
// 1. 减少嵌套层级
// 不好
const a = new PIXI.Container();
const b = new PIXI.Container();
const c = new PIXI.Container();
a.addChild(b);
b.addChild(c);
c.addChild(sprite);
// 好
const container = new PIXI.Container();
container.addChild(sprite);
// 2. 使用 cacheAsBitmap
// 对于静态复杂内容
const staticUI = new PIXI.Container();
// ... 添加很多子对象
staticUI.cacheAsBitmap = true;
// 3. 使用 ParticleContainer
// 对于大量相似对象
const particles = new PIXI.ParticleContainer(10000, {
scale: true,
position: true,
rotation: true,
alpha: true
});
// 4. 禁用不需要的功能
container.interactiveChildren = false; // 禁用子对象交互
container.sortableChildren = false; // 禁用排序14.4 对象优化
14.4.1 对象池
typescript
/**
* 对象池
*/
class ObjectPool<T> {
private pool: T[] = [];
private factory: () => T;
private reset: (obj: T) => void;
constructor(factory: () => T, reset: (obj: T) => void, initialSize: number = 0) {
this.factory = factory;
this.reset = reset;
for (let i = 0; i < initialSize; i++) {
this.pool.push(factory());
}
}
get(): T {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.factory();
}
release(obj: T) {
this.reset(obj);
this.pool.push(obj);
}
clear() {
this.pool = [];
}
get size(): number {
return this.pool.length;
}
}
// Sprite 对象池
const spritePool = new ObjectPool<PIXI.Sprite>(
() => new PIXI.Sprite(),
(sprite) => {
sprite.texture = PIXI.Texture.EMPTY;
sprite.parent?.removeChild(sprite);
sprite.position.set(0, 0);
sprite.scale.set(1);
sprite.rotation = 0;
sprite.alpha = 1;
sprite.visible = true;
sprite.tint = 0xFFFFFF;
},
100
);
// 使用
const sprite = spritePool.get();
sprite.texture = myTexture;
container.addChild(sprite);
// 回收
spritePool.release(sprite);
// 带类型的对象池
class TypedPool<T extends PIXI.DisplayObject> {
private pools: Map<string, T[]> = new Map();
private factories: Map<string, () => T> = new Map();
register(type: string, factory: () => T) {
this.factories.set(type, factory);
this.pools.set(type, []);
}
get(type: string): T | null {
const pool = this.pools.get(type);
const factory = this.factories.get(type);
if (!pool || !factory) return null;
if (pool.length > 0) {
return pool.pop()!;
}
return factory();
}
release(type: string, obj: T) {
const pool = this.pools.get(type);
if (pool) {
obj.parent?.removeChild(obj);
pool.push(obj);
}
}
}14.4.2 延迟初始化
typescript
/**
* 延迟初始化
*/
class LazySprite {
private _sprite: PIXI.Sprite | null = null;
private textureUrl: string;
constructor(textureUrl: string) {
this.textureUrl = textureUrl;
}
get sprite(): PIXI.Sprite {
if (!this._sprite) {
this._sprite = PIXI.Sprite.from(this.textureUrl);
}
return this._sprite;
}
destroy() {
if (this._sprite) {
this._sprite.destroy();
this._sprite = null;
}
}
}
// 延迟加载容器
class LazyContainer extends PIXI.Container {
private loaded: boolean = false;
private loadFn: () => Promise<void>;
constructor(loadFn: () => Promise<void>) {
super();
this.loadFn = loadFn;
}
async ensureLoaded() {
if (!this.loaded) {
await this.loadFn();
this.loaded = true;
}
}
}14.4.3 缓存策略
typescript
/**
* 缓存策略
*/
// 计算结果缓存
class ComputeCache<K, V> {
private cache: Map<K, V> = new Map();
private maxSize: number;
constructor(maxSize: number = 100) {
this.maxSize = maxSize;
}
get(key: K, compute: () => V): V {
if (this.cache.has(key)) {
return this.cache.get(key)!;
}
const value = compute();
if (this.cache.size >= this.maxSize) {
// 移除最旧的
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
return value;
}
clear() {
this.cache.clear();
}
}
// 使用
const boundsCache = new ComputeCache<PIXI.DisplayObject, PIXI.Rectangle>();
function getCachedBounds(obj: PIXI.DisplayObject): PIXI.Rectangle {
return boundsCache.get(obj, () => obj.getBounds());
}
// 渲染纹理缓存
class RenderTextureCache {
private cache: Map<string, PIXI.RenderTexture> = new Map();
private renderer: PIXI.Renderer;
constructor(renderer: PIXI.Renderer) {
this.renderer = renderer;
}
render(key: string, target: PIXI.DisplayObject): PIXI.RenderTexture {
let texture = this.cache.get(key);
if (!texture) {
const bounds = target.getBounds();
texture = PIXI.RenderTexture.create({
width: bounds.width,
height: bounds.height
});
this.cache.set(key, texture);
}
this.renderer.render(target, { renderTexture: texture });
return texture;
}
invalidate(key: string) {
const texture = this.cache.get(key);
if (texture) {
texture.destroy();
this.cache.delete(key);
}
}
clear() {
for (const texture of this.cache.values()) {
texture.destroy();
}
this.cache.clear();
}
}14.5 内存管理
14.5.1 纹理内存
typescript
/**
* 纹理内存管理
*/
// 估算纹理内存
function estimateTextureMemory(texture: PIXI.Texture): number {
const bt = texture.baseTexture;
return bt.width * bt.height * 4; // RGBA
}
// 纹理内存监控
class TextureMemoryMonitor {
private textures: Set<PIXI.BaseTexture> = new Set();
track(texture: PIXI.Texture) {
this.textures.add(texture.baseTexture);
}
untrack(texture: PIXI.Texture) {
this.textures.delete(texture.baseTexture);
}
getTotalMemory(): number {
let total = 0;
for (const bt of this.textures) {
total += bt.width * bt.height * 4;
}
return total;
}
getMemoryMB(): string {
return (this.getTotalMemory() / 1024 / 1024).toFixed(2);
}
}
// 自动卸载不使用的纹理
class TextureManager {
private textures: Map<string, { texture: PIXI.Texture; lastUsed: number }> = new Map();
private maxAge: number = 60000; // 60 秒
get(key: string): PIXI.Texture | undefined {
const entry = this.textures.get(key);
if (entry) {
entry.lastUsed = Date.now();
return entry.texture;
}
return undefined;
}
set(key: string, texture: PIXI.Texture) {
this.textures.set(key, {
texture,
lastUsed: Date.now()
});
}
cleanup() {
const now = Date.now();
for (const [key, entry] of this.textures) {
if (now - entry.lastUsed > this.maxAge) {
entry.texture.destroy(true);
this.textures.delete(key);
}
}
}
}14.5.2 垃圾回收
typescript
/**
* 减少垃圾回收
*/
// 1. 复用对象
const tempPoint = new PIXI.Point();
const tempRect = new PIXI.Rectangle();
const tempMatrix = new PIXI.Matrix();
function updatePosition(sprite: PIXI.Sprite, x: number, y: number) {
tempPoint.set(x, y);
sprite.position.copyFrom(tempPoint);
}
// 2. 避免在循环中创建对象
// 不好
for (const sprite of sprites) {
const bounds = sprite.getBounds(); // 每次创建新 Rectangle
}
// 好
const bounds = new PIXI.Rectangle();
for (const sprite of sprites) {
sprite.getBounds(false, bounds); // 复用 Rectangle
}
// 3. 使用数组而不是创建新数组
// 不好
function getVisibleSprites(sprites: PIXI.Sprite[]): PIXI.Sprite[] {
return sprites.filter(s => s.visible); // 创建新数组
}
// 好
const visibleSprites: PIXI.Sprite[] = [];
function updateVisibleSprites(sprites: PIXI.Sprite[]) {
visibleSprites.length = 0; // 清空但不创建新数组
for (const sprite of sprites) {
if (sprite.visible) {
visibleSprites.push(sprite);
}
}
}
// 4. 字符串拼接
// 不好
let text = '';
for (let i = 0; i < 100; i++) {
text += `Item ${i}\n`; // 每次创建新字符串
}
// 好
const parts: string[] = [];
for (let i = 0; i < 100; i++) {
parts.push(`Item ${i}`);
}
const text = parts.join('\n');14.5.3 正确销毁
typescript
/**
* 正确销毁对象
*/
// 销毁 Sprite
function destroySprite(sprite: PIXI.Sprite, destroyTexture: boolean = false) {
sprite.removeAllListeners();
sprite.parent?.removeChild(sprite);
sprite.destroy({
children: true,
texture: destroyTexture,
baseTexture: destroyTexture
});
}
// 销毁 Container
function destroyContainer(container: PIXI.Container) {
// 递归移除事件监听
function removeListeners(obj: PIXI.DisplayObject) {
obj.removeAllListeners();
if (obj instanceof PIXI.Container) {
for (const child of obj.children) {
removeListeners(child);
}
}
}
removeListeners(container);
container.parent?.removeChild(container);
container.destroy({ children: true });
}
// 销毁场景
function destroyScene(scene: PIXI.Container, textureManager: TextureManager) {
// 停止所有动画
// ...
// 移除事件监听
scene.removeAllListeners();
// 销毁子对象
scene.destroy({ children: true });
// 清理纹理缓存
textureManager.cleanup();
// 触发垃圾回收(仅调试用)
// if (window.gc) window.gc();
}14.6 调试工具
14.6.1 PixiJS DevTools
typescript
/**
* PixiJS DevTools
*/
// 安装浏览器扩展:PixiJS DevTools
// 启用调试
// 在开发环境中,PixiJS 会自动连接到 DevTools
// 手动启用
if (process.env.NODE_ENV === 'development') {
// @ts-ignore
window.__PIXI_APP__ = app;
}14.6.2 自定义调试面板
typescript
/**
* 自定义调试面板
*/
class DebugPanel extends PIXI.Container {
private texts: Map<string, PIXI.Text> = new Map();
private background: PIXI.Graphics;
private lineHeight: number = 16;
constructor() {
super();
this.background = new PIXI.Graphics();
this.addChild(this.background);
}
set(key: string, value: any) {
let text = this.texts.get(key);
if (!text) {
text = new PIXI.Text('', {
fontSize: 12,
fill: 0x00FF00,
fontFamily: 'monospace'
});
text.y = this.texts.size * this.lineHeight;
this.texts.set(key, text);
this.addChild(text);
}
text.text = `${key}: ${value}`;
this.updateBackground();
}
private updateBackground() {
let maxWidth = 0;
for (const text of this.texts.values()) {
maxWidth = Math.max(maxWidth, text.width);
}
this.background.clear();
this.background.beginFill(0x000000, 0.7);
this.background.drawRect(0, 0, maxWidth + 10, this.texts.size * this.lineHeight + 5);
this.background.endFill();
}
}
// 使用
const debug = new DebugPanel();
debug.position.set(10, 10);
app.stage.addChild(debug);
app.ticker.add(() => {
debug.set('FPS', Math.round(app.ticker.FPS));
debug.set('Objects', app.stage.children.length);
debug.set('Visible', countVisible(app.stage));
});
function countVisible(container: PIXI.Container): number {
let count = 0;
for (const child of container.children) {
if (child.visible) {
count++;
if (child instanceof PIXI.Container) {
count += countVisible(child);
}
}
}
return count;
}14.7 优化清单
14.7.1 渲染优化清单
渲染优化清单
□ 使用纹理图集减少 Draw Call
□ 按纹理排序对象
□ 使用 ParticleContainer 处理大量相似对象
□ 实现视口剔除
□ 对静态内容使用 cacheAsBitmap
□ 减少容器嵌套层级
□ 避免频繁修改 zIndex
□ 减少滤镜和遮罩使用
□ 使用合适的纹理尺寸(2 的幂次)
□ 压缩纹理资源14.7.2 内存优化清单
内存优化清单
□ 使用对象池复用对象
□ 及时销毁不需要的对象
□ 正确销毁纹理和 BaseTexture
□ 避免内存泄漏(移除事件监听)
□ 实现资源卸载机制
□ 监控内存使用
□ 避免在循环中创建对象
□ 复用临时对象(Point, Rectangle 等)
□ 使用延迟加载
□ 限制纹理缓存大小14.7.3 代码优化清单
代码优化清单
□ 使用帧率无关的动画
□ 避免在 Ticker 中进行复杂计算
□ 使用条件更新(只在需要时更新)
□ 降低非关键逻辑的更新频率
□ 使用 Web Worker 处理复杂计算
□ 避免同步阻塞操作
□ 使用事件委托减少事件监听器
□ 实现分帧处理大量操作
□ 使用性能分析工具定位瓶颈
□ 在生产环境禁用调试代码14.8 本章小结
核心概念
| 概念 | 说明 |
|---|---|
| Draw Call | GPU 绘制调用次数 |
| 批处理 | 合并多个绘制为一次调用 |
| 视口剔除 | 只渲染可见区域 |
| 对象池 | 复用对象避免频繁创建销毁 |
| cacheAsBitmap | 将容器缓存为位图 |
| ParticleContainer | 高性能粒子容器 |
关键代码
typescript
// 性能监控
console.log(app.ticker.FPS);
// 视口剔除
sprite.visible = viewport.intersects(sprite.getBounds());
// 对象池
const sprite = spritePool.get();
spritePool.release(sprite);
// 缓存为位图
staticContainer.cacheAsBitmap = true;
// ParticleContainer
const particles = new PIXI.ParticleContainer(10000);
// 正确销毁
sprite.destroy({ children: true, texture: false });14.9 练习题
基础练习
实现一个 FPS 监控面板
实现简单的视口剔除
创建一个 Sprite 对象池
进阶练习
实现纹理内存监控
优化一个包含 1000 个对象的场景
挑战练习
- 实现一个完整的性能分析和优化系统
下一章预告:在第15章中,我们将学习 PixiJS 的高级特性与扩展。
文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个
