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.

Single-threaded versus threaded Godot web exports The single-threaded export runs on any static host without special headers and works in more browsers, with audio mixing on the main thread. The threaded export needs cross-origin isolation headers but can move audio and some engine work to other threads. single-threaded (default) no special headers works on any static host audio mixed on main thread start here threaded needs COOP + COEP headers SharedArrayBuffer required smoother audio under load when you control headers

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.

What a player downloads on first load A typical small Godot web export transfers the compressed engine, the project pack, the JavaScript loader and the HTML shell. The engine dominates on first load; on later loads only a changed pack needs downloading if the engine is cached. index.html + loader JS ≈ 0.3 MB game.wasm (engine) ≈ 36 MB raw, ≈ 9 MB Brotli game.pck (your project) size of your assets browser cache engine reused across updates

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. Serve application/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 .pck files. 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.

Compressed engine size, default versus custom template Megabytes transferred for the Godot web engine with the default export template and with a custom template that disables 3D and unused modules, both Brotli-compressed. MB transferred (Brotli) default template 9.1 MB custom template (2D only) 4.6 MB

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.

← Back to Graphics, Games & Simulation