WebGL optimization Tips & Tricks

Hello everyone, for last few days i’ve been trying to gather resources for WebGL optimizations.
All the knowledge is spread through internet. I tested most of them and gathered all into a document.
These are what worked for my release build. These are essentials for WebGL but don’t forget this tips & tricks won’t magically boost any poorly optimized games performance :slight_smile: Also i forgot to save the forums & links i searched through while testing so i’m not able to share those threads :confused: But i hope it helps;

Btw for some reason i’m not able to use This in code blocks i provided. So if you see a line starts and ends with “~~”, that line is the sample to avoid.

Project Settings

Graphics

  • Remove all unused “Always Included Shaders”

Player ⇒ Other Settings

  • Static Batching = true
  • Graphic Jobs = true
  • Texture Compression Format = ASTC (If you’re targeting Desktop devices you can use DXT)
  • Lightmap Encoding = Low Quality
  • HDR Cubemap Encoding = Low Quality
  • Keep Loaded Shaders Alive = true
  • Strip Engine Code = true
  • Managed Stripping Level = Medium

Publishing Settings

  • Enable Exceptions = Explicitly Thrown Exceptions Only
  • Compression Format = Brotli (If not possible for your host , could be set to = Gzip.)
  • Name Files As Hashes = true
  • Data Caching = true
  • Memory Growth Mode = Linear
  • Maximum Memory Size = 2048
  • Linear Memory Growth Step = 16

Quality

  • VSync Count = Don’t Sync
  • Realtime GI CPU Usage = Low

Build Settings

  • Code Optimization = Disk Size with LTO
    • This option increases build time significantly, so if it’s not a release build go with Shorter Build Time
  • Max Texture Size = 512 (Depending on game but should be more than enough for most of the games)

URP Settings

  • Settings Screenshot

Texture Import Settings

  • Read/Write = false
  • Generate Mipmaps = false
  • Max size = 64 (Depends on Texture Quality needs but most Textures are OK with 64)
  • Resize Algorithm = Mitchell
  • Format = Automatic
  • Compression = High Quality
  • Use Crunch Compression = true
  • Compressor Quality = 50 (Value can be adjusted by Quality needs)

Sound Import Settings

  • Load Type = If it’s a background music or something similar = Compressed In Memory, else = Decompress On Load
  • Compression Format = Vorbis
  • Quality = 50 (Value can be adjusted by Quality needs)
  • Sample Rate Setting = Preserve Sample Rate

Coding Tips & Tricks

This tricks are mostly targeting to reduce workload on GC

  • Avoid string manipulations

    ~~string sample = “Text1” + “Text2”;~~
    System.Text.StringBuilder sb = new("Text");
    sb.Append("Text2");
    string sample = sb.ToString();
    
  • Cache Dynamic Functions

    private void DoSomethingWithArrayOrList()
    {
    	~~Debug.Log(CountForSample()[0]);
    	Debug.Log(CountForSample()[1]);
    	Debug.Log(CountForSample()[2]);~~
    	
    	List<int> countedList = CountForSample();
    	Debug.Log(countedList[0]);
    	Debug.Log(countedList[1]);
    	Debug.Log(countedList[2]);
    }
    private List<int> CountForSample()
    {
    List<int> countToTen = new();
    for (int i = 0; i < 10; i++) 
    	countToTen.Add(i);
    return countToTen;
    }
    
  • Avoid gameObject.tag - gameObject.CompareTag - gameObject.name usage

    public class TagReplacer : MonoBehaviour
    {
        // If component is already on the object
        [SerializeField] private string StartTag;
        private void Awake()
        {
            if (!string.IsNullOrEmpty(StartTag))
            {
                myTag = StartTag;
            }
        }
        // If component is added runtime
        public void Initialize(string tag)
        {
            myTag = tag;
        }
        private string myTag;
        // Use this instead of gameObject.CompareTag
        public bool CompareTagReplaced(string inputTag)
        {
            return myTag == inputTag;
        }
    }
    
  • Cache Coroutines

    WaitForSecondsRealtime WFSRT = new WaitForSecondsRealtime(1);
    IEnumerator Delay()
    {
        Debug.Log(3);
        ~~yield return new WaitForSecondsRealtime(1);~~
        yield return WFSRT;
        Debug.Log(2);
        ~~yield return new WaitForSecondsRealtime(1);~~
        yield return WFSRT;
        Debug.Log(1);
        ~~yield return new WaitForSecondsRealtime(1);~~
        yield return WFSRT;
        Debug.Log(0);
        // Do stuff
    }
    

