obOB STUDIO
← Back to blog
Godot

Godot Web Export Size: 37 MB of Engine, 0.7 MB of Game

6 min readolivers-racers

In a Godot web export almost none of the size is your game. Mine is 37.68 MB of engine .wasm against a 0.70 MB .pck, so optimising assets moves under 2% of the download. Check your own split first, then check whether your host is actually compressing the .wasm. Mine wasn't, because CloudFront only compresses objects under 10 MB and the engine blob is nearly four times that.

the numbers first

Every thread about Godot web exports being too big skips straight to the advice. Strip your unused assets. Lower your import quality. Run wasm-opt. Compile a custom engine build. All perfectly good suggestions, and all offered before anyone has said what the files actually weigh, which feels a bit like being handed a diet plan by someone who hasn't asked what you eat.

So here's mine, laid out honestly. A real shipped 3D game. Fifteen maps, cars, boats, planes, the lot:

  • index.wasm: 37.68 MB. This is the Godot engine.
  • index.pck: 0.70 MB. This is my entire game.
  • index.js: 0.27 MB. The loader.
  • everything else (icons, HTML, audio worklets, manifest, service worker), 0.10 MB combined.

The game is the .pck. Fifteen maps, every vehicle, every texture and sound and script I have written over months. 0.70 MB. Just over 1.7% of the download.

The other 97% is index.wasm, which is the Godot engine. I ship three games off this domain, and I checked all three: the engine blob is byte-for-byte identical in every one. Same SHA-256, same 37.68 MB. It would be near enough the same file in your game too, assuming the same Godot version and export template.

That's worth sitting with, because it means the thing dominating my download isn't mine, isn't unique, and isn't going to change when I improve my game.

It also isn't free once it lands. A tab holding a decompressed WebAssembly engine plus a live 3D scene is exactly the kind of tab iOS Safari starts taking memory back from. Which is its own problem, and the reason I had to teach the page to recover from a lost WebGL context.

what this changes

If you're staring at a 38 MB export and rolling your sleeves up to optimise assets, check your own .pck first. Mine could vanish entirely (every texture, every sound, every line of code) and the download would shrink by under 2%. I could delete my whole game and barely dent it, which is a sentence I've had to sit with.

So every hour I might have spent lovingly recompressing textures would have been an hour spent on the wrong file. Good to know before the hour, ideally.

That reframes the problem completely. There are only really three levers on the engine blob:

Shrink it. wasm-opt -Oz from Binaryen, or compiling the engine from source with the features you don't use switched off. This is the one lever here I haven't personally benchmarked, so take it as received wisdom rather than a measurement. But a custom engine build is clearly a genuine project, and you're re-doing it on every engine update.

Cache it. It's the same bytes on every visit and across every game you ship from one origin. A service worker plus a long cache lifetime makes it a first-visit cost rather than an every-visit cost. Godot's web export can generate the service worker for you.

Compress it in transit. WebAssembly compresses extremely well. This is the cheapest lever by a distance, it costs you nothing at runtime, and it's the one I'd quietly got wrong.

the .wasm was being served uncompressed

I host the game on S3 behind CloudFront, and CloudFront has compression enabled. I checked the distribution config myself. "Compress": true, right there in writing. So I ticked that box in my head and went off to think about something else, the way you do.

Then, much later, I actually asked for the file. curl, real GET, explicit Accept-Encoding:

curl -s -o /dev/null -D - -H "Accept-Encoding: br, gzip" -w "WIRE: %{size_download}\n" https://obstudio.org/games/olivers-racers/index.wasm
HTTP/1.1 200 OK
Content-Length: 39509339
WIRE: 39509339

No Content-Encoding header. 39,509,339 bytes requested, 39,509,339 bytes delivered. Not a single byte saved anywhere along the way. Every player on a phone had been politely downloading the entire uncompressed engine, and nobody complained, because of course they didn't. It worked, it was just slow, and "slow" is the kind of thing people quietly forgive you for and then never come back.

Measured 5 August 2026. This is fixed now. If you curl that URL today you'll get `Content-Encoding: br` and about 8 MB, because of the change at the end of this post. The numbers above are what it looked like before.

