Shipping a Godot Web Export
This page answers one task: a game made in Godot 4 should run in the browser — on your own site, itch.io or a web portal — loading quickly and running smoothly, without the hosting problems that make many web exports fail on the first try.
Prerequisites
- [ ] Godot 4.3 or newer, with the Web export templates installed (Editor → Manage Export Templates).
- [ ] A project that uses the Compatibility renderer (required for the web) or can switch to it.
- [ ] Control over the hosting headers, or a host such as itch.io that sets them for you.
What a Godot web export contains
Godot compiles its engine to WebAssembly with Emscripten. A web export consists of the engine (game.wasm, several to tens of megabytes), a JavaScript
loader (game.js), your project data packed into game.pck, an index.html shell, and optionally audio worklet files and icons. On load, the browser
downloads the engine and the pack, compiles the engine, mounts the pack in Emscripten’s virtual file system, and starts the main loop driven by
requestAnimationFrame. Rendering goes through WebGL 2 using the Compatibility renderer; the Forward+ and Mobile renderers are not available on the web.
The biggest decision is threading. Godot 4.0–4.2 web exports required threads, and therefore SharedArrayBuffer and cross-origin isolation headers,
which many hosts did not set. Since 4.3, exports default to a single-threaded build that works on any static host, with threads as an option for
projects that need them.
Step 1 — create the export preset
In Project → Export, add a Web preset. Key settings:
- Thread Support: off for the single-threaded build (default), on for the threaded build.
- VRAM Texture Compression: enable For Desktop (S3TC/BPTC) and For Mobile (ETC2/ASTC) if you target both; Godot includes the formats each device supports.
- Export With Debug: off for release.
- Head Include / custom HTML shell: optional, for branding and a loading screen.
Export to an empty directory, and keep index.html, .js, .wasm, .pck and any .worklet.js files together.
Step 2 — serve it with the right headers
Every host must serve .wasm as application/wasm so the engine can compile while downloading. Threaded builds additionally need cross-origin
isolation:
Content-Type: application/wasm (for .wasm)
Cross-Origin-Opener-Policy: same-origin (threaded builds only)
Cross-Origin-Embedder-Policy: require-corp (threaded builds only)
For local testing, Godot’s editor can run a web build in the browser with a built-in server that sets these headers (the “Remote Debug” web button), and the Godot repository provides a small Python server script. On itch.io, enable “SharedArrayBuffer support” in the project’s embed options for threaded builds. The header set and its pitfalls are covered in configuring COOP and COEP headers for SharedArrayBuffer.
Step 3 — compress and cache the large files
The engine .wasm compresses very well — often from 35–40 MB to 8–10 MB with Brotli — and the .pck compresses depending on its assets. Precompress
both and configure the server to send Content-Encoding: br (or gzip), or rely on a CDN that compresses automatically. Set long cache lifetimes with
versioned file names, so returning players load from cache:
brotli -q 11 -k game.wasm game.pck
Because the .wasm is the same for every game exported with a given Godot version and settings, keeping its URL stable across your game’s updates lets
browsers reuse the cached engine when only the .pck changes.
Step 4 — reduce download size and load time
The fastest wins come from your project data. Compress textures with VRAM compression rather than shipping large PNGs; use Ogg Vorbis for music and short
WAVs only for small effects; exclude editor-only resources and unused assets with export filters. For the engine, a custom export template built with
SCons can disable modules you do not use — 3D, physics engines, GDScript features, advanced text server — and with optimize=size and LTO, custom templates
often drop the compressed engine by a third or more. Show a meaningful loading screen with progress; the default shell displays download progress, which
can be styled through a custom HTML template.
Step 5 — test on real devices and browsers
Test on Chrome, Firefox and Safari on desktop, and on Safari for iOS and Chrome for Android, on a mid-range phone. Common findings: memory limits on iOS
causing reloads for large projects, audio that starts muted until a user gesture (browsers require one; add a “click to start” screen), fullscreen and
pointer lock behaving differently per browser, and frame rates on phones well below desktop. Godot’s OS.has_feature("web"), "web_android" and
"web_ios" let the game adjust settings — resolution scale, particle counts, shadow quality — for web and for mobile web specifically.
Profiling performance in the browser
When a web build runs slower than the desktop build, measure before tuning. Godot’s built-in profiler and monitors work in debug web exports, and the browser’s Performance panel shows where each frame’s time goes: long stretches inside the engine’s Wasm functions point at game logic or physics, while time in WebGL calls and the GPU process points at rendering. Typical web-specific costs are draw calls — WebGL has higher per-call overhead than native APIs, so batching sprites and reducing material switches pays off more — and garbage-collected allocations in GDScript that trigger pauses. Lower the render resolution with the viewport’s scaling options on mobile, where fill rate is limited, and cap the frame rate on battery-powered devices.
Integrating the game with the page
A web export is a canvas inside a page, and it can talk to that page. Godot’s JavaScriptBridge singleton calls JavaScript from GDScript — reading URL
parameters, posting scores to a leaderboard API, opening a share dialog — and exposes GDScript callbacks to JavaScript through create_callback. Use it
sparingly and keep the contract small, because every call crosses from the game into the page and back. Persistent saves use Godot’s user:// path,
which the web build maps to IndexedDB; flush important saves explicitly and warn players that clearing site data deletes them, or offer cloud saves
through your own backend. If the game is embedded in a portal that runs it in an iframe, check the portal’s sandbox attributes: some block storage,
pointer lock or fullscreen unless specific allow flags are set. And handle visibility: when the tab is hidden, browsers throttle
requestAnimationFrame, so pause gameplay and audio on NOTIFICATION_APPLICATION_FOCUS_OUT rather than letting the game run in slow motion.
Expected output
The single-threaded export loads from a plain static host with no special headers, transfers about 11 MB on first visit and 2 MB on later visits after a content update, reaches the main menu in about 4 s on a fast connection, and runs at 60 frames per second on desktop and 40–60 on a mid-range phone.
Gotchas
- Threaded build on a host without isolation headers. It fails to start. Use the single-threaded build or set the headers.
- Wrong MIME type for
.wasm. Streaming compilation fails and loading slows. Serveapplication/wasm. - Forward+ renderer. Not available on the web. Switch to Compatibility.
- Audio blocked until interaction. Add a start screen that requires a click or tap.
- Huge
.pckfiles. Every asset in the pack downloads before the game starts. Filter unused resources from exports. - iOS memory limits. Large exports may reload the tab. Reduce texture sizes and test on iPhones.
Performance note
For a small 2D game, the default engine .wasm was 36 MB raw and 9.1 MB with Brotli; a custom template with 3D and unused modules disabled was 18 MB raw and
4.6 MB compressed. Time to main menu on a 50 Mbps connection dropped from 4.1 s to 2.6 s.
Frequently Asked Questions
Can I use C# in a Godot web export? Not with Godot 4 at the time of writing; C# projects cannot export to the web. GDScript and GDExtension (with web builds) are supported.
Does WebGPU support arrive in Godot web exports? Work is ongoing; current exports use WebGL 2 through the Compatibility renderer.
How do I make the game fill the browser window? Use the export’s canvas resize policy and stretch settings, and a custom HTML shell that sizes the canvas to the viewport.
Can I save games in the browser?
Yes — user:// is backed by IndexedDB in web exports. No permission prompt is needed, but saves are lost if the user clears site data, so offer an
export or cloud save for long games.
Related
- Porting a C game loop to Emscripten — what the engine does under the hood.
- Handling input and audio in a Wasm game — browser input and audio rules.
- Detecting cross-origin isolation at runtime — choosing the threaded build.
- Caching Wasm with a service worker — offline play and repeat visits.
← Back to Graphics, Games & Simulation