Game engines reach the browser by compiling a whole native engine to WebAssembly. Unity 2,152 's Web build target (Exporting from Unity to WebGL) and Godot 15,706 's Web export (Godot and PlayCanvas) both work that way, on WebGL 2 with a Wasm runtime, loader script and data pack; Unity offers WebGPU as an option, Godot does not support WebGPU, and threaded Godot builds need cross-origin isolation headers. Unreal Engine 133 has no browser export at all: HTML5 support left the engine during Unreal Engine 4, and the community-supported extension stops at 4.24.3. Epic's route to the web is Pixel Streaming, which runs the application on a server GPU and streams its frames and audio to the browser over WebRTC, so every visitor costs a GPU instance.
| Route | Runs where | First load | Page integration |
|---|---|---|---|
| Babylon.js 89,603 , Three.js 30,815 , PlayCanvas 62,178 | Visitor's GPU, JavaScript | Hundreds of KB to a few MB | DOM, CSS, SEO, React 7,897 |
| Unity or Godot web build | Visitor's GPU, WebAssembly | Engine runtime plus data | A canvas in an iframe or div |
| Unreal Pixel Streaming | Server GPU, video to the browser | A video stream | A video element |
Choose an engine export when the project already lives in that engine, for a game ported to the web or a simulation shared with desktop builds, and its team knows C# or GDScript better than JavaScript. For a shop page like BookNest's, whose store is part of a website with search, accounts and checkout, a JavaScript engine keeps the scene a first-class part of the page, loads faster and can be indexed and made accessible around it. None of the three engines was installed here, so this section rests on their documentation.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 360px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.5, 9, new BABYLON.Vector3(0, 1.4, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const label = (mesh, value, offsetY = 0, color = 'white') => {
const block = new BABYLON.GUI.TextBlock('', value);
Object.assign(block, { color, fontSize: 11, resizeToFit: true, outlineWidth: color === 'white' ? 2 : 0, outlineColor: '#333' });
ui.addControl(block);
block.linkWithMesh(mesh);
block.linkOffsetY = offsetY;
};
const box = (name, x, y, width, hex) => {
const mesh = BABYLON.MeshBuilder.CreateBox(name, { width, height: 0.6, depth: 0.9 }, scene);
mesh.position.set(x, y, 0);
mesh.material = new BABYLON.StandardMaterial(name, scene);
mesh.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
return mesh;
};
// One lane per route: where the work runs, what travels to the browser
const ROUTES = [
['Babylon.js, Three.js, PlayCanvas', 'JavaScript on the visitor\'s GPU', 'KB to a few MB of JS', '#1f5f8b', 'page: DOM, CSS, SEO'],
['Unity or Godot web build', 'WebAssembly engine on the visitor\'s GPU', 'engine runtime + data pack', '#3f7d3a', 'a canvas in the page'],
['Unreal Pixel Streaming', 'a server GPU per visitor', 'a WebRTC video stream', '#b5452f', 'a video element'],
];
const packets = [];
ROUTES.forEach(([name, runs, travels, hex, page], i) => {
const y = (2 - i) * 1.4; // one lane per row, top to bottom
const source = box('source' + i, -3.4, y, 2.2, i === 2 ? '#555555' : '#9e9e9e');
label(source, i === 2 ? 'server GPU' : 'web server', 0);
const browser = box('browser' + i, 3.4, y, 2.2, hex);
label(browser, page, 0);
const lane = BABYLON.MeshBuilder.CreateBox('lane', { width: 4.6, height: 0.04, depth: 0.1 }, scene);
lane.position.set(0, y - 0.2, 0);
const anchor = new BABYLON.TransformNode('a', scene);
anchor.position.set(0, y + 0.3, 0);
label(anchor, `${name}: ${travels}`, -14, '#222');
label(anchor, runs, 4, '#555');
const packet = BABYLON.MeshBuilder.CreateBox('packet', { size: i === 2 ? 0.18 : 0.26 }, scene);
packet.material = new BABYLON.StandardMaterial('p' + i, scene);
packet.material.emissiveColor = BABYLON.Color3.FromHexString(hex);
packets.push([packet, y, i]);
});
scene.onBeforeRenderObservable.add(() => {
const t = performance.now() / 1000;
for (const [packet, y, i] of packets) {
// the JS and Wasm routes download once, then run locally; streaming never stops
const phase = i === 2 ? (t * 1.5) % 1 : Math.min((t % 6) / 2, 1);
packet.position.set(-2.2 + 4.4 * phase, y - 0.05, 0);
packet.isVisible = i === 2 || (t % 6) < 2;
}
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>