How to Optimize Unity WebGL Builds for Faster Load Times (Unity 6)

Optimizing Unity WebGL build load times

Shipping a Unity game to the browser is a great way to reach players without an install step, but browser builds come with real constraints: everything has to download before it runs, and it all runs inside the browser’s sandbox. A build that loads instantly from your own machine can take agonizingly long over a real connection, or fail outright on a misconfigured server. This guide covers what actually moves the needle, in the order that matters: measure, compress (and serve it correctly), shrink code and assets, then defer what you can. In Unity 6 the platform is called Web in Build Profiles; everything here applies to “WebGL” builds in 2022 LTS too.

🛠 Curious how big your build will be?

Pick your project’s profile and compression settings to get a ballpark build size and load time before you optimize.

Try the WebGL Build Size Estimator →

Why Load Time Matters So Much

Unlike a native install, a browser build’s first impression is the loading bar. Every extra megabyte is extra seconds a player spends staring at a progress bar before they’ve seen anything of your game, and browser players leave fast. Web portals and game jams also judge you on it: a 15-second load on a phone is often the difference between a play and a closed tab.

Step 0: Measure Before You Optimize

Guessing wastes time. After any build, open the Editor log (Console window > ⋮ menu > Open Editor Log) and search for Build Report. You’ll find a size breakdown by category (Textures, Meshes, Sounds, Shaders, Scripts…) followed by a list of every asset in the build sorted by size. The top ten entries usually account for most of the download.

Then look at the files in your build’s Build folder:

File Contains Shrink it with
.data Scenes, assets, Resources Texture/audio settings, removing unused assets, Addressables
.wasm Your compiled C# plus engine code Code stripping, Code Optimization, removing packages
.framework.js JavaScript glue for the runtime Compression (it’s small already)
.loader.js The loader that fetches everything else Nothing to do; it’s tiny

Keep the numbers from this first build as your baseline, and re-check after each step below so you know what actually helped.

Step 1: Enable Compression, and Serve It Correctly

Go to Project Settings > Player > Web (WebGL) tab > Publishing Settings > Compression Format:

  • Brotli: the smallest files, and the best choice for release. Browsers only accept Brotli over HTTPS.
  • Gzip: slightly larger but faster to build and works over plain HTTP.
  • Disabled: only if your host or CDN compresses files on the fly for you.

Here’s the catch that breaks more Unity web builds than anything else: Unity pre-compresses the files (game.wasm.br, game.data.br…), so your web server must send them with a Content-Encoding header, plus the right Content-Type for .wasm. Without that, the browser receives compressed bytes it doesn’t know to decompress, and the game fails with “Unable to parse Build/…framework.js.br!”

Apache (most shared hosting). Put this in an .htaccess file in the build’s root folder. It requires mod_mime, which is almost always enabled:

<IfModule mod_mime.c>
  # Brotli-compressed builds
  RemoveType .br
  RemoveLanguage .br
  AddEncoding br .br
  AddType application/octet-stream .data.br
  AddType application/wasm .wasm.br
  AddType application/javascript .js.br
  AddType application/octet-stream .symbols.json.br

  # Gzip-compressed builds
  RemoveType .gz
  AddEncoding gzip .gz
  AddType application/octet-stream .data.gz
  AddType application/wasm .wasm.gz
  AddType application/javascript .js.gz
  AddType application/octet-stream .symbols.json.gz

  # Uncompressed builds: lets browsers stream-compile the WebAssembly
  AddType application/wasm .wasm
</IfModule>

Nginx. Add these inside your server block (the gzip versions are the same pattern with \.gz$ and Content-Encoding gzip):

location ~ .+\.(data|symbols\.json)\.br$ {
    gzip off;                       # the file is already compressed
    add_header Content-Encoding br;
    default_type application/octet-stream;
}
location ~ .+\.js\.br$ {
    gzip off;
    add_header Content-Encoding br;
    default_type application/javascript;
}
location ~ .+\.wasm\.br$ {
    gzip off;
    add_header Content-Encoding br;
    default_type application/wasm;
}

