[Unity] How to Shrink a WebGL Build for the Web
Published: May 5, 2026 (Updated September 25, 2026)
For: Developers shipping a Unity WebGL build as a web app and fighting load times
Introduction
A Unity project runs as a web app the moment you build it for WebGL. The problem is weight: a 3D project is heavy, so users wait, and on some devices the load fails outright.
This article shares what I actually do to keep PoseMirror — a web app built on a Unity project — light.
Environment
| Item | Value |
|---|---|
| Unity | 6000.0.58f2 |
| Render pipeline | URP |
| Scripting backend | IL2CPP (fixed for WebGL) |
| Hosting | Firebase Hosting |
What a WebGL build is made of
A Unity WebGL build produces four files under Build/.
Table 1: a build with compression off (Compression Format: Disabled) — sizes from PoseMirror
| File | Size | Share |
|---|---|---|
poseMirror.wasm |
35.88MB | 70.6% |
poseMirror.data |
14.37MB | 28.3% |
poseMirror.framework.js |
0.42MB | 0.8% |
poseMirror.loader.js |
0.11MB | 0.2% |
| Total | 50.79MB | — |
wasm and data are 99% of it, so shrinking those two files is the whole game.
- wasm (35.88MB) — your C# run through IL2CPP into C++ and compiled down
- data (14.37MB) — the assets in your scenes (textures, meshes, animation)
The three steps below take this 50.79MB down to 14.20MB.
Every number compared here is the total transfer size of the four files in Build/ (bytes on the wire, after Brotli).
Step 1: set Compression Format to Brotli (-72.0%)
This writes the build output itself in a compressed form, and it is by far the highest-impact setting.
Where: Player Settings → Publishing Settings → Compression Format
At the top of that panel, change Compression Format from Disabled to Brotli.
Tips: what Brotli is
A general-purpose compression format from Google that every major browser can decompress natively. Unity WebGL offers Disabled / Gzip / Brotli, and for the same content Brotli is generally smaller than Gzip.
The downside is that compressing takes a long time, which stretches your build. Using Gzip or Disabled while developing and Brotli only for the public build works well.
Note that serving compressed files also requires a server-side header saying the bytes are already compressed (covered in the publishing article).
| File | Uncompressed | Brotli | Reduction |
|---|---|---|---|
| wasm | 35.88MB | 6.65MB | -81.5% |
| data | 14.37MB | 7.36MB | -48.8% |
| framework.js | 0.42MB | 0.07MB | -83.3% |
| loader.js | 0.11MB | 0.11MB (left uncompressed) | — |
| Total | 50.79MB | 14.20MB | -72.0% |
Result: 50.79MB → 14.20MB (-72.0%).
Step 2: set textures to Max Size 1024 + Crunch compression (-19.7%)
This lowers the resolution ceiling of your images and compresses them again on top of the GPU format. It cuts the data file directly.
Where:
Select a texture, then Inspector → Default tab → Max Size 1024, Crunch compression on

