Skip to content

第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 CallGPU 绘制调用次数
批处理合并多个绘制为一次调用
视口剔除只渲染可见区域
对象池复用对象避免频繁创建销毁
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 练习题

基础练习

  1. 实现一个 FPS 监控面板

  2. 实现简单的视口剔除

  3. 创建一个 Sprite 对象池

进阶练习

  1. 实现纹理内存监控

  2. 优化一个包含 1000 个对象的场景

挑战练习

  1. 实现一个完整的性能分析和优化系统

下一章预告:在第15章中,我们将学习 PixiJS 的高级特性与扩展。


文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个

如有转载或 CV 的请标注本站原文地址