Optimizing Unity WebGL: Separate Builds for Mobile Web and Desktop Web

Why Use Double Builds for Your Unity 3D Game

Texture Compatibility


One of the key reasons is asset compatibility. In particular, texture support differs significantly between Mobile Web (Android & iOS devices) and Desktop Web (macOS, Linux, Windows). To better understand why this matters, let’s take a closer look at the data:

 Desktop BrowsersiOS and Android browser
RGB Compressed ETC2NoYes
RGB Crunched ETCNoYes
RGBA Compressed ETC2NoYes
RGBA Crunched ETC2NoYes
RGB Compressed DXT1, also known as BC1Partial (*)No
RGB Crunched DXT1, also known as BC1Partial (*)No
RGBA Compressed DXT5, also known as BC3
Partial (*)  
No
RGBA Crunched DXT5, also known as BC3Partial (*)No

* With linear rendering on web browsers that do not support sRGB DXT, textures are decompressed to RGBA32 at load time.

DXT1/5 formats will work on mobile, but it will fall back to uncompressed RGBA consuming more RAM and increasing load times due to texture conversion.

Refer to the Unity documentation for more details: https://docs.unity3d.com/2021.3/Documentation/Manual/class-TextureImporterOverride.html

Asset Optimisation

Build separation makes it possible to deliver optimised assets tailored for mobile devices.

Mobile screens are extremely dense in terms of dpi (pixels per inch). This means a pixel shader on a small 5–6 inch display performs nearly the same amount of calculations as on a full 2K desktop monitor with a much weaker mobile GPU. On top of that, the CPU is significantly weaker and often limited to single-threaded performance.

That’s where double builds shine – it allow you to supply alternative assets and settings for mobile, such as:

  • Optimized shaders and materials
  • Adjusted shadow quality
  • Simplified lighting (e.g., fully baked with support for just 1–2 dynamic lights)
  • Models with reduced geometry (High-poly models are difficult to transform using the limited vertex units available on mobile GPUs, and the extra detail is unnecessary for such small displays)
  • Simplified, GPU-friendly or GPU-baked animations (Relevant for all Web builds. Skeletal animations that cannot be fully processed on the GPU quickly become a bottleneck.)

Behaviour and Code Optimisation

Script defines, code generation, and similar techniques let us deliver platform-specific code and behaviors without introducing performance penalties or unnecessary abstraction layers.

This approach makes it possible to:

  • Optimize input methods for different devices (touch, mouse, controller, etc.)
  • Adapt UI layouts for smaller form factors or specific interaction styles
  • Replace or simplify heavy routines with optimized ones

How to Double Build

Note: Here you can find my example project demonstrating build automatisation, Mobile/Desktop Web build separation, and Dockerization support: https://github.com/AndreiMaksimovich/Unity-Build-and-Test-Automation

Separated Builds

An example of this approach is available in the repository mentioned above.
The core idea is to generate separate builds for each required platform type – such as Mobile Web, Desktop Web, Mobile Tablets (touch screens, 9-inch+), etc, and then route the player to the appropriate build automatically.

Merged Build

The idea behind this method is to merge multiple builds into a single one, allowing us to select different data, code, and asset sources before the Unity player initializes programatically. Let’s explore how this can be achieved.

Let’s examine the default Unity Web template in Unity 6.2 – specifically, the index.html file between lines 61 and 81:

      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/{{{ LOADER_FILENAME }}}";
      var config = {
        arguments: [],
        dataUrl: buildUrl + "/{{{ DATA_FILENAME }}}",
        frameworkUrl: buildUrl + "/{{{ FRAMEWORK_FILENAME }}}",
#if USE_THREADS
        workerUrl: buildUrl + "/{{{ WORKER_FILENAME }}}",
#endif
#if USE_WASM
        codeUrl: buildUrl + "/{{{ CODE_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
        symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}",
#endif
        streamingAssetsUrl: "StreamingAssets",
        companyName: {{{ JSON.stringify(COMPANY_NAME) }}},
        productName: {{{ JSON.stringify(PRODUCT_NAME) }}},
        productVersion: {{{ JSON.stringify(PRODUCT_VERSION) }}},
        showBanner: unityShowBanner,
      };

What we’re looking at here is the configuration for initializing a Unity instance. The key point is that this configuration is scripted – meaning we can dynamically change the Data file URL, Wasm file URL, and Streaming Assets URL on the fly, effectively switching both the codebase and assets.

Now let’s take a look at the index.html file in a web build:

      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/Build.loader.js";
      var config = {
        arguments: [],
        dataUrl: buildUrl + "/Build.data.gz",
        frameworkUrl: buildUrl + "/Build.framework.js.gz",
        codeUrl: buildUrl + "/Build.wasm.gz",
        streamingAssetsUrl: "StreamingAssets",
        companyName: "DefaultCompany",
        productName: "Demo",
        productVersion: "1.0.2",
        showBanner: unityShowBanner,
      };

This can be easily modified to something like:

      const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
      var buildUrl = "Build";
      var loaderUrl = buildUrl + "/Build.loader.js";
      var config = {
        arguments: [],
        dataUrl: buildUrl + (isMobile ? "/Build.Mobile.data.gz" : "/Build.data.gz"),
        frameworkUrl: buildUrl + "/Build.framework.js.gz",
        codeUrl: buildUrl + (isMobile ? "/Build.Mobile.wasm.gz" : "/Build.wasm.gz"),
        streamingAssetsUrl: (isMobile ? "StreamingAssets.Mobile" : "StreamingAssets"),
        companyName: "DefaultCompany",
        productName: "Demo",
        productVersion: "1.0.2",
        showBanner: unityShowBanner,
      };

This provides a simple way to differentiate and deliver separate code and assets for Mobile and Desktop.

How this can be automated:

  1. Create a custom template where dataUrl, codeUrl, and streamingAssetsUrl are replaced as shown above.
  2. Use this template to build the project separately for Mobile and Desktop.
  3. Merge the builds into one – essentially by copying the Data, Wasm, and Asset files from one build into the other, and renaming them to match the names from the template.

Note: This is a simplified explanation, but it works and can be extended to fit specific requirements.