Tips and tricks for using WebGL on desktop and mobile (tested up to 2021.3.11f1)

Hi friends,

Here are some tips and tricks I’ve used to get great WebGL builds that are fully compatible with mobile devices and work great on desktops too:

  • InputField not being supported on mobile: I ended up making an on-screen keyboard in Unity, and it works fine! Don’t overthink it. Authoring a jslib that calls the javascript prompt works fine too. It has been mentioned elsewhere here a bajillion times.

  • InputField apparently not supported anywhere. The underlying issue is that the input fields from UGUI and TextMeshPro both use IMGUI Event, which apparently is broken, disabled or otherwise malfunctioning in Unity 2021 LTS and does not interact correctly with Input System. Set Input Handling to “Both” to resolve the issue. If you are reading this and pay for Unity support, kindly ticket this issue.

  • Use flexbox to layout your canvas, and design for a fixed size. I choose 375x635, since that’s the iPhone X layout, and my content is in the iPhone SE safe area of 320x568. Then, I use flexbox to position my canvas in the center, and a post-process step to replace the JSON’s background color. Grab my modified responsive template here. You can discover responsive design sizes by entering “Responsive Design Mode” in Safari (see Imgur: The magic of the Internet). Stop using stupid margin or padding or other dumb CSS tricks and get rid of all the crap that’s in the default frame. Just do the modern thing. Alternatively, design for a responsive size, and full screen the canvas.

  • Use asmdef to control what is Editor versus Player code. This improves your compile speed too.

  • Always try to use a link.xml with high code stripping. There’s lots of documentation now about how link.xml works, but the overall jist is that the code stripper has a very hard time with static methods and third-party code. Be aggressive, don’t just give up and use Medium!

  • Disable modules you don’t need in the Package Manager window. This will require modifying your external libraries.

  • Use the Build Report Tool from the asset store to find assets and scripts you aren’t expecting to include in your build. For me, the biggest surprise was (1) System.Xml being referenced somewhere, and (2) plugins that pollute their Resources directory instead of using Editor Default Resources.

  • Compile for wasm targets and asm.js as a backup. iOS 13.0-13.1.2 do not work with WASM when the build is large, and iOS 12.1 and earlier do not work with Unity’s WASM at all. WASM also occasionally breaks on some specific iOS versions. However, Unity now supports it well for Mobile Safari and works with Apple to resolve issues. You can keep targeting both wasm and asm.js to prevent your experience from ever breaking on Mobile Safari.

  • Use sprite atlases to fine-tune your 2D asset compression. They’re great for controlling which assets need alpha channels and which don’t, and which assets can use crunch compression and which can’t. You should set each sprite’s import settings to correspond to the import settings on the asset, or just use RGBA32 on the individual sprites. Otherwise, the atlas builder will use the compressed version as its input, which is obviously a UX smell but what can we do.

  • Test on iOS 15 with WebGL 2.0, because it isn’t guaranteed to work. Otherwise, disable auto graphics and specify only WebGL 1.0. It’s the only engine supported by Safari for iOS, WebGL 2.0 as Unity implements it doesn’t work for the most part. There’s no point shipping around shaders or code for a WebGL 2.0 target you don’t really need.

  • On 2018-2020, use a UnityLoader.js and UnityLoader.min.js that doesn’t show a popup on mobile and switches to asm.js for iOS 12.1 and earlier. For 2018, I modified them to fix this. You can copy the files from here into your /Applications/Unity/Hub/Editor/2018.3.13f1/PlaybackEngines/WebGLSupport/BuildTools directory or copy them using a post-processing step. Feel free to use your favorite diff tool to visualize what I changed. My minified version isn’t actually minified since it’s tricky to minify this code.

  • Set Application.targetFramerate to -1. This ensures you use requestAnimationFrame to render your frames as quickly as possible.

  • Use asm.js and the Profiler to pin down weird stuff. For example, I discovered that only on iOS, the first call to DateTime.Now took 2275ms! Since you don’t have deep profiling, you should familiarize yourself with Profiler.BeginSample and Profiler.EndSample. This is really slow and annoying but it is super informative for finding real issues.

  • Native calls do not work in coroutines. They just don’t. A native call is one to a function with the [DllImport] attribute. That means if you use e.g. a websocket jslib and try to call a jslib declared method in the coroutine, it will silently fail.

  • Enable demangling in your development builds. This gives you more useful stack traces: PlayerSettings.SetPropertyString("emscriptenArgs", "-s DEMANGLE_SUPPORT=1 -s ASSERTIONS=1 -s -Werror", BuildTargetGroup.WebGL);

  • Use Unity’s native JSON. You really shouldn’t be using JsonFX/Newtonsoft/JsonObject, it will massively balloon your code size due to the compilation of generic specializations.

  • Use WebAudio yourself, and activate it on a click. I use this for looping music. Contemporary browsers require you to .play() audio in a click. Set the handler using html onclick="playAudio()", otherwise the canvas will eat the clicks.

  • Don’t be stupid with third party code. Asset store stuff is good because it works but it can be incredibly poorly written, accidentally referencing stuff like System.Xml, System.Data or System.Runtime and tank everything.

  • Don’t be stupid with fonts. If you only need a single size and a single character set, configure it. If you can use Unity’s UGUI instead of TextMesh Pro, do it. But overall, avoid showing too much text in your game, since that’s what HTML is good for!

  • Test on iOS devices. I know this scene is really big on Windows + Android but you gotta actually buy an iOS device and try it with your mobile site. You simply cannot reproduce the issues experienced by the ordinary user with the iOS Simulator that ships with XCode.

  • Don’t use DateTime. There are weird interactions with locale support and Unity player. Use NodaTime for time.

  • Test for the appropriate Chrome build to test Android devices. My recommendation is Chrome 44, likely the oldest version of Chrome you’ll have to deal with in the real world that can actually play WebGL games distributed this way. I had to use a polyfill for enumerateDevices and fix the worker code in the 2018.3 loader to get it to work on this platform. Google how to download old Chromium builds. This platform doesn’t support WASM.

  • Use a CDN, like CloudFront. You should definitely never serve directly from S3 or object buckets like that, because it’ll be way too slow over LTE.

  • Use brotli compression. It’s so much better than GZip. If you plan to use CloudFront with an S3 origin, make sure to set the .unityweb file’s Content-Encoding to br, and use an https (with http redirect) configuration on your CDN because Chrome only decompresses br natively for you from TLS URIs. Executing this code from your project directory will fix the “Exception: Failed building WebGL player” you have

  • Build with hashes. It’s imperative you build with hashes for simple setup with your CDN. Otherwise your assets will be perpetually mixed and buggy and you won’t really know why.

  • For compatibility questions, use caniuse.com. This is helpful for platforms you don’t have. Unity3D needs localStorage and WebGL 1.0 in canvas support.

  • Addressables is overrated. Since the content is decompressed and decoded on the main thread, your whole game will freeze, a worse experience that simply downloading the original content.

  • Assetbundles freeze for a while when loading. Loading is single threaded, so you shouldn’t expect to have any interactivity while external files, no matter how slow, are loaded into the Unity player.

  • Application.isMobilePlatform works except for iPads. Use this to determine if you are on a mobile phone browser. iPads will not report correctly.

  • Use PlayerPrefs to store local data. It uses IndexedDB internally and is well supported across browsers. However like all local browser storage it has interactions with playing from the localhost domain (for local testing) and cannot be read by other domains generally.

