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
Advanced performance optimization techniques for Web Engine. Learn about frame budgets, memory management, zero-GC patterns, and profiling tools.
Web Engine is built for high-performance 3D games on the web. This guide covers advanced optimization techniques, memory management patterns, and profiling tools to keep your game running at 60 FPS.
At 60 FPS, you have 16.67ms per frame. Budget your time carefully:
Frame Budget (16.67ms at 60 FPS):├── Input ~0.5ms ✓ Minimal├── Network ~0.5ms ✓ Minimal├── Logic ~2.0ms ⚠ Watch scripts├── Physics ~3.0ms ⚠ Main bottleneck├── Animation ~1.0ms ✓ Acceptable├── Audio ~0.5ms ✓ Minimal├── Render ~7.0ms ⚠ Draw calls└── PostRender ~2.0ms ✓ Overhead------16.67ms total
Performance Budget
If any single phase exceeds 10ms, you will drop frames. Use the FrameProfiler to identify bottlenecks.
Keep under 1ms. Pre-allocate input buffers.
Most expensive. Reduce collider count, use simpler shapes.
Minimize draw calls with instancing and batching.
Garbage collection pauses can cause frame drops. Follow these patterns to avoid allocations in the hot path:
import { Vector3Pool, QuaternionPool } from "@web-engine-dev/core";// ❌ BAD - Allocates every framefunction updatePosition(eid: number) {const offset = new THREE.Vector3(1, 0, 0);const position = getPosition(eid);position.add(offset);}// ✅ GOOD - Uses object poolfunction updatePosition(eid: number) {const offset = Vector3Pool.acquire();offset.set(1, 0, 0);const position = getPosition(eid);position.add(offset);Vector3Pool.release(offset);}
Web Engine provides pre-allocated pools for common Three.js objects:
// Global pools (acquire/release pattern)import {Vector3Pool, // THREE.Vector3 poolQuaternionPool, // THREE.Quaternion poolMatrix4Pool, // THREE.Matrix4 poolBox3Pool, // THREE.Box3 poolEulerPool, // THREE.Euler pool} from "@web-engine-dev/core";// Fast ring-buffer pools (no release needed, automatic rotation)import {FastVector3Pool,FastQuaternionPool,FastMatrix4Pool,FastColorPool,} from "@web-engine-dev/core";// Use fast pools for temporary calculationsconst temp = FastVector3Pool.acquire();temp.set(x, y, z);// No need to release - automatically recycled
// ❌ BAD - Creates new array every framefunction getVisibleEntities(entities: number[]): number[] {return entities.filter(eid => isVisible(eid));}// ✅ GOOD - Reuses bufferconst visibleBuffer = new Uint32Array(10000);let visibleCount = 0;function getVisibleEntities(entities: number[]) {visibleCount = 0;for (let i = 0; i < entities.length; i++) {const eid = entities[i];if (isVisible(eid)) {visibleBuffer[visibleCount++] = eid;}}return { buffer: visibleBuffer, count: visibleCount };}
// ❌ BAD - Creates closure every frameentities.forEach(eid => {updateTransform(eid, delta);});// ✅ GOOD - No closure allocationfor (let i = 0; i < entities.length; i++) {updateTransform(entities[i], delta);}
Web Engine uses bitECS for data-oriented storage. Components are stored in flat TypedArrays for cache-friendly iteration:
// Components are backed by TypedArraysconst Transform = defineComponent({x: Types.f32, // Float32Arrayy: Types.f32,z: Types.f32,rotX: Types.f32,rotY: Types.f32,rotZ: Types.f32,rotW: Types.f32,});// Fast iteration over packed dataconst entities = query(world, [Transform]);for (let i = 0; i < entities.length; i++) {const eid = entities[i];// Direct array access - no object allocationTransform.x[eid] += deltaX;Transform.y[eid] += deltaY;Transform.z[eid] += deltaZ;}
Load assets progressively to reduce initial memory footprint:
// Enable asset streaming in system configinitSystems({enableAssetStreaming: true,enableComputeCulling: true,});// Assets are loaded/unloaded based on camera distance// Distant objects use LOD meshes or are culled entirely
import { frameProfiler } from "@web-engine-dev/core";// Get recent frame statisticsconst stats = frameProfiler.getStatistics();console.log(`Average frame time: ${stats.averageFrameTime.toFixed(2)}ms`);console.log(`95th percentile: ${stats.p95FrameTime.toFixed(2)}ms`);console.log(`99th percentile: ${stats.p99FrameTime.toFixed(2)}ms`);// Get slowest systemsstats.slowestSystems.forEach(sys => {console.log(`${sys.name}: avg=${sys.avgTime.toFixed(2)}ms max=${sys.maxTime.toFixed(2)}ms`);});// Get phase breakdownObject.entries(stats.averagePhaseTimes).forEach(([phase, time]) => {console.log(`${phase}: ${time.toFixed(2)}ms`);});
import { PerformanceMonitor } from "@web-engine-dev/logging";import { logger } from "./logger";const perfMonitor = new PerformanceMonitor(logger);// In your game loopfunction gameLoop(timestamp: number) {perfMonitor.beginFrame(timestamp);perfMonitor.beginUpdate();// ... update logicperfMonitor.endUpdate();perfMonitor.beginRender();// ... renderingperfMonitor.endRender();perfMonitor.endFrame();requestAnimationFrame(gameLoop);}// Get performance snapshotconst snapshot = perfMonitor.getSnapshot();console.log(`FPS: ${snapshot.fps.avg.toFixed(1)}`);console.log(`Frame time: ${snapshot.frameTime.avg.toFixed(2)}ms`);
Use Chrome DevTools Performance panel to identify bottlenecks:
Memory Profiler
Use the Memory tab in DevTools to take heap snapshots. Look for unexpected object retention and memory leaks. The Allocation Timeline can show you exactly when allocations occur.
Key metrics to monitor in production:
Target: < 16.67ms for 60 FPS. Monitor 95th and 99th percentiles.
Watch for steady growth (memory leaks). Aim for stable sawtooth pattern.
Target: < 100 draw calls per frame. Use instancing and batching.
Target: < 500k triangles on screen. Use LOD and culling.
Performance Checklist
// 1. Pre-allocate outside loopsconst _tempVec3 = new THREE.Vector3();const _tempQuat = new THREE.Quaternion();function updateSystem(entities: number[]) {for (let i = 0; i < entities.length; i++) {// Reuse pre-allocated objects_tempVec3.set(x, y, z);}}// 2. Use TypedArrays for large datasetsconst positions = new Float32Array(10000 * 3);const velocities = new Float32Array(10000 * 3);// 3. Batch operations// ❌ BAD - Updates each object individuallyobjects.forEach(obj => scene.add(obj));// ✅ GOOD - Single batch updateconst group = new THREE.Group();objects.forEach(obj => group.add(obj));scene.add(group);// 4. Defer expensive operationslet frameCount = 0;function update() {frameCount++;// Only run expensive operations every N framesif (frameCount % 10 === 0) {updateNavMesh();}if (frameCount % 30 === 0) {rebuildSpatialIndex();}}