⚡ Jõudluse Optimeerimine
Profileerimine, mälu haldus, rendering pipeline, Web Workers, Object Pooling, LOD — saavuta 60 FPS!
📊 Samm 1 — Profileerimine
class PerformanceProfiler {
private timers: Map<string, number> = new Map();
private history: Map<string, number[]> = new Map();
private maxSamples = 120; // 2 sekundit @ 60fps
begin(label: string): void {
this.timers.set(label, performance.now());
}
end(label: string): number {
const start = this.timers.get(label);
if (start === undefined) return 0;
const elapsed = performance.now() - start;
if (!this.history.has(label)) this.history.set(label, []);
const samples = this.history.get(label)!;
samples.push(elapsed);
if (samples.length > this.maxSamples) samples.shift();
return elapsed;
}
getAverage(label: string): number {
const samples = this.history.get(label);
if (!samples || samples.length === 0) return 0;
return samples.reduce((a, b) => a + b, 0) / samples.length;
}
getMax(label: string): number {
const samples = this.history.get(label);
if (!samples) return 0;
return Math.max(...samples);
}
// Debug overlay
drawStats(ctx: CanvasRenderingContext2D): void {
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(10, 10, 250, 20 + this.history.size * 18);
ctx.fillStyle = '#00ff00';
ctx.font = '12px monospace';
let y = 28;
for (const [label, samples] of this.history) {
const avg = this.getAverage(label);
const max = this.getMax(label);
const color = avg > 16 ? '#ff4444' : avg > 8 ? '#ffaa00' : '#00ff00';
ctx.fillStyle = color;
ctx.fillText(`${label}: ${avg.toFixed(2)}ms (max: ${max.toFixed(2)}ms)`, 20, y);
y += 18;
}
ctx.restore();
}
}
// Kasutamine:
// const profiler = new PerformanceProfiler();
// profiler.begin('physics');
// updatePhysics();
// profiler.end('physics');
// profiler.begin('render');
// render();
// profiler.end('render');
♻️ Samm 2 — Object Pooling
class ObjectPool<T> {
private pool: T[] = [];
private active: Set<T> = new Set();
private factory: () => T;
private reset: (obj: T) => void;
constructor(
factory: () => T,
reset: (obj: T) => void,
initialSize = 50
) {
this.factory = factory;
this.reset = reset;
// Eelloo objektid
for (let i = 0; i < initialSize; i++) {
this.pool.push(factory());
}
}
acquire(): T {
let obj: T;
if (this.pool.length > 0) {
obj = this.pool.pop()!;
} else {
obj = this.factory(); // Pool tühi → loo uus
console.warn('ObjectPool: pool tühi, loon uue objekti');
}
this.reset(obj);
this.active.add(obj);
return obj;
}
release(obj: T): void {
if (this.active.delete(obj)) {
this.pool.push(obj);
}
}
get activeCount(): number { return this.active.size; }
get poolSize(): number { return this.pool.length; }
}
// Näide: Kuulide pool
interface Bullet {
x: number; y: number;
vx: number; vy: number;
active: boolean;
}
const bulletPool = new ObjectPool<Bullet>(
() => ({ x: 0, y: 0, vx: 0, vy: 0, active: false }),
(b) => { b.active = true; },
200
);
function shoot(x: number, y: number, angle: number): void {
const bullet = bulletPool.acquire();
bullet.x = x;
bullet.y = y;
bullet.vx = Math.cos(angle) * 500;
bullet.vy = Math.sin(angle) * 500;
}
// Uuendus: taaskasuta "surnud" kuulid
function updateBullets(delta: number): void {
for (const bullet of bulletPool['active']) {
bullet.x += bullet.vx * delta;
bullet.y += bullet.vy * delta;
if (isOffScreen(bullet)) {
bullet.active = false;
bulletPool.release(bullet);
}
}
}
🧵 Samm 3 — Web Workers (Mitmelõimelisus)
// ============ physics.worker.ts ============
// Eraldi fail — töötab teises lõimes!
self.onmessage = (e: MessageEvent) => {
const { type, data } = e.data;
switch (type) {
case 'pathfind': {
// Raske A* arvutus ei blokeeri main thread'i
const path = aStarPathfind(data.grid, data.start, data.end);
self.postMessage({ type: 'pathResult', data: path });
break;
}
case 'generateTerrain': {
const terrain = generatePerlinTerrain(data.width, data.height, data.seed);
// Transferable: kiire üleandmine (zero-copy)
self.postMessage(
{ type: 'terrainResult', data: terrain },
[terrain.buffer] // Transfer ownership
);
break;
}
}
};
// ============ main.ts ============
class WorkerManager {
private worker: Worker;
private callbacks: Map<string, (data: any) => void> = new Map();
constructor(scriptUrl: string) {
this.worker = new Worker(scriptUrl, { type: 'module' });
this.worker.onmessage = (e) => {
const { type, data } = e.data;
const cb = this.callbacks.get(type);
if (cb) {
cb(data);
this.callbacks.delete(type);
}
};
}
request<T>(type: string, data: any): Promise<T> {
return new Promise((resolve) => {
this.callbacks.set(type + 'Result', resolve);
this.worker.postMessage({ type, data });
});
}
terminate(): void {
this.worker.terminate();
}
}
// Kasutamine:
// const physics = new WorkerManager('/workers/physics.worker.js');
// const path = await physics.request('pathfind', { grid, start, end });
// Tulemus: main thread ei blokeeru!
🖥️ Samm 4 — Renderdamise Optimeerimine
// 1. Off-screen Canvas (eelrenderdamine)
class CachedSprite {
private cache: OffscreenCanvas;
private cacheCtx: OffscreenCanvasRenderingContext2D;
constructor(private sourceImage: HTMLImageElement, private scale = 1) {
const w = sourceImage.width * scale;
const h = sourceImage.height * scale;
this.cache = new OffscreenCanvas(w, h);
this.cacheCtx = this.cache.getContext('2d')!;
this.cacheCtx.drawImage(sourceImage, 0, 0, w, h);
}
draw(ctx: CanvasRenderingContext2D, x: number, y: number): void {
ctx.drawImage(this.cache, x, y);
}
}
// 2. Frustum culling (joonista ainult nähtavad)
function isInView(
entity: { x: number; y: number; width: number; height: number },
camera: { x: number; y: number; width: number; height: number }
): boolean {
return (
entity.x + entity.width > camera.x &&
entity.x < camera.x + camera.width &&
entity.y + entity.height > camera.y &&
entity.y < camera.y + camera.height
);
}
// 3. Batch rendering (grupeeritud joonistamine)
function batchRender(
ctx: CanvasRenderingContext2D,
sprites: Array<{ img: HTMLImageElement; x: number; y: number }>,
camera: { x: number; y: number; width: number; height: number }
): void {
// Sorteeri Z-järjekorras (taga → ees)
sprites.sort((a, b) => a.y - b.y);
// Joonista ainult nähtavad
for (const s of sprites) {
if (isInView({ x: s.x, y: s.y, width: 64, height: 64 }, camera)) {
ctx.drawImage(s.img, s.x - camera.x, s.y - camera.y);
}
}
}
// 4. requestAnimationFrame + delta time (mitte setInterval!)
let lastTime = 0;
function gameLoop(timestamp: number): void {
const delta = (timestamp - lastTime) / 1000;
lastTime = timestamp;
// Piira delta spike'e (nt tab vahetuse järel)
const clampedDelta = Math.min(delta, 0.05); // max 50ms
update(clampedDelta);
render();
requestAnimationFrame(gameLoop);
}
💾 Samm 5 — Mälu Haldus
// 1. Väldi GC pause — ära loo objekte game loopis!
// HALB:
function updateBad(): void {
const velocity = new Vector2(0, 0); // ❌ Iga frame uus objekt!
velocity.x = Math.cos(angle) * speed;
velocity.y = Math.sin(angle) * speed;
}
// HEA:
const _tempVec = new Vector2(0, 0); // Korduvkasutatav
function updateGood(): void {
_tempVec.set(Math.cos(angle) * speed, Math.sin(angle) * speed); // ✅
}
// 2. TypedArray suurte andmekogumite jaoks
// Objektide asemel kasuta struktuuri-massiive (SoA):
class ParticleSystem {
private count = 0;
private maxParticles: number;
// Structure of Arrays (SoA) — cache-sõbralik!
private x: Float32Array;
private y: Float32Array;
private vx: Float32Array;
private vy: Float32Array;
private life: Float32Array;
constructor(max = 10000) {
this.maxParticles = max;
this.x = new Float32Array(max);
this.y = new Float32Array(max);
this.vx = new Float32Array(max);
this.vy = new Float32Array(max);
this.life = new Float32Array(max);
}
emit(x: number, y: number, vx: number, vy: number): void {
if (this.count >= this.maxParticles) return;
const i = this.count++;
this.x[i] = x;
this.y[i] = y;
this.vx[i] = vx;
this.vy[i] = vy;
this.life[i] = 1;
}
update(dt: number): void {
for (let i = this.count - 1; i >= 0; i--) {
this.x[i] += this.vx[i] * dt;
this.y[i] += this.vy[i] * dt;
this.life[i] -= dt;
if (this.life[i] <= 0) {
// Swap-remove (O(1) kustutamine)
this.count--;
this.x[i] = this.x[this.count];
this.y[i] = this.y[this.count];
this.vx[i] = this.vx[this.count];
this.vy[i] = this.vy[this.count];
this.life[i] = this.life[this.count];
}
}
}
}
// 10 000 partiklit ilma GC pausideta!
📝 Harjutused
-
Performance Profiler
Loo profiler: begin/end, ajaloo graafik, värvikoodid (roheline/kollane/punane). Kuva reaalajas overlay mängu peal. FPS + frame time.
-
Object Pool geneerika
Loo geneerine ObjectPool<T>: acquire, release, autoExpand. Kasuta kuulide, partiklite ja vaenlaste jaoks. Mõõda GC pause'ide vahe enne/pärast.
-
Web Worker pathfinding
Liiguta A* algoritm Web Worker'isse. 100x100 grid, mitu agenti otsivad paralleelselt teed. Main thread jääb 60 FPS-le.
-
Renderdamise optimeerimine
1000 sprite'i stseen: lisa frustum culling, batch rendering, off-screen cache. Mõõda FPS enne ja pärast igat optimeerimist.
-
TypedArray partiklid
10 000 partiklit SoA (Structure of Arrays) struktuuriga. Swap-remove kustutamine. Võrdle AoS vs SoA jõudlust. Chrome DevTools Memory tab.
-
Spatial hashing + instanced rendering
Spatial hash grid 10 000 entity jaoks. Ainult lähedased entity'd kontrollivad kokkupõrkeid. Three.js: InstancedMesh 5000 puud. FPS mõõtmine.
-
LOD süsteem
Level-of-Detail: 3D objektid vahetavad detailsust kauguse põhjal (high → medium → low → billboard). Lisa fade-üleminekud. Mõõda draw calls.