The single greatest obstacle is that iOS constantly breaks WASM. Apple simply does not care. So always build asm.js.

A 2017 iPhone X is extremely fast, and supports WASM in 12.2. Its graphics and CPU are significantly faster than a Nintendo Switch and any shipping Android device. I guarantee you can make a great-performing WebGL game in Safari as long as you don’t do anything stupid!

31 Likes

Great post!

Regarding UnityLoader
I prefer to re-set the method that checks if mobile on the html page script, before calling UnityLoader.Instantiate, instead of editing UnityLoader.js, as I don’t want to rewrite the file in every install of new version of the engine

This is a fantastic thread! Would have saved me a lot of time!

I have found Brotli compression 4 or 5 to offer the most optimal speed. (using that setting decreased loading time by 27%)

I am not using a CDN and my mobile speeds are really slow, will start using a CDN.

To add a couple of things to the list, indexDB can be quite complex to get working on all browsers, we are moving to web sockets within the month, due to compatibility issues.

I am surprised that Unity disabled asm.js from 2019.1.

Has anyone been able to get streaming instantiation working? Currently stuck on that.

Actually I use streaming instantiation since 2017 version, but I use my own loader.

I just want to point out that these two things, IndexDB and Web Sockets, are not really related to each other.

There are polyfills for browser storage. But for a web game, you should really be storing the user’s data on the server.

