Unity WebGL Performance Optimisation: Real Build Results

Set a performance budget, inspect what Unity ships, and focus on measured constraints. This Unity 6 project cut build time from 1:03:41 to 0:03:57 before the asset pass began.

Unity WebGL browser performance dashboard connected to speed, memory, and asset compression tiles

Unity WebGL performance problems usually arrive in stages. A prototype loads acceptably on a development machine, then production assets, UI, analytics and more complex scenes push download size, memory use and frame time beyond the limits of the target browser.

The best time to set limits is before art production and feature work scale. The best time to improve an existing build is after measuring what it contains.

This guide combines both parts of that process. It starts with a practical performance budget, then shows what we found during an optimisation pass on a production Unity 6 training simulator. The project included truck-mounted crane rigs, terrain and dozens of first-party 3D models. The build times and file sizes below came from that project.

Set the performance budget first

A performance budget gives design, art and engineering a shared set of constraints. It does not need to predict every production issue. It needs to define what the team will measure, the target device range and the point at which a change requires review.

First-load budget

For WebGL, first load is part of the product design. A training module used inside a corporate LMS may tolerate a longer initial load when the session lasts 30 minutes. A public interactive experience may need to start much faster.

Include the loader, compressed build files, first-scene assets, fonts, analytics and the surrounding site in the budget. Measure on representative networks and devices. Office fibre and a development workstation can hide delays that appear immediately on managed laptops, public Wi-Fi and mobile connections.

Memory budget

Browser, device and operating-system limits all affect available memory. Define a target for the first scene and the heaviest expected session. Track texture memory, meshes, audio, loaded scenes, AssetBundles, Addressables and managed allocations.

Test lower-spec target devices during production. A memory issue found during final QA usually requires more disruptive asset and scene changes than one found during an early content milestone.

Frame-time budget

Tie the frame-rate target to the use case. A precise simulation may require a consistent 60 frames per second. A product viewer may be acceptable at 30 if input remains responsive.

Profile the browser build as well as the Unity project. Canvas size, browser GPU behaviour, page scripts, high-DPI rendering and background-tab throttling can all affect the result. A clean Editor profile does not prove that the deployed build will perform well.

Asset budget

Define maximum texture sizes, mesh budgets per view, audio limits, lightmap rules and streaming groups before content production expands. Treat exceptions as measured decisions rather than letting every asset establish a new baseline.

XGameDev applies this planning across Unity development and interactive HTML5 content projects. The browser is a deployment platform with its own constraints, so those constraints need to be present in the project plan.

Measure the build before changing it

Source file size is a poor proxy for what Unity ships. Import settings, platform overrides and compression determine the packed contribution.

This project stored build reports under Assets/BuildReports/. Its report data included the packed byte count and source asset GUID for each entry:

PackedAssets:
  m_Contents:
  - fileID: 1
    classID: 21
    packedSize: 3572
    offset: 61440
    sourceAssetGUID: c9adb1f...
    buildTimeAssetPath: Assets/...

We compared packedSize with buildTimeAssetPath instead of sorting source folders by disk usage. Some third-party terrain textures exceeded 100 MB as source .tif files, but their import settings already capped the shipped versions. A set of smaller first-party prop textures was the larger build contributor.

Use the report produced by your build pipeline, or capture the same information through Unity's BuildReport API. Confirm which assets reached the player before editing them.

A decompression setting added about an hour to each build

These builds used the same project and hardware. Settings were changed one at a time.

Build Profile Time Size Result
A Local development 1:03:41 150.5 MB .unityweb output
B Local development after the change 0:03:57 183.8 MB .data.br and .wasm.br output
C Release with LTO 0:09:58 149.2 MB Production build

The difference between builds A and B was Decompression Fallback under WebGL Publishing Settings. It was enabled in the development profile.

When the option is enabled, Unity includes JavaScript decompression code in the loader and uses .unityweb extensions. This supports hosting environments that cannot send the correct Content-Encoding response header. Unity documents the fallback as a larger, less efficient loading path.

In this project, generating that path also dominated the Postprocess built player step. The deployment already used a controlled origin with Brotli headers, and its upload scripts expected .data.br, .wasm.br and .framework.js.br files. Disabling the fallback reduced the build from 1:03:41 to 0:03:57 and restored the expected filenames.