Can’t change server headers? That’s the case on GitHub Pages and many file hosts. Enable Decompression Fallback in Publishing Settings. Unity then names the files .unityweb and decompresses them in JavaScript. It works everywhere, at the cost of a bigger loader and slower startup, so use it only when you have to. Game portals such as itch.io document their own requirements; check them before uploading.

Verify it: open your deployed game, press F12, go to the Network tab, reload, and click the .wasm.br request. The response headers should show content-encoding: br and content-type: application/wasm.

Step 2: Shrink the Code (.wasm)

Web builds always use IL2CPP, which converts your C# and the engine code it needs into WebAssembly. Four settings control how big that gets:

  • Managed Stripping Level (Player > Other Settings > Optimization): set it to Medium or High to strip unused managed code. Keep Strip Engine Code enabled too.
  • Code Optimization (Build Profiles > Web in Unity 6; Build Settings in 2022): the default Shorter Build Time is right for iteration. For release builds, choose Disk Size with LTO for the smallest .wasm, or Runtime Speed with LTO if CPU performance matters more than download size. LTO builds take noticeably longer.
  • Enable Exceptions (Publishing Settings): keep the default Explicitly Thrown Exceptions Only, or None if you’re confident. The “Full” options add a lot of code and slow the game down, so use them only to debug a crash.
  • Unused packages: open Package Manager > In Project and remove what you don’t use. Templates often include packages like Visual Scripting or Timeline that a small web game never touches.

Higher stripping levels can remove code that’s only reached through reflection, such as JSON serializers, dependency injection or anything found by name at runtime. The symptom is a build-only MissingMethodException or data that deserializes as empty. Protect those types with [UnityEngine.Scripting.Preserve], or with a link.xml file anywhere under Assets:

<linker>
  <!-- keep every member of this type, even if nothing references it directly -->
  <assembly fullname="Assembly-CSharp">
    <type fullname="MyGame.SaveData" preserve="all"/>
  </assembly>
  <!-- keep a whole third-party assembly -->
  <assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>

Step 3: Compress and Right-Size Textures

Textures are usually the biggest part of .data. For each large texture in the Build Report:

  • Cap Max Size to what’s actually visible on screen. A 4K texture on a prop that covers 200 pixels is pure waste. Use the texture’s Web platform override tab so desktop and console builds keep their full size.
  • Pick the right compression format. DXT (BC) suits desktop browsers; ASTC or ETC2 suits mobile browsers. The Web Build Profile has a Texture Compression setting for the whole build.
  • Use Crunch compression for a smaller download at a small quality cost. It works best on large color textures and less well on normal maps and pixel art.
  • Disable Read/Write unless a script reads the pixels. When it’s on, Unity keeps a second copy of the texture in memory.
  • Turn off mipmaps for UI and 2D sprites that are always drawn at one size, and pack sprites into Sprite Atlases.
  • Check lightmaps. Baked lightmaps at high resolution are easy to overlook and often huge. Lower Lightmap Resolution or Max Lightmap Size for web builds.

Illustration of a speedometer showing fast load times for a Unity WebGL build

Step 4: Audit Audio

  • Music is often the single largest item in a web build’s report. Use a compressed format and lower the Quality slider until you can hear the difference, then go back one step.
  • Force To Mono for sound effects. Stereo rarely matters for a short footstep or click.
  • Override the sample rate (22,050 Hz is fine for most SFX) and trim silence from the start and end of clips.
  • Don’t preload rarely used clips. Untick Preload Audio Data on clips that only play in later levels, or move them into Addressables.

