The secret life of Chrome extension service-workers
While working on the WebSocket debugger extension, I'd occasionally see extra windows pop up unexpectedly. Clearly, the extension service worker was being re-initialised at times. On restart, it would check its configuration, see the need for an open window, and create a new one — completely oblivious to the existing one. It felt almost like the service worker had a mind of its own, instead of just running consistently in the background. These restarts happened at seemingly random intervals.
Read about the role of service workers in Chrome extensions if you want to learn more about their usage. Essentially, your service worker is the heart of your extension. It runs when you install the extension and handles all the background legwork. Think of it as granting your extension superpowers — but as it’s famously said, 'with great power comes great responsibility.’
Understanding service worker lifecycle for optimal extension performance
The service worker doesn’t just spring to life during installation. It can be activated at various points, so it needs to be prepared to handle that gracefully. This means understanding its tasks upon each restart — what to initialise and what might already be in progress. The crucial point is that if it's running again, it means it was terminated at some point earlier.
To conserve memory and power, Chrome can (and will!) terminate your service worker and its state whenever your extension isn’t actively in use. There's usually a reason behind this. Here are the common scenarios that trigger termination:
- Extension update or installation: Changes to the extension's code necessitate a service worker restart.
- Chrome shutdown: Upon browser closure, service workers are terminated.
- Idle timeout (30 seconds): The primary reason service workers are terminated — this is done to conserve resources.
- Prolonged request (over 5 minutes): To prevent stalled service workers, those involved in long-running requests are terminated.
- Delayed fetch response (over 30 seconds): Similarly, fetches taking too long will trigger service worker termination.
For the full details on the lifecycle, check out the Chrome developer docs.
Chrome extension local storage for a seamless user experience
When your service worker gets terminated, all your variables and listeners vanish. This means trouble if, like the WebSocket debugger, you rely on tracking open and active debug windows and their IDs. To keep things running smoothly, you need to store this information persistently. That way, upon restart, you can retrieve it and reinitialise everything properly, and hopefully, the user won’t even notice that anything has changed.
This is where Chrome’s local storage comes to the rescue. It’s a perfect tool for saving these bits of data and other things your extension needs to remember.
Handling special cases: Updates and smooth extension maintenance
Now, we’ve handled normal initialisation, but there are special cases, for example, after an extension update, where you might want to flush out state. Thankfully, Chrome lets you install an 'onInstalled' listener within your service worker. Here's how that looks:
Important note: If your service worker's 'onInstalled' listener is set up before termination, it will be gone. You'll need to reinstall it. Ideally, this should be the very first thing your service worker does upon restart, especially if it relies on flags initialised by the listener.
In my experience, the 'onInstalled' listener seems to be activated immediately after installation, whenever it's needed, because the initialisation itself is an installation event. Luckily, I haven't encountered any race issues here. However, it's important to remember that this listener won't be called on restarts after the idle, request or fetch timeouts have caused termination.
Example: The WebSocket debugger — A case study in extension state management
For the WebSocket debugger specifically, it's essential to know whether its window is already open or needs to be opened. Since installation events force all extension-related tabs and windows to close, determining if the current restart is due to an installation becomes crucial.
Here’s how the WebSocket debugger handles this: the moment you open its window, the extension saves the window’s ID to local storage. This way, if the service worker is terminated, it knows the window exists, where to find it, and, importantly, that it’s already open and ready. Additionally, the service worker has listeners on the window to detect when the user closes it. If this happens, the window-enabled state is set to false, updating the local storage, marking the window as closed, and clearing the ID.
Watch out for unexpected twists: Network time and listener reliability
Here is a crucial point: when the service worker is terminated, all its window listeners are also lost. If a user closes the debugger window while the service worker is inactive, the close event won’t be detected. This leads to a mismatch between the stored state (which assumes the window is open) and the actual state (where the window is closed). Since we can’t definitively know the user’s intent, the safest approach is to disable the window if the stored state indicates it’s open, but it can’t be found. The key takeaway is the need to verify your state variables rigorously. Ensure any information your extension believes to be true is actually reflected in the window’s current status.
A time-keeping solution: The watchdog timer for robust extensions
There’s another ‘gotcha’ related to the WebSocket debugger’s lifecycle, and this one is machine-specific (definitely an issue on Macs). When handling WebSocket data, all timestamps are relative to a specific point in time called network monotonic time. All network events share this time, and while the exact start of the epoch isn’t critical, it’s usually 0 when the browser starts up.
The solution is straightforward: Record the wall-clock time alongside your first network monotonic timestamp. With a bit of simple maths, you can then convert any future monotonic timestamps into real time.
Handling the unexpected: When network monotonic time stops during sleep
It's a nice, simple solution ... until your laptop goes to sleep and wakes up. We’ve addressed service worker termination, but there’s another twist: network monotonic time STOPS when your machine sleeps! It restarts when the machine wakes up. This is fine if the service worker was also terminated (since all variables reset). However, if your service worker remains active, your time conversion function suddenly starts returning the WRONG time. The real clock moved forward, but network monotonic time was frozen. If you’re asleep for an hour, timestamps will now be an hour behind!
A solution: Implementing a watchdog timer
To combat this, we need a mechanism to detect sleep/wake cycles and adjust our network monotonic time reference accordingly. This is where a watchdog timer comes into play. Here’s how it works within the WebSocket debugger:
Initialisation: During service worker startup, a timer is set to run at regular intervals (e.g. every minute).
Vigilance: The timer compares the expected interval with the actual elapsed time since the last check. A significant discrepancy signals the machine likely went to sleep.
Reset: If sleep is detected, the network monotonic time reference points are cleared and reinitialised, ensuring timestamps remain in sync with real time.
Writing Chrome extensions has been a wild ride! It's a constant challenge to create something reliable and consistent when you have so little direct control over the environment where your code runs. But hey, who doesn't love a bit of a challenge?
About the author
Kate Wilkins is a principal engineer at Deriv with a lifelong love of continuous learning, not just technology but everything from computer languages, frameworks, and fundamental technologies to sailing and construction.