The same request against index.js, sitting in the same folder on the same distribution:

HTTP/1.1 200 OK
Content-Encoding: br

Brotli, no problem. So compression is on and working. It just wasn't happening for the file where it was worth 31 MB of saving, and it was happening for the file where it was worth a couple of hundred KB.

two different reasons, and I got the first one wrong

My first theory was content type. Turning compression "on" at a CDN means on for the MIME types it recognises, and I assumed application/wasm wasn't one of them. I wrote that down confidently. It's wrong, and I'm leaving the correction in rather than quietly editing it out, because the way I got caught is the interesting part.

I ran an experiment that looked decisive: a 114 KB .pck, served as binary/octet-stream, nowhere near any size ceiling.

HTTP/1.1 200 OK
Content-Type: binary/octet-stream
Content-Length: 114548
WIRE: 114548

Uncompressed. So, I concluded, it isn't the size. It's the type. Neat, tidy, and a textbook case of proving a thing about one file and quietly assuming it applies to another.

Because CloudFront's documented list of compressible types does include application/wasm. It does not include binary/octet-stream. And separately, it only compresses objects between 1,000 and 10,000,000 bytes.

So the two files fail for two entirely different reasons:

  • The .pck is blocked by its content type. My experiment was right about that one.
  • The .wasm is blocked by its size. At 39,509,339 bytes it is nearly four times over the cap, and its content type was never the problem at all.

The fix is the same either way, and for the .wasm it isn't merely advisable but mandatory: a 39.5 MB object will never be CDN-compressed on the fly no matter what you set its type to. But my stated reason was half wrong, and the experiment I was so pleased with is what hid it. I tested the small file because it was convenient, then transferred the conclusion to the big one without checking whether anything else had changed. Both files were uncompressed, so the theory looked confirmed.

Which is a very easy thing to never notice, because the site is fast, the game works, and nothing anywhere reports an error.

what to do about it

Since you can't talk the CDN into compressing those types, compress them yourself before they ever get uploaded. Brotli the .wasm and .pck at build time, upload the compressed bytes, and set Content-Encoding: br on the object so the browser knows to inflate it. On S3 that's metadata on the object, so it belongs in your deploy script next to the upload, not in a console somewhere you'll forget.

One real caveat, and it's the reason to think for a second before doing it: an object with Content-Encoding: br baked into its metadata is served that way to everyone, whether or not that client said it understood brotli. There's no negotiation left. You've decided. For a WebAssembly game that's a bet I'm comfortable with, since brotli-over-HTTPS and WebAssembly arrived in roughly the same browser generation, so anything modern enough to run your export can inflate it. I haven't tested that against a support matrix, mind. It is not a thing to do casually to files with a wider audience.

For what it's worth, here's what the change actually bought, measured on the deploy: 39,509,339 bytes down to 7,995,810. A 79.8% cut, about 31 MB per cold visitor, on a file I could not otherwise have shrunk at all.

check yours

The whole diagnosis is one command, and the answer is the presence or absence of a single header:

curl -s -o /dev/null -D - -H "Accept-Encoding: br, gzip" https://yourgame.example/index.wasm

Use a GET, not a HEAD. I tried to be clever and avoid downloading 39 MB, ran a HEAD instead, and got a beautifully confident false negative. No Content-Encoding even on the JavaScript file that a real GET proves is brotli'd. So for about ten minutes I believed something that simply wasn't true, purely because I didn't want to wait for a download. There's a lesson in there and I don't love it.

If there's no Content-Encoding in that response, the engine blob is going out raw, and that's your single biggest win available for the least work. Worth more than anything you'll do to your textures.

ob

Written by Oliver

I build browser games and simulations on my own, everything here runs in a tab, with no installer and no account. The biggest is Oliver's Racers: procedural circuits in Godot 4, online multiplayer relayed by a Raspberry Pi in my room, and an Android build. Almost nothing here is imported artwork; the cars, trees and grandstands are built out of boxes and cylinders in code at load time.

More about me · See the projects