Enhancing WebGL Load Times: Strategies and Insights
Written on
Chapter 1: Understanding WebGL Load Times
One of the primary challenges faced by WebGL applications is their loading duration. In contrast to local OpenGL applications that can quickly access large assets from an SSD, WebGL must retrieve these assets over the internet, which can significantly delay the experience for users.
Recently, I have been developing a new portfolio site heavily utilizing WebGL. When users visit and experience a delay while assets download, they might leave before the content even renders. To enhance user satisfaction, I dedicated substantial effort to reducing loading times and achieved remarkable results.
Asset Loading Strategy
Previously, the loading process for models and other assets was fairly straightforward, resembling this:
var model = Sparrow.ModelLoader.loadGLTF(engine, "model.glb", () => {
// onload
});
The loadGLTF function initiated an XMLHttpRequest, prompting the model's download. While the asynchronous nature of XMLHttpRequests allows the code to continue executing, it can become a bottleneck when multiple assets are required simultaneously. For instance:
var model1 = loadGLTF();
var model2 = loadGLTF();
var texture1 = loadTexture();
var animatedModel1 = loadAnimatedModel();
var texture2 = loadTexture();
// potentially many more assets
Since the first call to loadGLTF() doesn’t pause execution, all subsequent downloads commence right away, leading to shared bandwidth and slowed performance.
Recognizing that asset importance varies, I understood that essential components like the terrain mesh and key landmarks should be prioritized, while less critical details can be loaded later.
To address this, I developed a new AssetLoader class for my Sparrow WebGL engine. This class centralizes all asset downloading responsibilities, introducing a priority parameter to the loading functions that can be set to REQUIRED, IMPORTANT, or DEFAULT.
Now, invoking loadModel() does not trigger an immediate download. Instead, requests are queued, and only the REQUIRED assets are downloaded when the engine’s main loop runs. While these assets are loading, a loading screen is displayed, ensuring users remain engaged until all crucial components are ready for rendering.
Loading Screen Implementation
Equally important as optimizing load times is providing user feedback. Some assets take longer to download, so I implemented a loading screen that displays a progress bar indicating the number of required assets downloaded. Currently, it treats all assets equally by counting them, but I plan to enhance it to reflect the total bytes downloaded.
GLB File Support
To further improve efficiency, I aimed to reduce the file sizes of some assets. The glTF format, a standard for WebGL models, has three versions: Embedded, binary, and separate. Initially, my engine only supported embedded files, which, while easy to manage, aren’t optimal for performance.
Recently, I added support for binary glTF files (.glb). This format stores data in a more efficient manner, significantly improving loading times. Upon rewriting my glTF parser, I ensured it could handle all three file types seamlessly, enhancing parsing speed from ~40ms for embedded files to ~10ms for binary GLB files.
Furthermore, I discovered that the option to export embedded glTF files was removed in the latest Blender version, making my timing for implementing GLB support fortuitous.
Draco Mesh Compression is another feature I plan to explore for further compressing data, particularly beneficial for large models with extensive vertices.
Water Texture Generation
Lastly, I tackled texture size by generating the two largest assets— a 4.5MB texture and a 7.6MB normal map for water—procedurally in JavaScript. This approach proved to be faster than downloading the original files, as creating a 2048x2048 normal map takes between 300 to 500ms.
Though I’m still fine-tuning the water texture options, including potentially reducing resolution, the procedural generation process currently occurs on the main thread. I may explore using a WebWorker for this task in the future.
This discussion may lack visually striking screenshots, yet the insights shared here are crucial. An impressive WebGL scene can lose its appeal if loading times frustrate users. I’m pleased with the progress I’ve made in optimizing load times, but I recognize the ongoing potential for further enhancements.
In the video "Boost the Performance of Your WebGL Unity Games!" by Florin Ciornei from the JS GameDev Summit 2023, strategies are discussed to enhance WebGL performance, which aligns with the optimizations I've implemented in my projects.
Additionally, "Optimizing Unity for WebGL" provides valuable insights into improving Unity applications for WebGL, further enriching the understanding of performance enhancement techniques.