[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

Unity Project Settings, Player, Publishing Settings. Compression Format is Brotli, Decompression Fallback is on, Enable Exceptions is Explicitly Thrown Exceptions Only, Initial Memory Size is 512MB, Memory Growth Mode is Geometric and Maximum Memory Size is 2048MB

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 Unity Inspector showing a normal map's import settings. On the Default tab, Max Size is 1024, Compression is Normal Quality, Use Crunch Compression is checked and Compressor Quality is 60

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

Unity Project Settings, Player, the Optimization section inside Other Settings. Strip Engine Code is checked and Managed Stripping Level is set to High

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

Unity's Project window. Under Assets, the folders Resources and StreamingAssets are outlined in red

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

An FBX's import settings in Unity's Inspector, on the Model tab. Mesh Compression and Read/Write are outlined in red, as is the Animation tab at the top

  • 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

The Unity Package Manager window. In Project is selected in the left list, outlined in red, and the right side lists the packages this project holds, grouped by Asset Store and Unity

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 Analytics
  • Advertisement
  • In-App Purchasing
  • Visual Scripting
  • Input 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"

The WebAssembly Language Features section inside Unity's Publishing Settings. Enable Exceptions, outlined in red, is set to Explicitly Thrown Exceptions Only

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.

Unity's Enable Exceptions dropdown, open. It lists None, Explicitly Thrown Exceptions Only (checked), Full Without Stacktrace and Full With Stacktrace

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.

PoseMirror while loading: a blue progress bar about a third of the way across a black background, with Loading... 30% underneath

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 browser

Related (English):

Related (Japanese):

References

← Back to portfolio TOP