Skip to content

Instantly share code, notes, and snippets.

@aras-p
Last active November 14, 2024 09:14
Show Gist options
  • Save aras-p/740c2d4f9977ce92b7de72b1394dd365 to your computer and use it in GitHub Desktop.
Save aras-p/740c2d4f9977ce92b7de72b1394dd365 to your computer and use it in GitHub Desktop.
Unity 6 "empty" web build file sizes

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).

Default (3D, URP) template

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).

2D, Built-in Render Pipeline template

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.

Turn off splash/logo, remove packages we don't need

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.

Try out "disk size" code optimization

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.

Increase code stripping level

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.

Remove Input System, Unity UI

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.

Try to remove built-in engine modules

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.

Change code optimization setting back

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!

@AGulev
Copy link

AGulev commented Oct 22, 2024

Yes, we did test "as is." In Defold, it's also possible to optimize the build size.
For example, in this case, I excluded a few modules (physics, rig and model components, the ability to load PNGs, etc.):

CleanShot 2024-10-22 at 10 58 58@2x

Also, I removed gamepad mappings
As result 817Kb :
https://ahul.eu/html5_empty_appmanifest_1.9.4/
CleanShot 2024-10-22 at 11 05 54@2x

@juj
Copy link

juj commented Oct 22, 2024

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.

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)

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)

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.

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.

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.

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.

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.

@aras-p
Copy link
Author

aras-p commented Oct 22, 2024

This is an unfortunate "practically a red herring" item

That's true; while uncompressed it is 2.7MB, compressed it contributes something like 20KB to the data brotli file size. I was just puzzled at the uncompressed size. These days probably does not matter much, "back in the day" extra 3MB of runtime memory usage especially on constrained platforms (like mobile at the time) would have been a big deal.

@JohannesDeml
Copy link

Very nice write-up and also thanks for linking to my repo! One small other improvement you can do is set the compiler configuration to master, other than that my setup is very similar (but I'm trying to have a bit more capabilities in there that a lot of games have such as physics).
C++ Compiler Configuration

I also tried looking into build sizes for godot at some point and I think they are also a bit high in the defold comparison. For godot however I found the startup times to be a lot longer than for Unity, a metric that I find quite a bit more interesting than build sizes, but which is a lot harder to collect.

@AGulev
Copy link

AGulev commented Oct 22, 2024

I also tried looking into build sizes for godot at some point and I think they are also a bit high in the defold comparison.

We just downloaded the engine and built the release build of HTML5, nothing fancy (we did the same for all three engines).

I found the startup times to be a lot longer than for Unity, a metric that I find quite a bit more interesting than build sizes, but which is a lot harder to collect.

I fully agree with you—it's more important to optimize a high-level metric like the time from clicking a link to the "ready for interaction" state in the game, rather than focusing solely on a low-level metric like build size. It’s possible to optimize size using aggressive compression, but that can significantly increase decompression time and prevent the build from using WebAssembly (WASM) streaming. Additionally, larger WASM files take more time to initialize, among other issues. This is particularly noticeable on low-cost phones, which make up a significant percentage of web game players (I make games for Poki and other platforms, so I experiment with this a lot and pay close attention to web optimization).

@juj
Copy link

juj commented Oct 22, 2024

SpiderMonkey blog had a nice post about how Firefox optimized Wasm compilation times recently: https://spidermonkey.dev/blog/2024/10/16/75x-faster-optimizing-the-ion-compiler-backend.html

It’s possible to optimize size using aggressive compression, but that can significantly increase decompression time and prevent the build from using WebAssembly (WASM) streaming

Using Brotli + Streamed Wasm Instantiation is definitely the way to go. If using a custom compressor instead of Brotli, by designing it to operate in a streaming fashion (which one should, if running on the web at page load time), then one can use Streamed Fetches to pipeline it with Streamed Wasm Instantiation.

@alikamarainen
Copy link

alikamarainen commented Oct 23, 2024

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.

https://github.com/Unity-Technologies/UnityDataTools can be used to examine the .data file. However, I'm quite confident that if you dig into the file, you will find out that pretty much all of the savings come from the decrease in the global-metadata.dat, an IL2CPP metadata file. We're currently in the middle of investigation, but it would appear the managed code stripping is unable to remove the majority of dead engine code that is pulled from the regular C# packages (opposed to modules) and their dependencies, for example, System.Xml and System.Linq. This dead engine code also increases the size of the IL2CPP metadata file significantly.

@MPurscheUnity
Copy link

Regarding "Try to remove built-in engine modules"

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.

I think that is because the build system is already relatively clever and will not included built-in modules if they are not actually used by assets, scenes or scripts in the project. That should be controlled by the "Player Settings -> Other Settings -> Optimization -> Strip Engine Code" setting. This can sometimes get tripped up if there are assets in the project(or packages) that will not end up in the build. So it still makes sense to disable the modules you don't plan to use for those cases.

@andypoly
Copy link

Can you not show the largest Compressed data elements in "Default (3D, URP) template" because as mentioned this is what matters.
Large images are a red herring, it depends post compression what size things are. Then we can truly see what makes up that 3.7mb data

@aras-p
Copy link
Author

aras-p commented Oct 28, 2024

Can you not show the largest Compressed data elements in "Default (3D, URP) template" because as mentioned this is what matters

I would, if Unity reported that! It only reports uncompressed size during the build tho :(

@anonymous2585
Copy link

  • 2.7MB: URP FilmGrain textures (10 textures, 256KB each).
  • 0.4MB: URP blue noise textures (7 textures, 64KB each).

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.

I was able to remove them from my builds. You have to uncheck "Post-processing->Enabled" in all your "Universal Renderer Data" assets, and make sure the Post Process Data scriptable object is not referenced anymore anywhere in the assets. I think it was with Unity 2023.1.16f1.

@matthew-rister
Copy link

It's worth noting that many of these recommendations are included in updated Unity 6 documentation:

@RubyStevens
Copy link

Thanks for sharing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment