Unity WebGL Memory and Performance Optimization

Many Unity developers have taken the plunge and started exporting WebGL versions of their games. Unfortunately, many of them also run into issues that can lead to performance degradation and instability. This is a particularly tricky problem on older machines that have less resources available.

This article will outline several techniques for resolving some common WebGL performance issues we’ve seen with games published on Kongregate, and hopefully get you off to a good start when it comes to delivering a great plugin-free experience with your WebGL game.

Measure Twice, Optimize Once

One of the most common mistakes developers make when trying to optimize content is making assumptions about what is taking up lots of time or space. With Unity and WebGL this happens often when deciding how much heap space to allocate for the WebGL application in the player settings. Without doing research on the subject, it can be easy to assume that increasing the memory available to the application will lead to better performance. In reality you generally want to allocate the smallest amount of memory possible, which can be counter-intuitive.

Reducing the size of the heap can greatly reduce or eliminate the number of “The browser could not allocate enough memory for the WebGL content” errors your players see, but how do you know what to set it to? Luckily, you can get a pretty good feel for how much memory your application is using with a simple JavaScript snippet which you can either paste into the browser console (for ad-hoc debugging), or as a permanent addition to your index.html template:

setInterval(function() {
  if (typeof TOTAL_MEMORY !== 'undefined') {
    try {
      var totalMem = TOTAL_MEMORY/1024.0/1024.0;
      var usedMem = (TOTAL_STACK + (STATICTOP - STATIC_BASE) +
                    (DYNAMICTOP - DYNAMIC_BASE))/1024.0/1024.0;
      console.log('Memory stats - used: ' + Math.ceil(usedMem) + 'M' +
                  ' free: ' + Math.floor(totalMem - usedMem) + 'M');
    } catch(e) {}
  }
}, 5000);

This will output the amount of used and free memory into the console every 5 seconds, like so: Memory stats - used: 155M free: 37M. With this very simple addition, you now have some insight into how to tune your heap size, which is fantastic. You can watch these numbers as you play your game and get a feeling for what you should cap your memory usage at. Be careful not to lower it too much, though, or you’ll start seeing memory allocation errors that tell you to increase your heap size.

A Note on Dynamic Heap Sizing

When searching around for solutions to out-of-memory issues, you may stumble upon an Emscripten flag called ALLOW_MEMORY_GROWTH, which sounds like the perfect solution to your woes. These days enabling this flag can have a negative impact on performance, and it can also cause out-of-memory issues, as the memory manager has to allocate even more space when trying to grow the heap. The Unity team has never recommended enabling this flag, and we do not recommend it either.

Smoothing Out Initial Loading

When using Unity 5.3 and above, your game assets will be compressed with gzip by default, and will have a gz prefix on the filenames. As described in a previous WebGL blog post, if you upload these to Kongregate, we will automatically set the Content-Encoding: gzip header so that the assets can be decompressed by the browser instead of JavaScript, leading to a much better loading experience.

However, if you are hosting your game externally, you will need to manually ensure that the assets are being served with the correct encoding. You will know if this is an issue because Unity will log messages like Decompressed Release/MyGame-WebGL.jsgz in 500ms in the console, the browser may appear to lock up during loading, and your progress bar will not work properly.

If you are using apache or a compatible server, you can use the .htaccess file that Unity generates for you to resolve this issue. However, if you are hosting on S3 or something similar, you can force the encoding by changing the filenames in your index.html to add the gz suffix, like so:

var Module = {
    TOTAL_MEMORY: 201326592,
    errorhandler: null,
    compatibilitycheck: null,
    dataUrl: "Release/MyGame-WebGL.datagz",
    codeUrl: "Release/MyGame-WebGL.jsgz",
    memUrl: "Release/MyGame-WebGL.memgz",
  };

You will also need to ensure that S3 is set up to send the Content-Encoding: gzip header for each of the files that end in gz, or you will get decompression errors upon loading the game. You can use the browser network inspector to ensure the header is being sent properly by the server.

Optimizing Asset Loading

Another unexpected source of performance problems is caused by loading asset bundles. For Unity versions lower than 5.5, asset bundles are compressed using LZMA by default. Decompressing the LZMA content for WebGL builds causes a very noticeable pause. These pauses not only make your game run very poorly, but can bring up the dreaded “this content is not responding” error dialog. Luckily, Unity 5.3 and above support the faster LZ4 format for asset bundles (and it is the default for WebGL in 5.5+), so you can resolve this issue relatively easily.

It should be noted that LZ4 assets can be a bit larger than LZMA, but the tradeoff is generally acceptable. If it is not, you can use gzip compression on top of the asset bundles, and serve them with the Content-Encoding: gzip header (like mentioned above) to have the browser decompress them automatically.

One of our developers was kind enough to share the C# code they’re using in their build script to enforce LZ4 compression for WebGL:

BuildAssetBundleOptions options = (buildTarget == BuildTarget.WebGL) ?
                                  BuildAssetBundleOptions.ChunkBasedCompression :
                                  BuildAssetBundleOptions.None;
BuildPipeline.BuildAssetBundles(bundlePath, buildMap,  options, buildTarget);

Keep Up on Unity Patch Releases

The Unity team releases patches for supported versions of Unity frequently. Keep an eye on them and see if any of the WebGL-related changes might resolve issues you are experiencing.

For example, Unity 5.3.6 patch 6 resolved an issue where WebGL builds were allocating much more memory than needed, which is definitely a bug you’ll want to have fixed when you build your game. If upgrading is not a viable option, they also generally have you covered with workarounds. In this case, you can reduce your footprint on lower versions with the following build setting:

PlayerSettings.SetPropertyString("emscriptenArgs",
                                 " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1",
                                 BuildTargetGroup.WebGL);

Other Resources

  • Read the Unity Blog and documentation religiously -- such a vast amount of knowledge is available there. Many of the concepts in this article are discussed in much more technical detail here, for example.
  • Check out this Kongregate Developer Blog entry focused on managing in-game memory for Unity WebGL projects.