(I am paraphrasing question asked by Rich Harris in the "Stuff I wish I'd known sooner about service workers" gist.)
If I have code in my service worker that runs outside an event handler, when does it run?
And, closely related to that, what is the difference between putting inside an install
handler and putting it outside an event handler entirely?
The install event is the first event a service worker gets, and it only happens once. A promise passed to installEvent. waitUntil() signals the duration and success or failure of your install. A service worker won't receive events like fetch and push until it successfully finishes installing and becomes "active".
Event handler code can be made to run when an event is triggered by assigning it to the target element's corresponding onevent property, or by registering the handler as a listener for the element using the addEventListener() method.
You can look at Service Worker Detector, a Chrome extension that detects if a website registers a Service Worker by reading the navigator. serviceWorker.
The service worker lifecycle consists of mainly 3 phases, which are: Registration. Installation. Activation.
In general, code that's outside any event handler, in the "top-level" of the service worker's global scope, will run each and every time the service worker thread(/process) is started up. The service worker thread may start (and stop) at arbitrary times, and it's not tied to the lifetime of the web pages it controlled.
(Starting/stopping the service worker thread frequently is a performance/battery optimization, and ensures that, e.g., just because you browse to a page that has registered a service worker, you won't get an extra idle thread spinning in the background.)
The flip side of that is that every time the service worker thread is stopped, any existing global state is destroyed. So while you can make certain optimizations, like storing an open IndexedDB connection in global state in the hopes of sharing it across multiple events, you need to be prepared to re-initialize them if the thread had been killed in between event handler invocations.
Closely related to this question is a misconception I've seen about the install
event handler. I have seen some developers use the install
handler to initialize global state that they then rely on in other event handlers, like fetch
. This is dangerous, and will likely lead to bugs in production. The install
handler fires once per version of a service worker, and is normally best used for tasks that are tied to service worker versioning—like caching new or updated resources that are needed by that version. After the install
handler has completed successfully, a given version of a service worker will be considered "installed", and the install
handler won't be triggered again when the service worker starts up to handle, e.g., a fetch
or message
event.
So, if there is global state that needs to be initialized prior to handling, e.g., a fetch
event, you can do that in the top-level service worker global scope (optionally waiting on a promise to resolve inside the fetch
event handler to ensure that any asynchronous operations have completed). Do not rely on the install
handler to set up global scope!
Here's an example that illustrates some of these points:
// Assume this code lives in service-worker.js // This is top-level code, outside of an event handler. // You can use it to manage global state. // _db will cache an open IndexedDB connection. let _db; const dbPromise = () => { if (_db) { return Promise.resolve(_db); } // Assume we're using some Promise-friendly IndexedDB wrapper. // E.g., https://www.npmjs.com/package/idb return idb.open('my-db', 1, upgradeDB => { return upgradeDB.createObjectStore('key-val'); }).then(db => { _db = db; return db; }); }; self.addEventListener('install', event => { // `install` is fired once per version of service-worker.js. // Do **not** use it to manage global state! // You can use it to, e.g., cache resources using the Cache Storage API. }); self.addEventListener('fetch', event => { event.respondWith( // Wait on dbPromise to resolve. If _db is already set, because the // service worker hasn't been killed in between event handlers, the promise // will resolve right away and the open connection will be reused. // Otherwise, if the global state was reset, then a new IndexedDB // connection will be opened. dbPromise().then(db => { // Do something with IndexedDB, and eventually return a `Response`. }); ); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With