This timing is a project result, not a general estimate for every Unity version and machine. If a Web build spends an unexpected amount of time in post-processing, compare a build with this setting disabled. Leave it enabled when the hosting environment cannot provide the required compression headers.

Check stale build processes before changing settings

We also encountered this build-system error:

Internal build system error. Read the full binlog without getting a BuildFinishedMessage.
Backend did not emit BuildFinishedMessage. ... Unable to rename state file
'Library/Bee/TundraBuildState.state' => '...state.map'

In that case, a linker process left behind by a crashed or force-closed Editor session still held the state file. Ending that process released the lock.

For a slow link that has not failed, compare the linker's accumulated CPU time with wall-clock time before stopping it. Similar values indicate active work. A large IL2CPP input can keep a non-LTO link busy for a long time.

Reduce the IL2CPP input carefully

The project used the default Minimal managed stripping level. Its linked IL2CPP input reached 399 MB. Moving to Medium stripping removed more unreachable managed code and reduced the work passed to later build steps.

Medium stripping also increases the chance that reflection-based code will need a [Preserve] attribute or link.xml entry. We tested the player paths that use reflection after changing it. Treat stripping as a build-size change with runtime test requirements.

The package audit also found com.unity.visualscripting installed without any graphs in the project. The only code reference was a leftover using Unity.VisualScripting; in editor-only third-party demo code. Removing that dependency reduced generated code in this project.

Check Packages/manifest.json against actual runtime and editor use. Reflection-heavy and code-generation packages deserve particular attention because their contribution may be larger than their visible use in scenes.

We also enabled Strip Unused Mesh Components. Unity can then remove vertex channels that assigned shaders do not use, such as tangents, vertex colours or secondary UV data. Check any objects that switch shaders at runtime because a later shader may expect a channel that was absent during the build.

For inherited builds with several overlapping problems, a structured Unity technical audit helps separate build-pipeline, asset and runtime work.

Texture resolution was the largest asset opportunity

The build report identified 126 first-party textures that packed at about 2.4 MB each, close to 300 MB before transport compression. They covered crane parts, control panels and small mechanical props such as bolts, handles and levers.

One representative texture used a 2048 maximum size, ASTC 6x6 compression and mip maps. Its packed size matched the expected storage for those settings. The compression format was behaving as configured; the resolution was too high for the asset's on-screen size.

We reduced the maximum size from 2048 to 1024 for 113 reviewed texture imports. That cuts pixel area to one quarter and put the estimated packed reduction near 225 MB. We inspected representative assets at their normal camera distances before applying the batch change. Third-party terrain assets kept their existing settings because their shipped sizes were already controlled.

Calculate the expected storage from resolution, texture format and mip settings before changing formats. If the packed size matches the calculation, resolution is usually the first variable to test.

Find unused assets by GUID

Unity scenes, prefabs and assets refer to one another by GUID. Searching for a folder name does not establish whether its contents are referenced.

For four versions of the project's crane models, we read each GUID from its .meta file and searched the serialised .prefab, .unity and .asset files that could reference it.

Model folder Current references Decision
Legacy version 1 None found Removed, 21 MB of source assets
Legacy version 3 None found Removed, 92.8 MB across 194 files
Current version Main scene and active prefab Kept
Alternate truck variant Active prefab Kept

The current reference scan found 113 MB of obsolete source content. An earlier build report showed about 24 MB of packed textures from one of those legacy versions before its last reference was removed. The combination of report data and the current GUID scan established both what had shipped and what was now safe to delete.

A GUID search still needs care. References can exist in code, Addressables configuration, Resources loading conventions, custom databases or generated content. Include every reference path used by the project before deleting an asset.

Review model imports per use case

All 56 first-party and vehicle FBX files had Mesh Compression set to Off. Medium compression reduced their build contribution, with no visible change during review at the distances used by this simulator.

We did not batch-disable Read/Write Enabled. Several models belonged to a rope and cable system that could read mesh data at runtime. Disabling it can reduce memory by removing the CPU-side mesh copy, but the change needs a per-model runtime test. A visual inspection alone may not expose the failure.