Step 5: Resources, Fonts and Shaders

  • Everything inside any Resources folder ships, whether or not a scene uses it. It’s the most common source of mystery megabytes. Move assets out of Resources unless you load them with Resources.Load.
  • Fonts: TextMeshPro font assets with huge character sets (especially full CJK fonts) are large. Generate static atlases with only the characters you need, or use dynamic fonts carefully.
  • Shader variants: in URP, turn off pipeline features you don’t use (extra shadow cascades, SSAO, additional lights), since each one multiplies the variants compiled into the build. The Build Report’s Shaders category tells you whether this matters for you.
  • Unused scenes and assets: only scenes in the Scene List (and what they reference) are included, so clean up the list. Deleting unused assets from the project doesn’t shrink the build unless something referenced them, but it makes the Build Report easier to read.

Step 6: Load Non-Critical Content Later with Addressables

If your game has content that isn’t needed at the start, such as later levels, optional cosmetics or a second game mode, don’t put it in the first download. With the Addressables package, that content becomes separate bundles, uploaded next to your build and downloaded only when requested:

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class LevelLoader : MonoBehaviour
{
    [SerializeField] AssetReference level2Prefab;   // drag an Addressable prefab here

    public void LoadLevel2()
    {
        level2Prefab.InstantiateAsync().Completed += handle =>
        {
            if (handle.Status != AsyncOperationStatus.Succeeded)
                Debug.LogError("Level 2 failed to load: " + handle.OperationException);
        };
    }
}

Use Addressables.LoadSceneAsync("Level2") the same way for whole scenes. If the bundles are hosted on a different domain or CDN, that server must send an Access-Control-Allow-Origin header, or the browser blocks the download with a CORS error.

Step 7: Faster Startup and Instant Repeat Visits

  • Data Caching (Publishing Settings, on by default) stores the .data file in the browser’s IndexedDB, so returning players skip most of the download.
  • Name Files As Hashes gives every file a content-based name. Pair it with a long cache header on your Build folder (Cache-Control: public, max-age=31536000, immutable): browsers cache aggressively, and a new build automatically gets new file names.
  • Make the first scene tiny. A lightweight bootstrap scene (logo, “Click to play”, progress bar) that loads the real game with SceneManager.LoadSceneAsync gets pixels on screen seconds sooner.
  • Show real progress. In a custom WebGL template, the loader reports progress from 0 to 1:
    createUnityInstance(canvas, config, (progress) => {
      progressBarFill.style.width = (100 * progress) + "%";
    }).then((unityInstance) => {
      loadingScreen.remove();
    });
  • Memory settings: leave Memory Growth Mode on Geometric. Raise Initial Memory Size only if profiling shows the heap growing repeatedly during load. Raise Maximum Memory Size (default 2 GB) only if you hit out-of-memory errors, and treat that as a sign to cut memory use, especially for mobile browsers.

Step 8: Runtime Performance in the Browser

Download size is only half the job. Once the game loads, it has to hold a frame rate on integrated laptop GPUs and phones. The biggest wins are fewer draw calls (static batching, SRP Batcher, combined meshes), cheap lighting (baked where possible, few real-time shadows) and simple post-processing. Leave Application.targetFrameRate at its default on Web so the browser paces frames; set a lower value only if you want to cap it (for example, 30 fps for battery-friendly mobile play). Unity 6 also ships an early-access WebGPU graphics backend. It’s worth experimenting with, but WebGL 2 remains the safe default for broad browser support.

📦 Draw calls dragging down browser frame rates?

Download size is only half of WebGL performance. Mesh Baker combines meshes and atlases materials to cut draw calls, which matters a lot on the low-end laptops and phones your players will run the game on.

See Mesh Baker and other tools in our Asset Store roundup →

Step 9: Test on Real Hosting, Not Just Locally

