DEV Community

Cover image for TCJSGame Performance Optimization: Making Your Games Run Buttery Smooth
Kehinde Owolabi
Kehinde Owolabi

Posted on

TCJSGame Performance Optimization: Making Your Games Run Buttery Smooth

TCJSGame Performance Optimization: Making Your Games Run Buttery Smooth

Game Performance

You've built an amazing TCJSGame project—the mechanics are tight, the visuals are polished, and the gameplay is engaging. But then you test it on a slower device or add a few dozen game objects, and suddenly your smooth 60 FPS becomes a choppy 20 FPS. Sound familiar?

Performance optimization isn't just about making games faster—it's about creating consistent, reliable experiences that work beautifully across all devices. In this deep dive, we'll transform your TCJSGame projects from good to buttery smooth.

Why Performance Matters More Than You Think

Before we dive into code, let's understand what's at stake:

  • Player Retention: 53% of mobile users abandon sites that take longer than 3 seconds to load
  • User Experience: Consistent frame rates feel more professional and polished
  • Device Compatibility: Your game should work on budget phones and older computers
  • Battery Life: Efficient games drain less battery on mobile devices

📊 Performance Assessment: Know Your Baseline

First, let's establish how to measure performance in TCJSGame:

class PerformanceMonitor {
    constructor() {
        this.frameCount = 0;
        this.lastFpsUpdate = 0;
        this.currentFPS = 0;
        this.frameTimes = [];
        this.averageFrameTime = 0;
    }

    update() {
        this.frameCount++;
        const now = Date.now();

        // Calculate FPS every second
        if (now - this.lastFpsUpdate >= 1000) {
            this.currentFPS = this.frameCount;
            this.frameCount = 0;
            this.lastFpsUpdate = now;

            // Log performance data (remove in production)
            if (this.currentFPS < 50) {
                console.warn(`Low FPS: ${this.currentFPS}. Average frame time: ${this.averageFrameTime}ms`);
            }
        }

        // Track frame times
        this.frameTimes.push(now);
        if (this.frameTimes.length > 60) {
            this.frameTimes.shift();
        }

        // Calculate average frame time
        if (this.frameTimes.length > 1) {
            const totalTime = this.frameTimes[this.frameTimes.length - 1] - this.frameTimes[0];
            this.averageFrameTime = totalTime / (this.frameTimes.length - 1);
        }
    }

    getPerformanceReport() {
        return {
            fps: this.currentFPS,
            averageFrameTime: this.averageFrameTime,
            status: this.currentFPS >= 50 ? 'Excellent' : 
                   this.currentFPS >= 30 ? 'Acceptable' : 'Poor'
        };
    }
}

// Usage
const perfMonitor = new PerformanceMonitor();

function update() {
    perfMonitor.update();

    // Your game logic here...

    // Log performance periodically
    if (display.frameNo % 300 === 0) {
        console.table(perfMonitor.getPerformanceReport());
    }
}
Enter fullscreen mode Exit fullscreen mode

⚡ Frame Rate Optimization Strategies

1. Smart Game Loop Management

TCJSGame's default game loop runs every 20ms (50 FPS), but you can optimize this:

class OptimizedGameLoop {
    constructor() {
        this.lastUpdate = 0;
        this.updateInterval = 16; // ~60 FPS
        this.slowUpdateInterval = 33; // ~30 FPS for non-critical systems
        this.lastSlowUpdate = 0;
    }

    update() {
        const now = Date.now();

        // Main update (60 FPS)
        if (now - this.lastUpdate >= this.updateInterval) {
            this.mainUpdate();
            this.lastUpdate = now;
        }

        // Slow update for non-critical systems (30 FPS)
        if (now - this.lastSlowUpdate >= this.slowUpdateInterval) {
            this.slowUpdate();
            this.lastSlowUpdate = now;
        }
    }

    mainUpdate() {
        // Critical systems: input, physics, collision
        this.updatePlayerMovement();
        this.updatePhysics();
        this.checkCollisions();
    }