Treat blurry WebGL text as a rendering issue

The simulator's TextMeshPro labels, buttons and HUD text showed visible fringing in windowed WebGL mode. Three settings compounded the problem.

Regenerate the SDF atlas for its display range

The InterRegular and InterBold TMP assets used sampling sizes around 164 and 152 points with 5 pixels of padding. Regenerating them at 90 points with 9 pixels of padding and SDFAA hinting produced cleaner text at the sizes used by this interface.

These values are specific to this font and UI. Inspect the atlas and its intended display range rather than copying the numbers into another project.

Match the browser container to the authored aspect ratio

The Canvas Scaler used a 1920 by 1080 reference resolution, while the Web template fixed #unity-container at 1024 by 768. The 4:3 container forced a 16:9 interface to scale to about 0.53 of its intended width in windowed mode.

The template was changed to preserve 16:9 while fitting within the viewport:

#unity-container {
  width: max(320px, min(1920px, 100vw, calc((100vh - 56px) * 16 / 9)));
  aspect-ratio: 16 / 9;
}

Put a ceiling on high-DPI rendering

Unity can match the WebGL render target to the canvas's CSS size and the display's device pixel ratio. A device pixel ratio of 3 creates nine times as many render-target pixels as a ratio of 1. A ratio of 4 creates sixteen times as many.

The project limited the value in its loader configuration:

devicePixelRatio: Math.min(window.devicePixelRatio || 1, 2),

That retained high-DPI rendering up to 2 while preventing the highest-density devices from multiplying pixel work further. The right ceiling depends on the UI, GPU range and visual target, so profile representative devices at 1, 1.5 and 2 before fixing the production value.

Addressables changes when bytes load

The main 3D scene moved into Unity Addressables so the menu could start from a smaller initial payload. The world then loaded when the trainee started a scenario.

Addressables can defer and split downloads. It does not reduce the underlying asset data. The heavier scene still needs asset optimisation, a loading transition and a memory plan.

Two Web-specific details affected this build:

  • Web AssetBundles must use LZ4 or remain uncompressed. Unity's Web platform does not support LZMA AssetBundles.
  • Bundle compression and HTTP transport compression are separate. An LZ4 bundle can also be served with gzip or Brotli transport compression when the server is configured for it.

Check every Addressables group, including generated or built-in groups, after the first content build. Then load each remote group in the target browser and inspect the console and network response headers.

Hosting is part of the performance budget

A Brotli-compressed Unity build needs the server to return Content-Encoding: br for its compressed build files when JavaScript decompression fallback is disabled. Incorrect headers can prevent the browser from loading the player.

Caching policy then determines how much returning visitors download:

  • Build assets at versioned or content-hashed URLs can use a long cache lifetime with immutable.
  • index.html and mutable content catalogues should revalidate so they can point at the current release.

Validate the deployed response, not only the server configuration file. Use the browser network panel or a header request to confirm Content-Encoding, Content-Type, Cache-Control and the requested filename for each build asset.

Teams that need the Unity build and its surrounding Web delivery handled together can use XGameDev's interactive HTML5 development service.

Add the numbers to each review

Track the same measures for milestone and release builds:

  1. Initial compressed download size.
  2. Time until the menu or first useful interaction.
  3. Peak memory in the first scene and heaviest session.
  4. Frame time on each target device class.
  5. Main player, WebAssembly and Addressables sizes.
  6. Build duration by pipeline step.
  7. Largest packed asset contributors.

Store the report and profiler captures with the build record. When a number changes sharply, inspect the changes made since the previous measurement. XGameDev's GameDevOps service covers the related build and delivery automation needed to make these checks repeatable.

The largest improvements in this project came from evidence already available in the build pipeline. One publishing setting explained most of the build-time problem. Packed asset data identified the texture group worth reviewing. GUID searches separated obsolete models from active ones. Browser testing connected a text-quality problem to canvas sizing and high-DPI rendering.

Set the budget early, measure the deployed build and change one variable at a time. That produces results the team can verify and keeps optimisation work tied to the devices and delivery environment that matter.

Need Help with a Unity WebGL Build?

Contact Us