This short post by Defold people about "empty project build size" comparison between Defold, Unity and Godot (twitter, mastodon) sparked my interest.
It is curious that Godot builds seem to be larger than Unity? Would not have expected that! Anyway.
A way more extensive Unity "web" build comparison and analysics is over at https://github.com/JohannesDeml/UnityWebGL-LoadingTest but here are just my short notes in trying out Unity 6 (6.0.23 - Oct 2024).
Create a project using default ("3D", Universal Render Pipeline) template, do nothing just switch to "Web" platform and make a build: 10.7MB (3.7MB data, 6.9MB code). Takes about 6 minutes to finish the build.
Look at (uncompressed) asset sizes that are printed in the Editor.log during the build (note: it's been an issue for years that some of what goes into the build files is not reported for some reason), but anyway. The largest (uncompressed) data contributors are:
- 2.7MB: unity logo/splash texture (!). I remember years ago we tried to keep it small since "this goes into all builds". Apparently not anymore.
- 2.7MB: URP FilmGrain textures (10 textures, 256KB each).
- 0.5MB: "unity_builtin_extra" - various "built-in" assets that are included into the build if something needs/uses them. Again, it's known for years that it would be more useful to report details on what got included, but here we are.
- 0.4MB: URP blue noise textures (7 textures, 64KB each).
- 0.3MB: URP anti-aliasing (SMAA) AreaTex texture.
- 0.2MB: URP UberPost shader.
Looks like various things, mostly related to URP post-processing, are "always included" into the build, even if you don't explicitly use them. The 3+ MB above is just "film grain" textures, "blue noise" textures, anti-aliasing texture, plus a bunch of shaders and so on.
Not terribly large, but curious things: 80KB is URP "Runtime Debugging" truetype font (PerfectDOSVGA437.ttf), plus 16KB is another runtime debugging font (DebugFont.tga). There's also 60KB of URP "DebugUIbitField.prefab". All of these sound like some sort of "debug overlay/visualization" thingy, that is for some reason included into the build (even if I'm making a non-Development build!).
There's 3KB of Assets/Resources/PerformanceTestRunInfo.json
and while it is tiny, I wonder why it is there at all, and what it does
contain (there's no asset like that anywhere in the project or packages; it somehow gets generated during build time apparently).
Anyway, at this point sounds like URP has not really paid much attention to minimizing the build sizes, so let's try our good old friend, the built-in render pipeline (BiRP).
Create a project using "2D + BiRP" template that is an option in Unity Hub. Again switch platform to "Web" (unity hangs at "compiling scripts: backend" state with zero CPU utilization; kill it, restart, now works), make a build. 7.7MB (1.5MB data, 6.1MB code), takes almost 4 minutes to build.
Ok, so BiRP saves about 2MB worth of (Brotli-compressed, which is default) data size, good; Brotli-compressed code size is smaller too. Out of uncompressed assets reported in Editor.log, the same 2.7MB for splash screen / logo is still there, the rest is peanuts.
With Unity 6 you can turn off the default splash/logo even in the free ("personal") license, so do that.
Also, while the project feels like it is "empty" and contains nothing, that is not actually true; it contains several dozen packages. I think I'm not gonna need: Visual Scripting, Timeline, Version Control, Performance Testing API (whatever that is), Multiplayer Center, Visual Studio Editor, Test Framework. Turn those off in the package manager window.
There's also Burst, Mathematics, Collections, as well as a bunch of "2D" related packages like Aseprite Importer and so on. Confusingly enough, the package manager UI does not allow me to remove them, since they are part of "2D feature". It only allows me to "unlock" said packages, but what does it do I have no idea. After unlocking them, they still can't be removed. You know what, just remove the whole "2D" feature. Afterall, basic "2D" (sprites, materials, 2D physics etc.) are still built-in and available.
Build is 6.9MB (1.1MB data, 5.6MB code), 3 minute build time.
In the build profiles window, "code optimization" setting defaults to "shorter build time". Switch that to "disk size", build. Build size increases, lol (7.1MB), and takes over 10 minutes to build. Ok that does not sound terribly useful! Forget about it, change code optimization setting back to default.
Hidden deep inside Player Settings (which is organized like a major mess), there are several settings that might affect build size:
- Player Settings -> Other Settings -> Optimization -> Managed Stripping Level. Change to "High". This is what allows to remove "not used by the game" parts of the "engine", I think.
- Player Settings -> Other Settings -> Configuration -> ILCPP Code Generation. Change to "Faster (smaller) builds". Various tooltips there talk about "scripting backends" which is confusing for all of these platforms where there's only one scripting backend.
- Player Settings -> Publishing Settings -> Web Assembly Features -> Enable Exceptions. Change to "None".
- Player Settings -> Publishing Settings -> Web Assembly Features -> Use WebAssembly.Table. Turn on. Might lose some old browsers support, but the tooltip indicates that it might save some code size.
- Graphics Settings -> Shader Settings -> Video. Change to "Don't include".
Build: 4.8MB (0.9MB data, 3.8MB code), two minutes to build.
Now, for some reason there's still a lot of engine code that does not get removed. We have removed almost all packages from the project... but not all of them! You know what, let's remove "Input System" and "Unity UI", just to see.
Build: 2.3MB! (0.4MB data, 1.8MB code), one minute to build.
Now we're talking! And again, it is hard to say why for example the data file got twice smaller; the Editor.log build size report does not contain useful information. But the engine code size got way smaller. I did not check whether it is the input system package, or the Unity UI package that "drags in" a ton of engine code.
In the package manager UI left sidebar there is a section called "Built-in" with no explanations. And it lists a bunch of things that do not have descriptions either. These are not "packages" but rather "engine modules" (IMHO a largely misguided and/or unfinished effort from years ago). Let's try to turn off all the ones we think we don't need: Accessibility, AI, Cloth, Director, Physics (keep Physics 2D), Screen Capture, Terrain, Terrain Physics, UIElements, Umbra, Unity Analytics, Vehicles, Video, VR, Wind, XR.
Build: size unchanged. Turning off these "built-in modules" does not do much/anything! It might help to avoid accidentally adding a dependency to them during development, but otherwise if you have code stripping on already (see sections above), it won't reduce file sizes.
Previously (with "managed code stripping" player setting at default), build profile code optimization setting for "disk size" was not useful (larger code, and way longer build times). But maybe now it would be better?
Code optimization set to "disk size": 2.1MB (0.4MB data, 1.6MB code), build time 70 seconds.
Code optimization set to "disk size with LTO": 2.0MB (0.4MB data, 1.5MB code), build time 60 seconds.
That's it!
This is an unfortunate "practically a red herring" item: the logo texture is DXT block compressed, quite large, and mostly transparent, so it is indeed inefficient and bloated in uncompressed form. However, Unity does LZ compression on assets, and the .data files get even Brotli compressed on top of that, so the mostly identical DXT blocks do get efficiently compressed.
It is possible to get the logo removed from the build altogether, but iirc that requires disabling both logo and splash screen separately, a bit of a UX footgun. However by doing that, one can see that the network transfer size shouldn't grow by more than a few dozen KBs given the compression. (as the logo was mostly transparent, hence got efficiently compressed anyway)
Shorter build time maps to Clang/LLVM
-O2
, Disk Size is Clang/LLVM-Os
. There is also-Oz
in Clang, which one can explicitly try to add in emscriptenLinkerArgs. In the past it was reported that -Oz was "like -Os, but just wastes more time", although more recently I've got reports of "-Oz does produce considerably smaller builds than -Os".Apart from that, we do routinely see that enabling LTO does reduce build sizes, although it also has an effect of exploding build times, since interprocedural optimization is very costly.
Instead of these two settings, recommend enabling "WebAssembly 2023" setting instead. This enables native Wasm Exceptions and implies WebAssembly.Table, and also removes uses of "dynCalls", which removes code size further, while retaining exception support if necessary.
This is the general difficulty with C# type hierarchy, virtual functions and reflection: C# language does not Dead Code Eliminate as naturally as C/C++ does, so IL2CPP code stripping pass is needed to try to infer what code is used and what isn't. The design decisions that went into the designing the C# language are the single biggest reason that Unity .wasm code is so large that it is.
From that perspective we do see in general that removing C# packages from project that should be effectively unused can still reduce generated code size even further.