    slowUpdate() {
        // Non-critical systems: AI, pathfinding, particle updates
        this.updateEnemyAI();
        this.updateParticleSystems();
        this.updateBackgroundElements();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Delta Time Mastery

Ensure consistent movement regardless of frame rate:

class DeltaTimeMovement {
    constructor() {
        this.lastTime = Date.now();
    }

    update() {
        const currentTime = Date.now();
        const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
        this.lastTime = currentTime;

        // Use delta time for all movement calculations
        this.updateWithDelta(deltaTime);
    }

    updateWithDelta(dt) {
        // Bad: Frame-rate dependent
        // player.x += 5;

        // Good: Frame-rate independent
        player.x += 300 * dt; // 300 pixels per second

        // Apply to all moving objects
        enemies.forEach(enemy => {
            enemy.x += enemy.speed * dt;
        });

        particles.forEach(particle => {
            particle.x += particle.velocityX * dt;
            particle.y += particle.velocityY * dt;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

🧠 Memory Management Excellence

1. Object Pooling Pattern

Stop creating and destroying objects constantly:

class ObjectPool {
    constructor(createFn, resetFn, initialSize = 10) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.pool = [];
        this.active = new Set();

        // Pre-populate pool
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(this.createFn());
        }
    }

    acquire() {
        let obj;

        if (this.pool.length > 0) {
            obj = this.pool.pop();
        } else {
            obj = this.createFn();
        }

        this.resetFn(obj);
        this.active.add(obj);
        return obj;
    }

    release(obj) {
        if (this.active.has(obj)) {
            this.active.delete(obj);
            this.pool.push(obj);
        }
    }

    releaseAll() {
        this.active.forEach(obj => {
            this.pool.push(obj);
        });
        this.active.clear();
    }
}

// Usage for bullets
const bulletPool = new ObjectPool(
    // Creation function
    () => new Component(5, 5, "yellow", 0, 0, "rect"),
    // Reset function
    (bullet) => {
        bullet.x = player.x;
        bullet.y = player.y;
        bullet.speedX = 10;
        bullet.speedY = 0;
        bullet.active = true;
    },
    20 // Initial pool size
);

function fireBullet() {
    const bullet = bulletPool.acquire();
    display.add(bullet);

    // Automatically return to pool after 2 seconds
    setTimeout(() => {
        bulletPool.release(bullet);
        display.add(bullet, 1); // Remove from display
    }, 2000);
}
Enter fullscreen mode Exit fullscreen mode

2. Efficient Garbage Collection

Minimize JavaScript garbage collection pauses:

class GarbageCollectionAware {
    constructor() {
        // Reuse arrays and objects instead of creating new ones
        this.reusableArray = [];
        this.reusableVector = { x: 0, y: 0 };
        this.tempBounds = { left: 0, right: 0, top: 0, bottom: 0 };
    }

    // Bad: Creates new array every call
    findNearbyEnemiesBad(player) {
        return enemies.filter(enemy => {
            return Math.abs(enemy.x - player.x) < 100;
        });
    }

    // Good: Reuses array
    findNearbyEnemiesGood(player) {
        this.reusableArray.length = 0; // Clear efficiently

        for (let i = 0; i < enemies.length; i++) {
            if (Math.abs(enemies[i].x - player.x) < 100) {
                this.reusableArray.push(enemies[i]);
            }
        }

        return this.reusableArray;
    }

    // Reuse object for calculations
    calculateDistance(obj1, obj2) {
        this.reusableVector.x = obj2.x - obj1.x;
        this.reusableVector.y = obj2.y - obj1.y;
        return Math.sqrt(this.reusableVector.x * this.reusableVector.x + 
                        this.reusableVector.y * this.reusableVector.y);
    }
}
Enter fullscreen mode Exit fullscreen mode

🎨 Rendering Performance

1. Viewport Culling System

Only render what's visible:

class ViewportCulling {
    constructor() {
        this.visibleObjects = [];
        this.lastCameraX = 0;
        this.lastCameraY = 0;
        this.updateThreshold = 50; // Only update when camera moves significantly
    }

    updateVisibleObjects(allObjects) {
        const camera = display.camera;

        // Skip update if camera hasn't moved much
        if (Math.abs(camera.x - this.lastCameraX) < this.updateThreshold &&
            Math.abs(camera.y - this.lastCameraY) < this.updateThreshold) {
            return;
        }

        this.lastCameraX = camera.x;
        this.lastCameraY = camera.y;

        // Calculate viewport bounds with margin for objects entering screen
        const margin = 100;
        const viewport = {
            left: camera.x - margin,
            right: camera.x + display.canvas.width + margin,
            top: camera.y - margin,
            bottom: camera.y + display.canvas.height + margin
        };

        this.visibleObjects = allObjects.filter(obj => {
            return this.isInViewport(obj, viewport);
        });
    }

    isInViewport(obj, viewport) {
        return obj.x + obj.width >= viewport.left &&
               obj.x <= viewport.right &&
               obj.y + obj.height >= viewport.top &&
               obj.y <= viewport.bottom;
    }

    render() {
        this.visibleObjects.forEach(obj => {
            if (obj.update) {
                obj.update(display.context);
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Batch Rendering for Similar Objects

class BatchRenderer {
    constructor() {
        this.batches = new Map();
    }

    addToBatch(object, batchKey) {
        if (!this.batches.has(batchKey)) {
            this.batches.set(batchKey, []);
        }
        this.batches.get(batchKey).push(object);
    }

    renderBatch(batchKey) {
        const batch = this.batches.get(batchKey);
        if (!batch || batch.length === 0) return;

        const ctx = display.context;
        const firstObj = batch[0];

        // Set common style for entire batch
        ctx.fillStyle = firstObj.color;

        // Render all objects in batch
        batch.forEach(obj => {
            if (obj.type === "rect") {
                ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
            }
            // Add other types as needed
        });

        this.batches.set(batchKey, []); // Clear batch for next frame
    }

    renderAll() {
        this.batches.forEach((batch, key) => {
            this.renderBatch(key);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

🗺️ Advanced TileMap Performance

1. Chunk-Based Loading

class ChunkedTileMap {
    constructor(chunkSize = 10) {
        this.chunkSize = chunkSize;
        this.loadedChunks = new Set();
        this.activeChunks = new Set();
    }

    getChunkKey(x, y) {
        return `${Math.floor(x / this.chunkSize)},${Math.floor(y / this.chunkSize)}`;
    }

    updateActiveChunks(playerX, playerY) {
        const playerChunk = this.getChunkKey(playerX, playerY);
        const newActiveChunks = new Set();

        // Load 3x3 area around player
        for (let dx = -1; dx <= 1; dx++) {
            for (let dy = -1; dy <= 1; dy++) {
                const chunkX = Math.floor(playerX / this.chunkSize) + dx;
                const chunkY = Math.floor(playerY / this.chunkSize) + dy;
                const chunkKey = `${chunkX},${chunkY}`;

                newActiveChunks.add(chunkKey);

                if (!this.loadedChunks.has(chunkKey)) {
                    this.loadChunk(chunkX, chunkY);
                }
            }
        }

        // Unload distant chunks
        this.activeChunks.forEach(chunkKey => {
            if (!newActiveChunks.has(chunkKey)) {
                this.unloadChunk(chunkKey);
            }
        });

        this.activeChunks = newActiveChunks;
    }

    loadChunk(chunkX, chunkY) {
        const chunkKey = `${chunkX},${chunkY}`;
        this.loadedChunks.add(chunkKey);

        // Load your chunk data here
        console.log(`Loading chunk: ${chunkKey}`);
    }

    unloadChunk(chunkKey) {
        this.loadedChunks.delete(chunkKey);

        // Clean up chunk resources
        console.log(`Unloading chunk: ${chunkKey}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Efficient Collision Detection

class SpatialHashGrid {
    constructor(cellSize = 100) {
        this.cellSize = cellSize;
        this.grid = new Map();
    }

    getCellKey(x, y) {
        return `${Math.floor(x / this.cellSize)},${Math.floor(y / this.cellSize)}`;
    }

    insert(object) {
        const cells = this.getObjectCells(object);
        cells.forEach(cellKey => {
            if (!this.grid.has(cellKey)) {
                this.grid.set(cellKey, new Set());
            }
            this.grid.get(cellKey).add(object);
        });

        object._cellKeys = cells;
    }

    getObjectCells(object) {
        const startX = Math.floor(object.x / this.cellSize);
        const startY = Math.floor(object.y / this.cellSize);
        const endX = Math.floor((object.x + object.width) / this.cellSize);
        const endY = Math.floor((object.y + object.height) / this.cellSize);

        const cells = [];
        for (let x = startX; x <= endX; x++) {
            for (let y = startY; y <= endY; y++) {
                cells.push(`${x},${y}`);
            }
        }
        return cells;
    }

    getNearbyObjects(object) {
        const nearby = new Set();
        const cells = this.getObjectCells(object);

        cells.forEach(cellKey => {
            if (this.grid.has(cellKey)) {
                this.grid.get(cellKey).forEach(obj => {
                    if (obj !== object) {
                        nearby.add(obj);
                    }
                });
            }
        });

        return Array.from(nearby);
    }

    update(object) {
        // Remove from old cells
        if (object._cellKeys) {
            object._cellKeys.forEach(cellKey => {
                if (this.grid.has(cellKey)) {
                    this.grid.get(cellKey).delete(object);
                }
            });
        }

        // Insert into new cells
        this.insert(object);
    }

    clear() {
        this.grid.clear();
    }
}

// Usage
const spatialGrid = new SpatialHashGrid(150);

function efficientCollisionCheck(object) {
    const nearby = spatialGrid.getNearbyObjects(object);

    for (let other of nearby) {
        if (object.crashWith(other)) {
            return other;
        }
    }
    return null;
}
Enter fullscreen mode Exit fullscreen mode

🔊 Audio Performance

1. Sound Pooling for Frequent Effects

class OptimizedAudio {
    constructor() {
        this.soundPools = new Map();
        this.maxConcurrentSounds = 8;
        this.activeSounds = new Set();
    }

    createSoundPool(soundName, poolSize = 5) {
        const pool = [];
        for (let i = 0; i < poolSize; i++) {
            const sound = new Sound(`sounds/${soundName}.wav`);
            pool.push(sound);
        }
        this.soundPools.set(soundName, {
            pool: pool,
            currentIndex: 0
        });
    }

    playPooled(soundName) {
        // Limit concurrent sounds
        if (this.activeSounds.size >= this.maxConcurrentSounds) {
            return;
        }

        if (!this.soundPools.has(soundName)) {
            this.createSoundPool(soundName);
        }

        const poolData = this.soundPools.get(soundName);
        const sound = poolData.pool[poolData.currentIndex];

        // Reset and play
        sound.sound.currentTime = 0;
        sound.play();

        // Track active sound
        this.activeSounds.add(sound);

        // Remove from active after completion
        setTimeout(() => {
            this.activeSounds.delete(sound);
        }, 2000); // Assume max sound length

        // Move to next sound in pool
        poolData.currentIndex = (poolData.currentIndex + 1) % poolData.pool.length;

        return sound;
    }
}
Enter fullscreen mode Exit fullscreen mode

🛠️ Real-World Optimization Workflow

1. Performance Budgeting

class PerformanceBudget {
    constructor() {
        this.budgets = {
            frameTime: 16, // 60 FPS target
            memory: 50, // MB
            drawCalls: 100,
            physicsChecks: 1000
        };

        this.current = {
            frameTime: 0,
            memory: 0,
            drawCalls: 0,
            physicsChecks: 0
        };
    }

    startFrame() {
        this.current.drawCalls = 0;
        this.current.physicsChecks = 0;
        this.frameStart = performance.now();
    }

    endFrame() {
        this.current.frameTime = performance.now() - this.frameStart;
        this.checkBudgets();
    }

    checkBudgets() {
        const warnings = [];

        if (this.current.frameTime > this.budgets.frameTime) {
            warnings.push(`Frame time ${this.current.frameTime}ms exceeds budget ${this.budgets.frameTime}ms`);
        }

        if (this.current.drawCalls > this.budgets.drawCalls) {
            warnings.push(`Draw calls ${this.current.drawCalls} exceed budget ${this.budgets.drawCalls}`);
        }

        if (warnings.length > 0) {
            console.warn('Performance budget exceeded:', warnings);
        }
    }

    incrementDrawCalls() {
        this.current.drawCalls++;
    }

    incrementPhysicsChecks() {
        this.current.physicsChecks++;
    }
}

const perfBudget = new PerformanceBudget();

function optimizedUpdate() {
    perfBudget.startFrame();

    // Your optimized game logic
    updateGameLogic();

    perfBudget.endFrame();
}
Enter fullscreen mode Exit fullscreen mode

2. Progressive Enhancement

class ProgressiveEnhancement {
    constructor() {
        this.qualityLevel = this.detectQualityLevel();
        this.applyQualitySettings();
    }

    detectQualityLevel() {
        // Simple detection based on frame rate
        const initialFPS = this.measureInitialFPS();

        if (initialFPS > 55) return 'high';
        if (initialFPS > 30) return 'medium';
        return 'low';
    }

    measureInitialFPS() {
        // Measure FPS during first few seconds
        let frameCount = 0;
        const startTime = Date.now();

        const measure = () => {
            frameCount++;
            if (Date.now() - startTime < 2000) {
                requestAnimationFrame(measure);
            } else {
                const fps = frameCount / 2; // 2 seconds
                console.log(`Detected FPS: ${fps}`);
            }
        };
        measure();
    }

    applyQualitySettings() {
        switch (this.qualityLevel) {
            case 'high':
                this.enableParticles(true);
                this.enableShadows(true);
                this.setParticleCount(100);
                break;
            case 'medium':
                this.enableParticles(true);
                this.enableShadows(false);
                this.setParticleCount(50);
                break;
            case 'low':
                this.enableParticles(false);
                this.enableShadows(false);
                this.setParticleCount(0);
                break;
        }
    }

    dynamicAdjust() {
        // Monitor performance and adjust quality dynamically
        const recentFPS = this.getRecentFPS();

        if (recentFPS < 25 && this.qualityLevel !== 'low') {
            this.qualityLevel = 'low';
            this.applyQualitySettings();
        } else if (recentFPS > 50 && this.qualityLevel === 'low') {
            this.qualityLevel = 'medium';
            this.applyQualitySettings();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

📈 Case Study: Optimizing a Complex Game

Let's see these techniques in action with a before/after example:

Before Optimization

// Naive implementation - poor performance
function update() {
    // Creating new arrays every frame
    const visibleEnemies = enemies.filter(e => 
        Math.abs(e.x - player.x) < 500
    );

    // Inefficient collision checking
    visibleEnemies.forEach(enemy => {
        enemies.forEach(other => {
            if (enemy !== other && enemy.crashWith(other)) {
                // Handle collision...
            }
        });
    });

    // No object pooling
    if (player.attacking) {
        const bullet = new Component(5, 5, "red", player.x, player.y, "rect");
        bullets.push(bullet);
    }
}
Enter fullscreen mode Exit fullscreen mode

After Optimization

// Optimized implementation
const optimizedSystem = new OptimizedGameLoop();
const objectPool = new ObjectPool(/* ... */);
const spatialGrid = new SpatialHashGrid();
const perfMonitor = new PerformanceMonitor();

function optimizedUpdate() {
    perfMonitor.update();
    optimizedSystem.update();

    // Use object pooling
    if (player.attacking) {
        const bullet = bulletPool.acquire();
        // Configure bullet...
    }

    // Efficient collision detection
    const nearby = spatialGrid.getNearbyObjects(player);
    for (let enemy of nearby) {
        if (player.crashWith(enemy)) {
            handleCollision(player, enemy);
        }
    }

    // Performance-aware rendering
    if (perfMonitor.currentFPS < 45) {
        reduceParticleEffects();
    }
}
Enter fullscreen mode Exit fullscreen mode

🚀 Your Performance Challenge

Ready to optimize? Try these exercises:

  1. Profile your existing game and identify the top 3 performance bottlenecks
  2. Implement object pooling for your most frequently created/destroyed objects
  3. Add viewport culling to stop rendering off-screen objects
  4. Create a performance budget and monitor it during development
  5. Implement progressive enhancement that adapts to device capabilities

📚 Key Takeaways

  • Measure First: Always profile before optimizing
  • Object Pooling: Eliminate garbage collection spikes
  • Spatial Partitioning: Reduce collision check complexity
  • Viewport Culling: Render only what's visible
  • Delta Time: Ensure consistent movement across devices
  • Progressive Enhancement: Adapt to hardware capabilities

Performance optimization isn't about making your code more complex—it's about making it smarter. With these TCJSGame optimization techniques, you can create games that feel polished and professional on any device.

What's the biggest performance challenge you've faced in your TCJSGame projects? Share your experiences and solutions in the comments below!


Mastered performance? In our next article, we'll explore advanced TCJSGame architecture patterns that will take your projects to professional level. Follow to stay updated!


This performance optimization guide provides practical, actionable techniques that developers can implement immediately. The real-world examples and before/after comparisons make complex optimization concepts accessible and immediately valuable for TCJSGame developers at all levels.

Top comments (0)