Game Engine UI Rendering: How Games Achieve 120+ FPS
Related Articles:
- From HTML to Pixel: A Deep Dive into Browser Rendering - Learn how modern browsers use GPU acceleration
- From HTML to Pixel without GPU - Explore the pre-GPU era of browser rendering
While web browsers struggle to maintain 60 FPS, modern game engines routinely achieve 120+ FPS for complex UI systems. How do game engines accomplish this, and what can web developers learn from their approach?
The Fundamental Difference: Immediate Mode vs Retained Mode
Web Browsers: Retained Mode Rendering
Web browsers use retained mode rendering - they maintain a persistent representation of the UI:
Web Browser Rendering:┌─────────────────────────────────────┐│ DOM Tree (Persistent) ││ ├── HTML Elements ││ ├── CSS Styles ││ └── JavaScript State │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ Render Tree (Persistent) ││ ├── Computed Styles ││ ├── Layout Information ││ └── Paint Layers │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ GPU Rendering ││ ├── Layer Compositing ││ ├── Rasterization ││ └── Screen Output │└─────────────────────────────────────┘Characteristics:
- Persistent state: DOM/CSSOM trees maintained in memory
- Incremental updates: Only changed elements re-render
- Complex diffing: Browser must determine what changed
- Memory overhead: Large object graphs in memory
Game Engines: Immediate Mode Rendering
Game engines use immediate mode rendering - they redraw everything every frame:
Game Engine Rendering:┌─────────────────────────────────────┐│ Frame Start ││ ├── Clear Screen ││ ├── Process Input ││ └── Update Game State │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ Immediate Mode UI ││ ├── Draw Button (x, y, w, h) ││ ├── Draw Text (x, y, text) ││ ├── Draw Image (x, y, texture) ││ └── No State Management │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ Direct GPU Commands ││ ├── Vertex Buffers ││ ├── Shader Programs ││ └── Screen Output │└─────────────────────────────────────┘Characteristics:
- No persistent state: UI elements don’t exist between frames
- Direct rendering: Immediate GPU commands
- No diffing: Everything redrawn every frame
- Minimal memory: No UI object graphs
Performance Comparison: Web vs Game Engine
| Aspect | Web Browser | Game Engine |
|---|---|---|
| Rendering Mode | Retained Mode | Immediate Mode |
| State Management | Complex DOM/CSSOM | Minimal/None |
| Memory Usage | High (object graphs) | Low (direct rendering) |
| Update Overhead | Diffing + incremental | Full redraw |
| Typical FPS | 30-60 FPS | 120+ FPS |
| Latency | 16-33ms | 8ms or less |
How Game Engines Achieve High FPS
1. Direct GPU Communication
Game engines bypass the browser’s abstraction layers:
// Game Engine (Direct GPU)glBegin(GL_QUADS);glVertex2f(x, y);glVertex2f(x + width, y);glVertex2f(x + width, y + height);glVertex2f(x, y + height);glEnd();
// Web Browser (Multiple Layers)DOM → Render Tree → Layout → Paint → Composite → GPU2. Optimized Rendering Pipeline
Game engines use specialized rendering pipelines:
Game Engine Pipeline:┌─────────────────────────────────────┐│ Input Processing ││ ├── Mouse/Keyboard Events ││ └── Game State Updates │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ UI Drawing Loop ││ ├── Clear Frame Buffer ││ ├── Draw Background ││ ├── Draw UI Elements ││ └── Draw Overlays │└─────────────────────────────────────┘ ↓┌─────────────────────────────────────┐│ GPU Submission ││ ├── Batch Commands ││ ├── Minimize State Changes ││ └── Direct Memory Access │└─────────────────────────────────────┘3. Batched Rendering
Game engines batch similar rendering operations:
// Efficient batchingglBindTexture(GL_TEXTURE_2D, buttonTexture);for (int i = 0; i < buttonCount; i++) { // Draw all buttons in one batch drawButton(buttons[i]);}glBindTexture(GL_TEXTURE_2D, 0);4. Minimal State Changes
Game engines minimize GPU state changes:
// Optimized state managementglEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);// Draw all transparent elementsglDisable(GL_BLEND);// Draw all opaque elementsReal-World Example: Button Rendering
Web Browser Approach
<button class="game-button" onclick="handleClick()">Start Game</button>Rendering Steps:
- DOM Parsing: Create button element
- CSS Processing: Apply styles, compute layout
- Render Tree: Add to render tree
- Layout: Calculate position and size
- Paint: Create paint layers
- Composite: GPU compositing
- Event Handling: Attach event listeners
Memory Usage: ~2KB per button (DOM + CSSOM + Event handlers)
Game Engine Approach
// Immediate mode renderingvoid renderButton(float x, float y, float width, float height, const char* text) { // Direct GPU commands drawRectangle(x, y, width, height, buttonColor); drawText(x + padding, y + padding, text, textColor);}Rendering Steps:
- Direct GPU Call: Draw rectangle
- Direct GPU Call: Draw text
- No State Management: No persistent objects
Memory Usage: ~50 bytes per button (just parameters)
Advanced Game Engine Techniques
1. Command Buffers
Game engines pre-record rendering commands:
// Command buffer approachstruct RenderCommand { CommandType type; float x, y, width, height; Color color; Texture* texture;};
std::vector<RenderCommand> commandBuffer;
// Record commandscommandBuffer.push_back({DRAW_RECT, x, y, w, h, color, nullptr});
// Execute all commands in one batchexecuteCommandBuffer(commandBuffer);2. GPU Instancing
Render thousands of similar elements efficiently:
// Instance renderingglBindBuffer(GL_ARRAY_BUFFER, instanceBuffer);glDrawArraysInstanced(GL_TRIANGLES, 0, 6, instanceCount);3. Spatial Partitioning
Only render visible UI elements:
// Frustum culling for UIif (isInViewport(uiElement)) { renderUIElement(uiElement);}Web Technologies Adopting Game Engine Techniques
1. Canvas API
Web developers can use immediate mode rendering:
// Canvas immediate modeconst canvas = document.getElementById("gameCanvas");const ctx = canvas.getContext("2d");
function renderFrame() { // Clear everything ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw UI elements directly ctx.fillStyle = "#4CAF50"; ctx.fillRect(10, 10, 100, 40);
ctx.fillStyle = "white"; ctx.fillText("Start Game", 20, 35);
requestAnimationFrame(renderFrame);}2. WebGL
Direct GPU access in the browser:
// WebGL immediate modeconst gl = canvas.getContext("webgl");
function drawButton(x, y, width, height) { // Direct GPU commands gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.drawArrays(gl.TRIANGLES, 0, 6);}3. React Canvas
React’s experimental canvas renderer:
// React Canvas (experimental)import { Canvas } from "react-canvas";
function GameUI() { return ( <Canvas> <Button x={10} y={10} width={100} height={40}> Start Game </Button> </Canvas> );}Performance Benchmarks
Web Browser (Complex UI)
DOM Elements: 1000CSS Rules: 500JavaScript: 50KBMemory Usage: 50MBFPS: 30-45Latency: 22-33msGame Engine (Complex UI)
UI Elements: 1000Rendering Commands: 2000Memory Usage: 5MBFPS: 120+Latency: 8msKey Takeaways for Web Developers
1. Consider Canvas for High-Performance UI
- Use Canvas API for game-like interfaces
- Immediate mode rendering for complex animations
- Direct GPU access via WebGL
2. Minimize DOM Complexity
- Reduce DOM tree depth
- Use CSS transforms instead of layout changes
- Batch DOM updates
3. Optimize Rendering Pipeline
- Use
will-changefor GPU layers - Minimize paint operations
- Leverage compositor-only animations
4. Adopt Game Engine Patterns
- Immediate mode for real-time interfaces
- Command batching for similar operations
- Spatial culling for large lists
Future of Web UI Rendering
1. WebGPU
Next-generation GPU API for web:
// WebGPU (future)const gpu = navigator.gpu;const device = await gpu.requestAdapter().requestDevice();
// Direct GPU commandsconst commandEncoder = device.createCommandEncoder();const renderPass = commandEncoder.beginRenderPass();2. React Server Components
Reducing client-side rendering:
// Server Componentsasync function GameUI() { const gameState = await fetchGameState(); return <GameInterface state={gameState} />;}3. WebAssembly + Canvas
High-performance UI with WASM:
// WASM + Canvasconst wasmModule = await WebAssembly.instantiateStreaming( fetch("ui-renderer.wasm"));wasmModule.instance.exports.renderUI(canvasContext);Conclusion
Game engines achieve high FPS through:
- Immediate mode rendering (no persistent state)
- Direct GPU communication (minimal abstraction)
- Optimized batching (efficient command submission)
- Minimal memory overhead (no object graphs)
Web developers can adopt these techniques through:
- Canvas API for immediate mode rendering
- WebGL for direct GPU access
- Performance optimization of existing DOM-based UIs
- Emerging technologies like WebGPU and WASM
The gap between web and game engine performance is narrowing as web technologies evolve to support more efficient rendering patterns!
Curious ?
What if we could bring game engine performance to web applications? Let’s explore WebGPU and the future of high-performance web rendering…
Stay tuned /