Build And Run uses Unity’s own local server, which sends the correct headers, so it hides exactly the problems your real host will have. Opening index.html straight from disk doesn’t work at all, and a plain python -m http.server doesn’t send Content-Encoding for compressed builds. Instead:

  1. Upload the build to the server you’ll actually use (a staging folder is fine).
  2. Open DevTools (F12) > Network, tick Disable cache, and set throttling to Fast 4G or Slow 4G.
  3. Reload and time how long until the game is interactive. Then untick Disable cache and reload again to check the repeat-visit time.
  4. Test on at least one real phone and in Safari as well as Chrome. Mobile browsers have tighter memory limits and are where builds fail first.

Troubleshooting

“Unable to parse Build/….framework.js.br! This can happen if build compression was enabled but web server hosting the content was misconfigured…”

The server isn’t sending Content-Encoding: br (or gzip). Add the Apache or Nginx configuration from Step 1, confirm the site is served over HTTPS for Brotli, or switch on Decompression Fallback if you can’t change headers.

“Incorrect response MIME type. Expected ‘application/wasm’”

The game usually still loads, but the browser falls back to a slower compile path. Add the application/wasm type for .wasm, .wasm.br and .wasm.gz as shown above.

“Out of memory. If you are the developer of this content, try allocating more memory to your WebGL build in the WebGL player settings.”

The heap hit its limit. Reduce what’s loaded at once (texture sizes, audio preloading, Addressables for later content), make sure Memory Growth Mode isn’t None, and only then consider raising Maximum Memory Size. On iOS Safari, the practical limit is well below the desktop maximum.

Works with Build And Run, blank page or errors on the real site

Almost always server headers (see above), a path problem (the Build folder renamed or not uploaded), or Brotli served over plain HTTP. The browser Console (F12) shows the actual error. Start there.

Addressables or asset bundles fail with a CORS error

The host serving the bundles must send Access-Control-Allow-Origin for your game’s domain. Either host the bundles on the same domain as the game, or add that header on the CDN.

FAQ

Brotli or Gzip for Unity web builds?

Brotli, if your site uses HTTPS and you can set response headers. It produces noticeably smaller files. Use Gzip if you’re stuck on HTTP, and Decompression Fallback only when you can’t set headers at all.

Why is my WebGL build so big when my game is small?

Check the Build Report. The usual culprits are uncapped texture sizes, uncompressed music, forgotten files in a Resources folder, large font atlases and unused packages. Build an empty scene with the same settings to see your engine-and-pipeline baseline (it’s a handful of megabytes compressed, more with URP or HDRP). Everything above that is your content, and the report shows exactly which content.

Do Unity web builds work on phones?

Unity 6 supports mobile browsers, but they have much less memory and GPU power than desktops. Use ASTC/ETC2 texture compression, keep memory use down, and test on real devices, especially iPhones in Safari.

Should I use WebGPU instead of WebGL?

Not for a release yet, unless you need compute shaders and can accept limited browser coverage. WebGL 2 runs almost everywhere today. Try WebGPU in a branch to see how it performs for your game.

Quick Optimization Checklist

  • Baseline recorded from the Build Report in the Editor log
  • Brotli (or Gzip) enabled, and Content-Encoding / application/wasm headers verified in DevTools
  • Managed Stripping Level Medium/High, link.xml for reflection-based code, tested in a real build
  • Release builds use Disk Size with LTO (or Runtime Speed with LTO)
  • Textures capped with Web overrides, compressed, Read/Write off
  • Audio compressed, SFX mono, rarely used clips not preloaded
  • Nothing unnecessary in Resources; unused packages removed
  • Later content moved to Addressables; Data Caching and hashed file names on
  • Tested on the real host with throttling, on desktop and a phone

Web optimization is mostly about discipline rather than one big trick. Compression, stripping, texture and audio sizing each shave off a percentage, and together they add up to a build that loads fast enough for players to stick around and give your game a chance.

Download size is only half the budget. Once the build loads, it still has to hit a frame rate inside a browser tab — our Performance Budget Calculator and performance budget guide cover the other half.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *