Skip to main content
  1. Blog posts/

Optimize Size of Godot Releases

Godot Wasm
Table of Contents

How to reduce godot export templates from 64MB to 2.4MB for simple web games. Includes a bit of cheating though.

How do games get exported from godot?
#

The Godot engine exports projects using templates. These templates are pre-built binaries of the engine itself. Your scripts, assets, config, native extensions are merely loaded and interpreted by the exported template. This is good news for you, as you don’t need to recompile the engine every time you want to release your game (or even test it).

However this also means that godot can’t know during the compilation of the engine what parts of the engine you will need. For example, if you create a single-player retro 2D game, chances are slimm you will need the 3d renderer, it’s supporting shader pipelines, the network stack or maybe even audio. This means there might be quite some things in the pre-built templates that can be optimized away.

Baseline
#

The official download page contains the officially built export templates. That’s a zip file full of executables for all sorts of targets and build types. You’ll find linux, windows, macos, android, web and so on.

Since we are interested in the web build, let’s pick that one and compare it to a linux one:

  • linux_release.x86_64: 64MB
  • web release:
     7.2K godot.audio.worklet.js
     4.5K godot.html
     378K godot.js
      971 godot.offline.html
     5.6K godot.service.worker.js
      33M godot.wasm
      294 godot.worker.js
    

Is this enough?
#

For a native build, this might already be fine. I would not go down the rabbit hole of opmizing for binary size for native linux binary if it has 64MB. Given that assets come on top, this might be only a tiny part of the final size.

For the web though, the story might be different: The Binary has to be transferred, parsed and kept in browser memory. On mobile devices, this leads to significant delays on loading, espcially if you need to transfer it over mobile broadband.

Reading the Docs
#

If you encounter a problem in a piece of software, one option is to read the docs (it’s unheard of, I know). The godot docs have an article about how to optimize your builds for size.

Before we do any of the steps mentioned there, let’s do a plain build to see if everything is working. For preparation, cloning the repo and selecting a branch is nearly enough. We need to have a few dependencies installed but then invoking scons according to the docs is all we need:

scons platform=web target=template_release

This gives us a bunch of files in the ./bin/ folder of the repo

  28K godot.web.template_release.wasm32.engine.js
 349K godot.web.template_release.wasm32.js
  32M godot.web.template_release.wasm32.wasm
  294 godot.web.template_release.wasm32.worker.js
 377K godot.web.template_release.wasm32.wrapped.js
 7.5M godot.web.template_release.wasm32.zip
 4.0K .web_zip

The zip file contains the renamed files around it ready to be supplied to the editor. the 32M are already very slightly less than the original 33M, probably due to some slightly different configuration.

Let’s have a closer look at the build output at this moment:

➜  godot git:(4.3-stable) ✗ scons platform=web target=template_release
scons: Reading SConscript files ...
Auto-detected 24 CPU cores available for build parallelism. Using 23 cores by default. You can override it with the -j argument.
Building for platform "web", architecture "wasm32", target "template_release".
Checking for C header file mntent.h... (cached) yes
scons: done reading SConscript files.
scons: Building targets ...
[  0%] Generating core/object/gdvirtual.gen.inc ...
[  0%] Generating core/disabled_classes.gen.h ..
...
[100%] Linking Program bin/godot.web.template_release.wasm32.js ...
[100%] em++: warning: -pthread + ALLOW_MEMORY_GROWTH may run non-wasm code slowly, see https://github.com/WebAssembly/design/issues/1271 [-Wpthreads-mem-growth]
cache:INFO: generating system asset: symbol_lists/4856b429d43f8435debfcc958dc04f9eb5b8b182.json... (this will be cached in "/home/marco/.emscripten_cache/symbol_lists/4856b429d43f8435debfcc958dc04f9eb5b8b182.json" for subsequent builds)
cache:INFO:  - ok
[100%] Creating 'bin/godot.web.template_release.wasm32.wrapped.js'
...
[100%] scons: done building targets.
[Time elapsed: 00:04:30.21]

