The open source PlayCanvas game engine is built specifically for the browser, incorporating 10 years of learnings about optimization. In this talk, you will discover the secret sauce that enables PlayCanvas to generate games with lightning fast load times and rock solid frame rates.
Optimizing HTML5 Games: 10 Years of Learnings
AI Generated Video Summary
PlayCanvas is an open-source game engine used by game developers worldwide. Optimization is crucial for HTML5 games, focusing on load times and frame rate. Texture and mesh optimization can significantly reduce download sizes. GLTF and GLB formats offer smaller file sizes and faster parsing times. Compressing game resources and using efficient file formats can improve load times. Framerate optimization and resolution scaling are important for better performance. Managing draw calls and using batching techniques can optimize performance. Browser DevTools, such as Chrome and Firefox, are useful for debugging and profiling. Detecting device performance and optimizing based on specific devices can improve game performance. Apple is making progress with WebGPU implementation. HTML5 games can be shipped to the App Store using Cordova.
1. Introduction to PlayCanvas and Game Optimization
♪ Hi, my name's Will Eastcott. I'm the creator of PlayCanvas. And I'm going to be talking to you today about optimizing HTML5 games based on 10 years of learnings working on the PlayCanvas game engine.
So PlayCanvas actually powers Snap Games, which is the gaming platform in Snapchat. It's had over 200 million players. There's a large number of games that you can check out of just about any genre, so I would encourage you to check them out. But PlayCanvas isn't just used by Snapchat-based game developers. It's used by game developers the world over to make all sorts of different types of games, from casual games to .io games. It's actually pretty popular for FPS game developers, and you can see several of those represented there.
Now, my personal journey in working on game optimization started in the early 2000s working for a company called Criterion Software on a game engine called Renderware. Now, Renderware was used to power about a third of the games in the PlayStation 2 generation, and day-to-day I would be working on this type of hardware. So we're talking a T-10,000 PlayStation 2 developer kit. And yeah, if you wanted to do performance analysis on that kind of hardware, you would struggle. And you would often need to go into Sony's HQ and work on special hardware that was developed by then, very expensive hardware. It was incredibly inconvenient.
Fast forward to 2022, and HTML5 game devs are living the dream, right? I mean, we've got incredibly powerful hardware in the palms of our hand. And we've got great tools that are built right into our web browsers. So is optimization still important? Well, spoiler alert, yes it is. Now, performance optimization, in my view, falls into two main areas. There's load times, and there's also frame rate optimization. And let's start talking about load times. Now, this is something that we don't want our end users to see, loading bars.
2. Investigating Load Times and Texture Optimization
So why does it matter whether we present our users with loading bars? After 6 seconds of waiting, we tend to lose 40% of our audience. To investigate load times, we can use advanced tools built into the browser, such as Chrome DevTools. By sorting resources based on size, we can identify opportunities for optimization. In HTML5 games, most of the data is texture-based, and large images can cause crashes and long load times. However, hardware texture compression can help by reducing GPU memory usage and eliminating decode costs.
So why does it matter whether we present our users with loading bars? Well, as it turns out, after 6 seconds of waiting, we tend to lose anything like 40% of our audience who just bounce, not prepared to wait for the page to load.
So when we begin an investigation into load time, what kind of tools do we have available to us that can help us investigate optimizing load times? Well, like I said, built right into the browser, you have some pretty advanced tools. In Chrome DevTools, you have a couple of tabs. You have the Networking tab, which shows you what resources My Game is loading, and then you have the Performance tab that shows how My Game is loading those resources. So when you start your investigation, you typically look for the low hanging fruits. If you look in the bottom left of the Browser tab, you'll see the number of requests made by the browser. You'll see the amount of data that's transferred, and you'll see the time it takes to load your game. Now, the first thing that you'll want to do is sort the list of resources based on size, because, obviously, the bigger the resource, the bigger the opportunities for optimization. You can search for duplicates or redundant resources that your game shouldn't really be loading in the first place. And if we look at the biggest file that's being loaded here into our game, it's a 2.2 megabyte jpeg. So we can ask ourselves, hey, can we downsize these resources? Can we optimize them somehow?
Now, as it turns out, in HTML5 games, typically most of the data tends to be texture-based data. And in our example in the previous slide, we had a 2.2 megabyte jpeg. Now, if the browser downloads this file, we need to decompress it into memory. And that is 48 megabytes of data. Then we have to pass it to WebGL to be used as a texture. And a copy of it is made, plus, we have to generate mitmaps, which is another 64 megabytes of data. So together, that's like 112 megabytes of data just for a single jpeg. Now, if you try to load about 10 of these into a browser tab on mobile, I guarantee you you'll crash the tab. So we need some kind of solution around this. Moreover, every jpeg reload, we have to pay, like, a decode cost. It takes time to actually decompress the data from jpeg to an uncompressed format. And in this case, this 2-megabyte texture takes 160 milliseconds, which is just excessive. Because it's going to cause the mainframe to block, and it's going to cause long load times. Luckily, we have hardware texture compression that we can use to load more optimized texture data. So if we take our original 2.2-megabyte jpeg and we crunch that down to some natively supported hardware texture formats like DXT, PVR, and ETC, we find that the sizes are actually larger than the original jpeg. So because the format of this hardware texture compression data is native to the GPU, we can just supply it to the GPU with no decode cost, which is great. Also, the amount of GPU memory used by hardware texture compressed data formats is between 1 5th and 1 10th of the original jpeg. So we're at least sure we're not going to be crashing the browser tab anymore. But we are paying the costs for having to download large images. And that's a problem.
3. Texture and Mesh Optimization
Fortunately, there's another texture format called basis, which compresses the original jpeg to a smaller size while maintaining the benefits of native formats. Compressing textures to basis in the Play Canvas Editor is a simple process. Mesh data is another significant contributor to download size, especially in 3D games. JSON is a commonly used format for loading mesh data, but it can result in large file sizes and slow parsing times. GLTF, an open standard owned by the Kronos Group, offers a solution to this problem and has a thriving ecosystem.
So what can we do? Well, fortunately, there's another texture format called basis, which is essentially an abstract compression format that is supposed to be transcoded to any of the natively supported formats at runtime, at load time of your game. So if we take that original jpeg and we convert it to a basis texture, it goes from 2.2 meg down to 1.7, which is great. But also, we get all of the benefits of these native formats, so again, between 1 5th and 1 10th of the original GPU memory that the jpeg occupied.
So what does compressing textures to basis look like in the Play Canvas Editor? Well, it's a very simple operation. You just select the textures that you're looking to compress. You can see we've got four 2K textures here, which are like 5 megabytes of PNG data. You just say, hey, I want these to be basis compressed. You import the transcoder. And you hit the compress button. And there you have it. It's very simple.
So after texture data, the next biggest contributor to download size in an HTML5 game is often mesh data, at least for 3D games. So let's talk a little bit about mesh optimization. Here we have something called the Stanford Dragon. It's a mesh that's often used in rendering experiments. And we're going to use it in some tests here. Now you can see it's a very dense mesh. It's got hundreds of thousands of vertices and triangles. Probably not a typical game asset, but it should make some, underline some points from building.
So what can we do? Well, the Kronos Group owns an open standards called GLTF, which is essentially designed to be the JPEG of 3D. A large ecosystem has grown up around GLTF. There is companies providing tools such as Play Canvas.
4. GLTF and GLB Optimization
We now use GLTF as the primary format for the engine. GLB, the binary format of GLTF, is significantly smaller than JSON and has a parse time of just 50 milliseconds. The glTF format stores data in a GPU-ready format, allowing direct parsing to WebGL with no processing. Compressing the glb file using Draco technology from Google reduces its size to 1.84 megabytes, with a decompression time of 0.4 seconds. Offloading decompression to a WebWorker thread can further optimize performance.
And we now use GLTF as the primary formats for the engine. So let's examine what GLTF, how that performs with this particular data. So GLB is the binary format of GLTF. So if we say that a GLB file containing this mesh we find that it's less than half the size of the original JSON file. And when use it, it's only marginally smaller than the JSON data. That's because JSON is text and it compresses very well. The key thing to notice is that the parse time for the mesh, the GLB file, is just 50 milliseconds, a tiny fraction of the time it takes to parse the JSON, which is pretty incredible but the reason for this is that the glTF format stores data in a GPU ready format. So once it's copied out of the file, you can parse it directly to WebGL with no processing. So this is a huge win for game engines and HTML5 games to ensure they're using glTF. Also, the peak memory usage is lower because like I say, we're not throwing around large JSON datasets. We can do even better than this though. We can compress the glb file using some technology from Google called Draco. This is an extension of the glTF specification and allows you to compress the vertex data. So here we can see that the 21meg glb can be compressed down to 1.84 megabytes. And you can even cheese it up slightly further down to 1.79 meg. The only slight issue that you must be aware of is that this data needs to be decompressed at load time. So to run the Draco decompressor for this mesh takes 0.4 seconds. But as we did with the basis textures, we can offload that to a WebWorker thread and we can then not store the main thread and essentially hide that process.
5. Game Resource Compression
It's important to compress game resources to improve load times. Verify that your server serves compressed resources by checking the content encoding header. The compression technique will vary depending on your back end services provider. For example, with Google Cloud, you can use util to specify which file types should be gzipped.
So, I've mentioned a few times it's important to use gzip or compression as part of your process to publish your games. It's very critical that your infrastructure, your server serves your game resources compressed. So to verify that you should be able to select any resource and look at the content encoding header. And check that it's set to the gzip or brotli. Now how you compress your data to put it on your infrastructure will depend on your back end services provider. The technique will be different per provider, but here there's an example how you would do it with Google Cloud. You'd use just util and you would specify which file types you want to be gzipped.
6. Optimizing Load Times and Game Design
Let's apply these techniques to the game Swoop. By converting JPEGs to basis and using GLB instead of JSON, we can reduce load time by a second. Unloading and asynchronously loading areas can create a seamless environment without loading bars. Bitmoji Party uses this technique to load assets while the user selects a gameplay option.
So let's apply some of these techniques to the HTML5 game Swoop. We can see that the original game used JPEGs and JSON for model data and the original load time was 4.5 seconds total. So just by converting, crunching all of these JPEGs down to basis and re-importing all of the artwork as GLB instead of JSON, we can shave an entire second of the load time. And that represents a few percent of your audience that you've managed to retain by reducing that load time.
There are other techniques for improving your load times, one which is actually thinking about your game design. So one of my favorite games of all time is Metroid Prime and they had an interesting technique where you could move from a large open area to another large open area through a tunnel. And when you're moving through the tunnel, they unload the previous area and start asynchronously loading the next area. When you get to the end of the tunnel, you shoot the door. And as soon as the next area has finished loading, the door will open. And it means that in the entire game, you don't see loading bars and the entire environment seems completely seamless. This technique is used in many PlayCanvas games. So here we have Bitmoji Party, which loads a very minimal set of assets to show that initial menu. It maybe takes two seconds for that first initial menu to load and be displayed and be interactive. And while the user is spending those two or three seconds just deciding what they want to select in terms of an initial gameplay option, we can be streaming the first set of assets in that mini-game on the right. And it means that in that particular game, you don't see loading bars.
7. Framerate Optimization and Resolution
Let's talk about framerate optimization and why it's important to scale your game from high-end to budget devices. Investigate framerate using the performance tab and hierarchical profiler. Focus on hotspots in the render function for performance gains. Use tools like Spectre.js to capture and analyze rendering frames. Choose the right resolution based on device capabilities and limit graphical complexity for better performance.
Let's move on to talk about framerate optimization. Why do we care about framerate these days? Surely smartphones are pretty powerful these days, right? Well, it's interesting if you go and look at some of the benchmarking numbers for phones that are on the market today. So I took the iPhone 13 and the Samsung Galaxy A21s, and the iPhone 13, in terms of CPU benchmarks, outperforms the A21s by like an order of magnitude. This is a huge difference. So it's important that your game can scale from the high end, all the way down to some of these more budget devices, as well, considering some of the legacy devices that aren't even on the market still.
What tools do we have in our arsenal to be able to investigate framerate? Well during load time investigation, we looked at the performance tab. The performance tab is very powerful. You can also use it to run a hierarchical profiler for your code. So you can capture a trace, over say 10 seconds, and you can then drill down into the core stack of your game, and identify the hotspots that take up the most CPU percentage, and then focus your efforts there. And we also have the timeline view in the Performance tab, which allows you to zoom into an individual frame, and have a visual representation of where time is spent in that individual frame. So here I can see that my game loop consists of an update and a render. Now in this case, the render function takes almost three times as long to run as the update function. So I'm clearly going to want to focus my efforts there because most of the performance wins can be found there. So to investigate rendering performance, there are many tools available that's found in browser extensions. One really great one that I recommend is Spectre.js. It allows you to capture a single frame of rendering, and it shows how WebGL is being driven. So you can see individual draw calls being submitted, and you can even drill down and see shader code that's being executed.
Perhaps the biggest mistake I see game devs make today is picking the wrong resolution for their game. Now, I picked a couple of devices here to make a point. The iPhone 13 Pro Max actually has 20% fewer physical pixels than the Samsung Galaxy S6, which is a seven-year-old device, whereas the S6 actually has, obviously, a much weaker GPU in terms of fragment processing. So it wouldn't make sense for you to just, like, render your game liberally at full device resolution on any device. What you can do is either give the user an option to render at different resolutions, or better yet, you can detect the GPU. You can do that on Android, and you can make a decision about the rendering resolution based on the family of GPU that you've detected. Something else you should consider is limiting the graphical complexity of your game. So, on the left, there's a game called Bitmoji Tennis, which is very simplistic. It uses a single emissive map for the environment. There are no dynamic shadows. Everything is baked. There's a single directional light for shadows, well, for lighting the characters. And then, on the right, you can see there's a much more complex demo.
8. Optimizing Draw Calls and Performance
The complexity of shaders affects GPU strain and frame rates. Carefully manage draw calls to minimize CPU and GPU costs. Techniques like Atlasine Textures and batching can optimize draw calls and reduce processing overhead. Three key pieces of advice: optimize early, design for performance, and test on your baseline device.
Graphically speaking, it's a technical demo that the Play Canvas team built, which uses physical shading, shadow mapping, image-based lighting, and other effects as well. And the complexity of the shaders of the game on the left is far simpler. And therefore it puts less strain on the GPU and you're able to render the high frame rates.
I also recommend that you be very careful about the number of draw calls that your game makes. Now, a draw call is essentially submitting a graphical primitive to the GPU with some render state. Every draw call will have an overhead in terms of CPU and GPU costs. And a typical HTML5 game might render between, say, 100 and 200 draw calls if you want to target some of these low-end devices.
So one technique for optimizing draw calls is Atlasine Textures. So for this environment that you see rendered there, there were several materials. There's wooden planks on the floor, there's wallpaper on the walls, et cetera. Now, these textures are atlased into a set of textures, and this means that draw calls can be combined together. Another technique is batching, where we have an environment that's rendered using seven distinct models, and some of these models are rendered multiple times. You can see, for example, some of the buildings are duplicated, the cat tire are duplicated. And, when we render this scene normally, this is kind of the way the scene is built up. You can see there's an individual draw call per object. But, if we use batching, we can combine similar models into combined draw calls, if you like. And, this means that we can render the entire scene in just six draw calls, instead of the original 50. So, almost an order of magnitude reduction, which pretty much maps to an order of magnitude lower processing on the CPU and GPU in terms of, well, I mean, on the CPU and on the GPU, it's a lower overhead, because there's fewer draw calls.
I'm going to leave you with three key pieces of advice here. Do not leave optimization considerations until the end of your development process. Also, design your game with performance in mind, right from the very beginning. And lastly, select what your baseline device is and test on that device early and throughout your development cycle. That's it for this talk. Thank you for listening.
9. Browser Usage and DevTools
Let's start by taking a look at the poll results. There's an overwhelming majority of Chrome users, which is not surprising. I primarily use Chrome, but I also test the engine on other browsers. Firefox's DevTools have caught up in recent years and are worth looking at. The CPU profiler is particularly useful. Opera shares Chrome DevTools, but I'm not sure if they have their own engine. Safari's DevTools are great for iOS debugging, allowing remote debugging on a connected phone. However, there are limitations when connecting to signed and production apps. The experience of connecting to Chrome on Android is also smooth.
So, let's start by taking a look at the poll results here. So it looks like there's an overwhelming majority of Chrome users here. Yeah, I guess this is sort of what I expected. I mean, I don't know about you, Omar, but I use Chrome mostly. But I have to kind of dip into all of the different browsers because, you know, obviously it's important that I test the engine everywhere it's gonna run. And yeah, so it's not uncommon I spend a bit of time in Safari and Firefox and so on, but... Yeah, and especially, I was gonna say the way the question is phrased like, you know, which browser do you use to optimize your games? Cause I've also will primarily be using Chrome DevTools, but I was pleasantly surprised last time I was trying Firefox that their DevTools I have caught up at least since, you know, five years ago when I last used it. So maybe it's not, I mean, I don't know if it's comparable, but it is worth looking at I think. I think the CPU profiler is really awesome. I use that quite a lot. Also I noticed there's some people who went for Opera here and I mean, presumably Opera shares Chrome DevTools with... I was gonna ask that, I don't know if they have their own engine. I've never used Opera. I mean, I've used it before. Yeah. And does Safari, cause I haven't done much iOS debugging, is the Safari have... Cause I know you can do the thing where you like you connect the phone and then you can kind of do this remote debugging. Does Safari have pleasantly... Well, DevTools? Well, yeah, the DevTools is pretty great. Yeah, I mean, as you mentioned, the main reason you're gonna use them is because you're connecting to an iOS device and then you can debug either a WebView or Safari browser. And that experience is pretty good. I think like the only kind of issue that we've had is because Play Canvas is used to build quite a few sort of hybrid applications where WebView is embedded in the application so that it can be shipped to an app store and you can't actually connect to an app that's been like signed and production. Which is kind of frustrating, but yeah, generally it works pretty well. And Chrome's the same with Android, right? Like the experience when you connect over, I mean, I normally do like USB logging, but that whole experience is pretty slick these days.
Detecting Device Performance and Optimizations
Dan asks about tools for detecting device performance. Play Canvas has a mini stats profiler that shows CPU and GPU utilization, as well as draw calls. GPU profiling is difficult on mobile, but a WebGL extension called Disk Joined Timer can provide accurate timings. Mark asks about optimizing games based on specific devices. Detecting the device in the browser can be limited, but user agent sniffing and window properties can provide clues. On Android, Chrome reports the GPU family, allowing for specific optimizations. Many game developers use if statements to target older GPUs and limit rendering complexity.
Cool, that makes sense. Now, we have a couple of questions streaming in from the audience. So Dan says, great talk, are there any tools for detecting device performance? So there are a lot of tools. So I'm not quite sure if Dan means sort of like, sort of theoretical performance or whether you just want to get like performance stats out of running app. I mean, in Play Canvas, we have something that we call the mini stats profiler. And it's like a panel that you automatically get as part of our launch page when you run your application. And it will show you CPU utilization, GPU utilization, and number of draw calls and stuff like that. So you've got like a real time kind of little hub down there that shows you performance stats of the device that you're running on. And it's really important that, you know, that's easy to do, especially on mobile, right? Cause most of us are targeting mobile these days. So having like easy ways to figure out like what performance you're getting on a mobile device is like really, really important. And the only, like one slight problem that you have on mobile these days is that if you want to do like GPU profiling, it's quite difficult in the browser. There's a WebGL extension called Disk Joined Timer, which allows you to essentially do, you know, accurate timings on the GPU. So that's what our mini stats profiler uses. But unfortunately it's not well supported on mobile. I don't think it's supported on iOS, for example. That makes sense. I think it's actually a good segue into the next question where Mark asks, if you ever find it useful to do real-time profiling of the hardware a game is running on and then do, like, based on that modifications of like, okay, for this device, we'll have like higher LODs or lower LODs or something like that. Do you ever do these like micro-optimizations based on specific devices? Yeah, that's a really good question. I mean, there was a slide in my talk where I kind of mentioned the huge disparity in power of mobile hardware today that you can actually just go out and buy in a shop. And so, you know, it's kind of, it would be a huge shame if you had to write a game which was just targeted towards the lowest common denominator hardware. So if you're gonna somehow like give your users, give your players a better experience, you'll want to find a way to let your game scale depending on the performance of the device, right? So that can be tricky because in the browser you're kind of limited in being able to detect the device on which you're running. So for example, on iOS, you don't know which GPU is being used. You can do things like user agent sniffing to give you clues about the device. You can even do things like make assumptions based on the things like the reported window width and height or like, you know, what's it called? The window dot device pixel ratio, whatever it is. Like those kinds of properties can give you a clue as to the age and power of the device. And then on Android, things are a bit easier because Chrome will report the actual GPU family that you're running on. So actually I know many game developers that write their games where they've got some kind of a statement in their code that says if Android, and then they've got like a switch statement with certain GPUs that they wanna do something specific for, normally it's like seven, eight, nine year old GPU families like the Armali 400 or the Adreno 300 family, right? Because on those older devices, you're gonna probably want to limit the complexity of what you're rendering. So maybe, I don't know, you're gonna turn off particles or you're gonna, I don't know, use lower LEDs like you say, they're normal. So yeah, there are techniques you can use to kind of sniff characteristics of the hardware and enable and disable certain aspects of the game. That's cool.
Micro-optimization and Apple's Progress
It's sad that we have to do micro-optimization for web games based on device differences. Apple has made progress with WebGPU implementation, supporting WebGL 2 and WebXR. They're catching up rapidly, and things will be exciting with WebGPU. HTML5 games can be shipped to the App Store using Cordova, which is easy and quick.
I mean, it's actual sad that we have to do this like micro-optimization based on it. Because I mean, in my mind, the reason I like the web is it's this whole publish once and it's all the same thing everywhere. But it's also, I guess, nice that we can, at least when you need to make these changes as needed based on the device.
We have one more question, it's a bit of a spicy question. Do you think Apple is purposely, this is Daniel asking, do you think Apple is purposely holding back because Fioria on iOS is not in danger of their App Store profits? Or do you think there are other reasons why they're so far behind on progressive web apps and modern APIs in general?
Yeah, that's some interesting, I mean, I think there probably are some considerations about the App Store business. I think we have to give Apple some credit here because they've really stepped up with their WebGPU implementation. So WebGL 2 is now out in production on iOS and macOS. And in addition to that, we're also seeing a commitment of Apple to implement WebXR into WebKit. So I think we're in a slightly different world to where we were say, two or three years ago when there was quite a lot of frustration that we were still lingering on WebGL 1. But yeah, I think that, let's give them some credit and say they're catching up very rapidly. And I think things are gonna be really exciting once WebGPU lands. I think it's available in a, is it like a tweak if you go into the settings browser in iOS. So yeah, I mean, things are looking pretty good now. The other thing to say is that, you know, you can make, you can ship games, HTML5 games to the app store using technologies like Cordova. Like we've got a guide in the Play Canvas developer documentation that explains that process, right? So if you just Google like Play Canvas Cordova or something, it'll take you to that page and you'd be surprised how easy the process is, right? Like you can do it in five minutes, build an IPA executable, which you can then test on your iOS device and it's yeah, super easy. That's awesome. That makes a lot of sense. Thank you so much, Will. It was so great having you here. Really appreciate it. It's super cool hearing all your insights. Thank you so much. My pleasure.