I optimized the loading time of Unity game from over 20 seconds to just 1.2 seconds, achieving a ~94% improvement.
Want to know how?
🔍 Have you heard of service workers?
In this post, we'll dive into what service workers are 🤔, how they work ⚙️, and how I used them to improve the performance of my web application.
What is a Service Worker? 🤖
A service worker is a background script that runs separately from the web page, and can:
- Cache assets 🗂️
- Intercept network requests 🌐
- Deliver offline functionality 🚫
Key Characteristics of Service Workers:
- Runs in the background: Service workers are not tied to the page's lifecycle, meaning they can continue running even if the page is closed
- Event-driven: Service workers listen to specific events, such as network requests (fetch event)
- No DOM access: Unlike other web workers, service workers don't have access to the DOM directly. They operate in the background, managing resources and network traffic
Here's a simple example of Service Worker script:
ServiceWorkerExample.js🔍
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function(error) {
console.error('Service Worker registration failed:', error);
});
});
}
In this snippet, the service worker is registered when the page loads. If the registration is successful, it logs the service worker's scope; otherwise, it logs an error.
My Challenge: Integrating a Unity WebGL Game into a web page
I was integrating a Unity WebGL game into a React.js website. The game consisted of large ".wasm" and ".data" files alongside JavaScript files that were crucial for the game to load. However, the first time the page loaded, it took over 20 seconds! 🚨 This was primarily due to the size of the ".wasm" and ".data" files.
The Solution: Caching with Service Workers
I created a "ServiceWorker.js" file, which caches all the necessary game assets to reduce the loading time. Here's what the file contains:
const cacheName = "$Project_name";
const contentToCache = [
"Build/Game.loader.js",
"Build/Game.framework.js",
"Build/Game.data",
"Build/Game.wasm",
"TemplateData/game_style.css"
];
self.addEventListener('install', function (e) {
console.log('[Service Worker] Install');
e.waitUntil((async function () {
try {
const cache = await caches.open(cacheName);
console.log('[Service Worker] Caching all: app shell and content');
for (const resource of contentToCache) {
try {
await cache.add(resource);
} catch (error) {
console.error(`[Service Worker] Failed to cache: ${resource}`, error);
}
}
} catch (error) {
console.error('[Service Worker] Failed to open cache', error);
}
})());
});
self.addEventListener('fetch', function (e) {
e.respondWith((async function () {
try {
const response = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (response) return response;
const fetchResponse = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
await cache.put(e.request, fetchResponse.clone());
return fetchResponse;
} catch (error) {
console.error(`[Service Worker] Error fetching resource: ${e.request.url}`, error);
throw error;
}
})());
});
What does this code do?
This service worker script performs the following actions:
- Caches essential files needed for the game to run, including ".wasm", ".data", and JS files during the install phase.
- Intercepts fetch requests to serve cached resources if available. If the resource isn't in the cache, it fetches it from the network, caches it, and returns the response.
The Result: Significant Performance Boost
By caching these assets, the difference was night and day - what once took over 20 seconds now loaded in just 1.2 seconds🚀. The next time users visit the page, the assets are already stored locally, drastically speeding up the experience.
⚠️ Be cautious! The Caching Problem
While this approach works for improving performance, it introduced another issue: outdated cached files.
Since we're actively developing the Unity game and releasing new versions, the cached files needed to be cleared every time a new version was deployed.
To solve this, I created a pipeline script that automatically unregisters the old service worker and clears the cache whenever we publish a new game build. This ensures that users always load the latest assets without being stuck with old, cached versions.
Conclusion
Implementing service workers in your web application can dramatically improve performance, especially when dealing with large assets like in my Unity WebGL game. However, it's important to handle caching carefully to avoid serving outdated files.
Have you had similar experiences optimizing load times? Did you use service workers or other techniques? Share your thoughts and insights in the comments below! 💬
Hope you learned something new today! ✌️
Top comments (0)