Let’s see, platform, architecture and target are picked up as expected. Emscripted outputs a warning regarding speed that we currently don’t care about, as well as something about symbol_lists that are cached, which might be interesting later.

So let’s do what we have been told in the docs and optimize for size.

LTO
#

First, we append lto=full to the scons command and do a complete rebuild of everything.

➜  godot git:(4.3-stable) ✗ scons platform=web target=template_release lto=full
scons: Reading SConscript files ...
Auto-detected 24 CPU cores available for build parallelism. Using 23 cores by default. You can override it with the -j argument.
Building for platform "web", architecture "wasm32", target "template_release".
Using LTO: full
Checking for C header file mntent.h... yes
scons: done reading SConscript files.
scons: Building targets ...
[  0%] Generating core/object/gdvirtual.gen.inc ...
....
[100%] Install file: "bin/godot.web.template_release.wasm32.wrapped.js" as "bin/.web_zip/godot.js"
[100%] Archiving bin/godot.web.template_release.wasm32.zip ...
[100%] scons: done building targets.
[Time elapsed: 00:09:45.33]

The Using LTO: full shows scons picked up our desired flag and the Time elapsed shows that it did some work for sure. Let’s see the result: 33M Spicy, it spent 5 minutes making the file even larger than before? Awesome…

Let’s not use lto for now.

Optimize for size
#

There is an option optimize=size, that reads awesome. Let’s see if it lives up to the expectations.

Four minutes later, we know it doesn’t, it’s still 32MB. If one were to read the docs thoroughly, one could have saved these four minutes, since the docs state that for web targets, this is the default.

Turning things off
#

I don’t need many of the features for my small 2D game, so let’s start turning things off. The docs recommend 3D rendering and the advanced text server.

scons platform=web target=template_release optimize=size module_text_server_adv_enabled=no module_text_server_fb_enabled=yes disable_3d=yes

The compilation time already went down: [Time elapsed: 00:03:24.86], so maybe the size did too? Indeed, with 25MB, we saved a chunk of data.

Disabling the advanced gui (disable_advanced_gui=yes) could save us one more megabyte, but I do need that functionality.

Before we go into disabling modules, let’s simplify the command line a bit. Scons supports using a custom.py, that we can configure these things in. The above translated looks like this:

platform = "web"
optimize = "size"
disable_3d = "yes"
module_text_server_adv_enabled = "no"
module_text_server_fb_enabled = "no"

Note: The target=template_release param can’t be part of the custom.py for some reason, so it has to be specified on the command line.

Using module_mono_enabled = "no", gets us down to 24MB. Doing this by hand is tedious.

Using the awesome generator, we can create the contents of this file.

Let’s go ahead and disable everything we can and rebuild once again. After compilation, we are met with a 19MB wasm file

With every module disabled, the engine would probably not be able to render anything but that’s not the point at this moment.

More disables
#

With scons --help, we can get a list of all flags available. So let’s update the list of our modules. Further, there is a whole host of options that sound very much not like being useful on the web, like wayland and alsa. If they are used in a web export, I don’t know, but off they go.

Disabling everything brings us down to 15MB.

Wasm tools
#

Agressive optimization with wasm-opt yields 23MB for the one with only 3D disabled and 13MB for the one with everything disabled.

Compression
#

Testing brotli, zopfli and plain old gzip, we can greatly compress the wasm file:

2.4M min_brot.wasm.gz
3.4M min_gzip.wasm.gz
3.2M min_zopf.wasm.gz

Compression using brotli did yield the smalles file size, hence the greatest gains. Since the output is equally “good” (all of them are lossless), there’s no reason not to use it.

Conclusion
#

Stripping down the engine to your needs is important to achieve a reasonable binary size. Compared to all the other steps, compressing the wasm files yielded the largest gain for the least amount of work. However, compression alone would not be enough: The larger the original binary, the more work the browser has to perform, this is especially noticeable on low power mobile devices.