Tips: what Crunch compression is
Textures are converted to a GPU format such as DXT at build time. Crunch (crunch / crnlib) is a lossy extra compression applied on top of that. The shipped file stays Crunch-compressed, and the CPU turns it back into the GPU format at load time.
The costs are the quality loss that comes with being lossy, plus longer builds and extra CPU work while loading. Normal maps and smooth gradients pick up noise easily, so treat it as a quality trade-off and compare the actual screen before settling on a value.
| 2048, Crunch OFF | 1024, Crunch ON | Reduction | |
|---|---|---|---|
| data | 10.85MB | 7.36MB | -32.1% |
| Total | 17.68MB | 14.20MB | -19.7% |
Result: 17.68MB → 14.20MB (-19.7%).
With a single character on screen, 1024 is visually indistinguishable in practice. Check whether you are still shipping 2048.
Step 3: set Managed Stripping Level to High (-5.8%)
This cuts unused C# code and engine features out at build time. It shrinks wasm without touching your code.
Where: Player Settings → Other Settings → Optimization
Move Managed Stripping Level from Low to High, and tick Strip Engine Code right above it.
Result: 15.07MB → 14.20MB (-5.8%).
| Low | High | Reduction | |
|---|---|---|---|
| wasm | 7.24MB | 6.65MB | -8.2% |
| data | 7.64MB | 7.36MB | -3.6% |
| Total | 15.07MB | 14.20MB | -5.8% |
Uncompressed, the wasm goes 40.49MB → 35.88MB (-11.4%).
Other things worth doing
None of these match the three steps above, but they either help a little or quietly undo your work if you forget them.
Keep junk out of Resources and StreamingAssets
Everything under Resources/ ships whether anything references it or not. And StreamingAssets/ is copied in verbatim, uncompressed, so keep both free of files you don't need.
Where: the Project window, directly under Assets
Both are special folders identified by name, sitting directly under Assets/. If you don't have them, your project simply doesn't use them and there is nothing to check.
Mesh settings
Where: select an FBX (or other model) → Inspector → Model tab
- Mesh Compression: Medium–High (quantizes vertex data; the default is Off)
- Read/Write: OFF (on keeps a second copy in CPU memory, doubling it)
- Don't import animation clips you never play (exclude them in the Animation tab at the top)
Remove packages you don't use
Unused packages left in the Package Manager are still referenced, so step 3's stripping never touches them. Delete the ones you don't use.
Where: menu bar → Window → Package Manager → pick "In Project" in the left list
What you see here is what the project actually carries. Select one you don't use and press Remove at the bottom right.
These tend to be added automatically at project creation, so drop them unless you know you need them.
Unity AnalyticsAdvertisementIn-App PurchasingVisual ScriptingInput System(if you're still on the old Input Manager)
When in doubt, remove it and see whether the build still compiles. If it was genuinely in use you'll get a compile error, and you can add the package back from the Package Manager.
Lower the exception support level
This decides how much exception-handling machinery gets baked into the wasm. Higher levels make failures easier to trace and make the wasm fatter.
Where: Player Settings → Publishing Settings → Enable Exceptions under "WebAssembly Language Features"
It sits lower in the same panel you opened in step 1, right after Power Preference.
The dropdown offers four levels. PoseMirror uses the second one, Explicitly Thrown Exceptions Only.
| Option | What you get |
|---|---|
| None | No exception handling at all. Smallest, but behaviour is undefined once something throws |
| Explicitly Thrown Exceptions Only | Only exceptions you throw yourself (recommended) |
| Full Without Stacktrace | Runtime exceptions such as null references too, with no stack trace |
| Full With Stacktrace | The above plus line tables embedded in the wasm. The heaviest |
Full With Stacktrace is handy while hunting a bug, but too heavy to carry in a public build. Raise it while debugging and put it back before you ship.
Step 4: shorten the wait people feel
Once the bytes are as small as they'll go, what's left is the time people spend waiting.
Put the progress bar outside Unity, in HTML
You build the progress bar into the HTML that hosts Unity. This is what PoseMirror actually shows.
Unity's canvas stays black until it boots, so without this bar the page looks broken rather than busy.
The third argument of createUnityInstance is a progress callback, which is what fills it.
const overlay = document.getElementById('unity-loading-overlay');
const bar = document.getElementById('loading-bar');
const loadingText = document.getElementById('loading-text');
createUnityInstance(unityCanvas, config, (progress) => {
const pct = Math.round(progress * 100);
bar.style.width = pct + '%';
loadingText.textContent = `Loading... ${pct}%`;
}).then((instance) => {
overlay.style.opacity = '0';
setTimeout(() => { overlay.style.display = 'none'; }, 500);
}).catch((err) => {
// Never leave a white screen. Without this the user just sees a frozen page
loadingText.textContent = 'Loading failed. Please reload the page.';
loadingText.style.color = '#d9534f';
console.error('Unity load error:', err);
});
That .catch() matters more than it looks. When a load fails on memory pressure or a file mismatch, a progress bar frozen in place is the worst thing you can show.
Summary
- Measure the four files in
Build/first — wasm-heavy and data-heavy need different fixes - 1) Brotli (50.79→14.20MB / -72.0%) → 2) textures at 1024+Crunch (17.68→14.20MB / -19.7%) → 3) Stripping High (15.07→14.20MB / -5.8%), in that order
- data → texture Max Size and Crunch compression, and keep junk out of
Resources/ - Perceived speed → show progress and failures in HTML, because the screen is empty until Unity boots
PoseMirror runs on exactly this setup: a Unity WebGL app at 14.2MB over the wire. You can watch it load at PoseMirror.
14.2MB over the wire. Nothing to install.
See the Unity WebGL app this article optimizes, running on your own phone.
▶ Open the 3D pose tool in your browserRelated (English):
- Best free 3D pose reference web apps for artists (2026) — what the app in this article looks like next to other tools
Related (Japanese):
- 【Firebase】Unity WebGLを無料で公開する手順 — the deployment walkthrough
- 【Unity】WebGLでピンチズームが効かない原因と解決法 — touch handling on mobile