Other Tips & Tricks

  • URP is generally performing better in WebGL builds
  • AVOID GPU Instancing usage on your materials (Different browsers are causing different issues)
  • Avoid Post Processing usage if possible
  • Pool your objects
  • Use simpler shaders
  • If Skybox is not visible in game, don’t forget to set Camera ⇒ Environment ⇒ Background Type = Uninitialized
  • Disable shadows or reduce their quality

Helpful Assets

5 Likes

This is experimental if I recall correctly. May lead to crashes.

I recommend to always set this to High the moment you create a project. Only lower it if you run into unexpected build-only issues. Normally, if you stay away from reflection and assets that use it, and sometimes code generation, High should work fine. But there’s also code that you need but gets stripped in Medium, too.

In essence: this is where you need to experiment, not blindly set a specific value.

Faster: no exceptions (unless using Wasm 2023 which requires exception handling).

Definitely always set this to Brotli!
If the server can’t handle it, configure the server to do so.
Only if you don’t have access to server configuration should you lower this setting. Your builds will be noticably larger!

Highly depends on the project! Can’t really advice specific settings here. Linear growth and a fixed step size can either help or hurt the project.

For iOS I would say that you still need to restrict max memory to 500 MiB at most.

Again project-specific. Some projects will want to prefer Runtime Speed over disk size.

Can heavily degrade your visuals (causes aliasing) unless you are using a flat-shaded, low-poly style.

Highly depends on where on the visual quality vs resource usage tradeoff the project lies. Some may favor high quality, others may need to favor reduced memory usage.

Those are widely known. The key is to monitor GC usage with the Profiler, and ideally in the Browser since GC behaviour in the browser is different than editor or other platform builds.

The “cache dynamic functions” thing is just the DRY principle applied. Don’t do the same thing twice (eg the same GetComponent() repeated all over again) but rather assign it to a local var after you got it once.

Most importantly, and generally: don’t “find” anything by string.

5 Likes

Yes, you’re right. Need to be cautious.

As my experience, setting it too high or too low can cause unexpected errors in final build. But of course experimenting is the spice here :sweat_smile:

I don’t suggest this if there’s try-catch handlings in your code.

Yes, Brotli seems to be the best so far. But in some cases (like mine), developers may not be in control of the server.

Thank you for your comments, these are more like a pointing finger to performance affecting points in WebGL when there is deeper optimizations are required. Like i said since all the knowledge and experiences are spread all around the net, i wanted to create a experience-sharing and gathering topic here. There is no settings preset for all of the games (if there would be we wouldn’t have any settings options i guess :laughing:), so all the variables defined above are samples that worked best for me. The ones who are seeking for optimization in WebGL should adjust those variables by their game and work environment. And i hope with contributions from everyone, we can create a good guideline here!

While it’s generally a good idea to avoid string manipulations, your specific example would run much worse with the replacement you suggested ^^. A StringBuilder makes most sense when you need to combine many string fragments into one string and the true benefit is only achieved when you cache the StringBuilder instance. Otherwise you will actually allocate more memory.

The string builder itself works like a List<char> in some sense. It also has an internal array as buffer that is replaced when needed to make room for more characters. Once your string is composed inside the buffer and you call ToString at the end, you will create a single new string out of the used characters in the buffer. When you recreate the StringBuilder every time, you just create additional objects. Several actually. The default capacity of a StringBuilder is 16 characters. So the internal array will be initialized with 16 characters. So just the array alone will by 32 bytes data + the object overhead (12 bytes on 32bit and around 20 bytes on 64bit systems). That’s just the array. The StringBuilder itself is also an object and also requires memory.

So using a normal String.Concat (which is used when you use + between two strings) would be cheaper in this case. Of course when the initial 16 bytes of the StringBuilder are reached, it will usually double the size by creating a new array and copying the old values over. When you construct really large strings, it can help when you set the capacity in the beginning when you can estimate the target size. Reusing the StringBuilder is of course the best solution memory-wise since the internal array can be reused.

If it’s really critical to avoid garbage allocation, some time ago vexe created his gstring solution. It uses a lot of evil unsafe code hacks to actually manipulate existing strings and cache them in a pool. Though you have to be careful how you use those strings. But they actually allow to avoid allocations completely in certain situations. Though you barely would need something like that.

2 Likes

Hello there !
Loving this thread. Used these advices ! thank you !

I am having such a hard time seting up brotli on server. Any recommendation ? what is the easiest server solution for this ?
Many thanks !

Targeting WebGL is really taking your Unity knowledge to the extremes. There’s a lot of things around the engine that can help you or hurt you depending on how or whether you use it. Some lesser known like TagHandle and the memory settings in Player Settings, some more known like the string concatenation and general caching and pooling.