Web Sockets work fine everywhere I have tried. I use it for my own multiplayer game. Just don’t do native calls inside coroutines, they don’t work.

Whats with mobile specific Texture compression? Like ETC, PVRTC & ASTC. I know there’s one guy here on the forums who found a solution to this (although a rather complicated one). Do you have a good approach for this?

Yeah mobile compressed textures are our biggest “wish list item”. The latest version of Unity even broke my backdoor way to do it on iOS. :frowning: Honestly for us I think it’s a bigger game changer than threads. At the moment on mobile we just roll with uncompressed textures at half the resolution of desktop (roughly the same memory).

In general I agree with the list except for two things:

  • There’s nothing wrong with using WebGL2 on firefox/chrome…not sure what the benefit of turning auto off is
  • asm.js on Safari iOS is WAY SLOWER than wasm on Safari iOS (for us it was like less than 1/3 the performance)…it was essentially unusable

I’d also like to add we finally got around to making all of our asset bundles uncompressed and then compressing them with gzip. It actually makes a huge difference on slow devices. They reduced load time on our ancient android device by over a 1/3. So I recommend taking the time to do that.

1 Like

I don’t remember where it was written(a post by chrome or firefox devs), but using WebGL2.0 where you can, should give some performance boost even if you use only WebGL1.0 methods. But I guess that it depends on browser version and other stuff

You mentioned it earlier somewhere on forum and I tested it. I was really surprised.
We are using cloudflare cdn with Content-Type: application/wasm for both bundles and engine files. Cloudlare uses brotli where possible.
In the end uncompressed + brotli has less size than compressed + gzip - win win.

This guide works essentially the same for 2019, you will need to modify your UnityLoader.js differently.

I’ve updated with my experiences using addressables and dealing with audio.

1 Like

Thank you for a fantastic list (and thread!).

Anyone have any experiences on the memory limit on builds?
an empty unity project runs at around 700mb memory on iOS and a simple game i made easily surpasses 1.1 gb. As i understand it, a recent update to iOS (12.2 i believe) increased the memory limit to around 2gb anyone able to confirm this?

Sounds like you’re building debug or development builds with no code stripping.

On empty projects I usually use 48MB or less. On my WebGL titles I keep it under 128MB pretty easily. 64MB-128MB is my target.

@doctorpangloss Can you share what where the assets that used and/or ways that you found to get rid of the system.xml.dll, and system.data.dll in your builds?
I too have both in my build and even trying to remove third party code wasn’t enough to get ridden of them.

Thanks

@Uli_Okm Just having import System.Data or System.Xml anywhere in your code is enough to import the libraries.

Thanks for the tip! I am still figuring out why they are being added EVEN in a empty project with only a empty scene, maybe Unity changed something in 2019.1…

Thank you for the tips! Do you have any tips about fullscreen mode on mobile? I’ve got one issue with it that I can’t solve yet: Upscaling of fullscreen mode in mobile Chrome browser

Because you cannot full screen on iOS Mobile Safari, I would say, you cannot do it at all :slight_smile:

I don’t even try iOS. I will be more then satisfied with Android.
Canvas behavior is very strange, I can’t understand the logic behind the canvas scaling. Also, In fullscreen mode I’ve got very strange canvas size. Something like 731.4 * 411.6.

Yes, canvas sizes are very odd in WebGL mobile. I’m more familiar with iOS, the sizes are the “responsive” sizes in “pixels” i.e. points, which are really meant to allow sites that specify pixel sizes to have “roughly” the same physical size across all devices. Nowadays, with 2019, you can set up a HiDPI canvas (better known to CTO Google as “retina”) and your canvas sizes will get even MORE confusing.

Personally, because of the changing standards, I would recommend feature detection and physical scaling, whenever possible, to determine your canvas scaling size. Specifically, with the appropriate meta tags in place, place a common element in your host .html page, measure its size in various units, and use that sizing information to determine your scale. This essentially outsources your sizing information to browser defaults or, if you style that element with bootstrap for example, Twitter engineers.

Hey Hi, anyone know how to detect closing the tab or browser? I need to clear my playerprefs on that but I couldn’t find any solution yet.