Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
Learn how to optimize your game for maximum performance with frame budget management, memory optimization, and draw call reduction.
STEM is designed for high-performance game development with a strict 16.67ms frame budget (60 FPS). This guide covers essential optimization techniques to keep your game running smoothly.
The frame budget is the maximum time available to render a single frame. For 60 FPS, you have 16.67ms per frame. STEM helps you stay within this budget.
| Phase | Target (ms) | Description |
|---|---|---|
| Input | 0.5 | Controller and keyboard input processing |
| Logic | 2.0 | Game logic and AI updates |
| Physics | 3.0 | Physics simulation and collision detection |
| Animation | 1.5 | Skeletal and sprite animations |
| Rendering | 8.0 | Draw calls, shaders, and GPU work |
| Post-Processing | 1.0 | Effects like bloom, SSAO |
| Buffer | 0.67 | Safety margin for frame variance |
Frame Budget Warning
import { performanceMonitor, frameProfiler } from '@stem/core/perf';// Get frame statisticsconst stats = frameProfiler.getStatistics();console.log('Average frame time:', stats.averageFrameTime, 'ms');console.log('P95 frame time:', stats.p95FrameTime, 'ms');console.log('P99 frame time:', stats.p99FrameTime, 'ms');// Get phase breakdownconsole.log('Phase times:', stats.averagePhaseTimes);console.log('Slowest systems:', stats.slowestSystems);// Check for warningsconst warnings = performanceMonitor.checkPerformance();warnings.forEach(warning => {console.log(`[${warning.severity}] ${warning.message}`);console.log('Recommendation:', warning.recommendation);});
Memory management is critical for maintaining consistent frame rates. STEM uses object pooling and typed arrays to minimize garbage collection.
Hot paths (code executed every frame) should avoid allocations. STEM provides tools to achieve zero garbage collection in critical systems.
import { memoryTracker } from '@stem/core/perf';// Enable memory trackingmemoryTracker.setEnabled(true);// Get memory statisticsconst stats = memoryTracker.getStats();console.log('Current:', stats.current.usedJSHeapSize / 1024 / 1024, 'MB');console.log('Peak:', stats.peak.usedJSHeapSize / 1024 / 1024, 'MB');console.log('Trend:', stats.trend); // 'increasing' | 'decreasing' | 'stable'console.log('GC events:', stats.gcEvents);// Detect memory leaksconst leak = memoryTracker.detectLeak();if (leak?.isLeaking) {console.warn('Memory leak detected!');console.log('Leak rate:', leak.leakRate, 'bytes/sec');console.log('Confidence:', leak.confidence);}
Use object pools for frequently created/destroyed objects like projectiles, particles, and temporary math objects.
import { FastVector3Pool } from '@stem/core/utils';// Acquire a pooled vectorconst position = FastVector3Pool.acquire();position.set(x, y, z);// Use it...const distance = position.distanceTo(target);// Release it back to the poolFastVector3Pool.release(position);// Create custom pools for game objectsclass BulletPool {private pool: Bullet[] = [];acquire(): Bullet {return this.pool.pop() || new Bullet();}release(bullet: Bullet): void {bullet.reset();this.pool.push(bullet);}}
Draw calls are expensive. Each draw call has CPU overhead for state changes and GPU commands. Target 100-200 draw calls for optimal performance.
Instancing renders multiple copies of the same mesh in a single draw call. STEM's InstancedRenderSystem handles this automatically.
import { addComponent, Instanced } from '@stem/core/ecs';// Mark entity for instanced renderingconst treeEntity = world.createEntity();addComponent(world, Instanced, treeEntity);Instanced.assetId[treeEntity] = treeAssetId;// All trees with the same asset will be batched automatically// 1000 trees = 1 draw call instead of 1000
Combine static meshes that share materials to reduce draw calls.
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils';// Combine multiple static geometriesconst geometries = [geometry1, geometry2, geometry3];const merged = BufferGeometryUtils.mergeGeometries(geometries);// One mesh instead of threeconst mesh = new THREE.Mesh(merged, sharedMaterial);
Combine multiple textures into a single atlas to reduce material switches and draw calls.
// Instead of multiple materials with different texturesconst material1 = new MeshStandardMaterial({ map: texture1 });const material2 = new MeshStandardMaterial({ map: texture2 });// Use one material with a texture atlasconst atlas = createTextureAtlas([texture1, texture2]);const material = new MeshStandardMaterial({ map: atlas });// Offset UVs to select the correct region in the atlasgeometry.setAttribute('uv', new BufferAttribute(atlasUVs, 2));
Optimized assets reduce load times, memory usage, and rendering cost.
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader';// Load compressed textureconst loader = new KTX2Loader();loader.setTranscoderPath('/basis/');loader.detectSupport(renderer);const texture = await loader.loadAsync('texture.ktx2');texture.generateMipmaps = false; // Already has mipmapstexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
Script performance directly impacts frame rate. Follow these guidelines for efficient scripting.
import { createTypedQuery } from '@stem/core/ecs';// Create query once outside the systemconst query = createTypedQuery([Transform, Velocity, Health]);export const MySystem = (world: IWorld) => {// Query entities (cached internally)const entities = query(world);// Process in tight loop (CPU cache friendly)for (let i = 0; i < entities.length; i++) {const eid = entities[i];// Direct component access (no lookups)const vx = Velocity.x[eid];const vy = Velocity.y[eid];Transform.position[eid][0] += vx;Transform.position[eid][1] += vy;}return world;};
// Bad: Expensive sqrt called every frameconst distance = Math.sqrt(dx * dx + dy * dy);if (distance < 10) {// do something}// Good: Use squared distanceconst distanceSq = dx * dx + dy * dy;if (distanceSq < 100) { // 10 * 10// do something}// Bad: Trig in loopfor (let i = 0; i < 360; i++) {const angle = i * Math.PI / 180;const x = Math.cos(angle);const y = Math.sin(angle);}// Good: Pre-computed lookup tableconst sinTable = new Float32Array(360);const cosTable = new Float32Array(360);for (let i = 0; i < 360; i++) {const angle = i * Math.PI / 180;sinTable[i] = Math.sin(angle);cosTable[i] = Math.cos(angle);}for (let i = 0; i < 360; i++) {const x = cosTable[i];const y = sinTable[i];}
Learn to identify and fix the most common performance issues.
| Symptom | Cause | Solution |
|---|---|---|
| High system time | Complex logic or AI | Optimize algorithms, spread work across frames |
| Many GC events | Allocations in hot paths | Use object pools, TypedArrays, reuse objects |
| Slow physics | Too many rigidbodies | Use static colliders, reduce simulation frequency |
| Input lag | Blocking operations | Move expensive work to web workers |
| Symptom | Cause | Solution |
|---|---|---|
| High draw calls | Too many meshes | Use instancing, merge meshes, texture atlases |
| High triangle count | Complex geometry | Use LOD system, reduce poly count, frustum culling |
| Slow fill rate | Too many pixels | Reduce resolution, optimize shaders, disable effects |
| Shader compile time | Many material variants | Reduce material count, use uber shaders |
import { performanceMonitor, frameProfiler } from '@stem/core/perf';// Get performance summaryconst summary = performanceMonitor.getSummary();// Check if CPU or GPU boundif (summary.frameTime > 16.67) {// Find the slowest systemconst slowest = summary.systems[0];console.log('Bottleneck:', slowest.name, slowest.time, 'ms');// Check allocation rateconst totalAlloc = Object.values(summary.allocations).reduce((sum, kb) => sum + kb, 0);if (totalAlloc > 50) {console.warn('High allocation rate:', totalAlloc, 'KB/frame');}// Check draw callsif (summary.drawCalls > 500) {console.warn('Too many draw calls:', summary.drawCalls);}}// Get detailed frame profileconst frames = frameProfiler.getRecentFrames(60); // Last 60 framesconst avgHotPaths = {render: frames.reduce((sum, f) => sum + f.hotPaths.render, 0) / frames.length,physics: frames.reduce((sum, f) => sum + f.hotPaths.physics, 0) / frames.length,script: frames.reduce((sum, f) => sum + f.hotPaths.script, 0) / frames.length,};console.log('Hot path breakdown:', avgHotPaths);
Performance Checklist