​SvelteKit Web Worker

Photo by Christopher Burns on Unsplash
​I ❤️ web workers!
These background threads are my go to strategy to compute anything I consider as too expensive for a frontend web application (e.g. Tie Tracker) or if it has to execute a recurring tasks (e.g. Papyrs or Cycles.watch).
​In these two last projects, I used SvelteKit. So here is how you can develop and communicate with web workers using such an application framework.

​Create a web worker

There is no particular requirements nor convention but I like to suffix my worker file​s with the extension ​.worker.ts​ - e.g. to create a bare minimum worker I create a file named ​my.worker.ts​ which finds place in the ​src/lib​ directory.
onmessage = () => { console.log('Hello World 👋'); }; export {};
onmessage is a function that fires each time the web worker gets called - i.e. each time postMessage is used to send a message to the worker from the window side.
​The empty ​export {}​ is a handy way to make the worker a module and solve following TypeScript error if not declared:
TS1208: 'my.worker.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.​

​Dynamically import

​To integrate the worker in a component it needs to be dynamically imported. SvelteKit relying on ViteJS for its tooling, it can be imported with the ​?worker​ suffix (see documentation).
<script lang="ts"> import { onMount } from 'svelte'; let syncWorker: Worker | undefined = undefined; const loadWorker = async () => { const SyncWorker = await import('$lib/my.worker?worker'); syncWorker = new SyncWorker.default(); }; onMount(loadWorker); </script> <h1>Web worker demo</h1>
​In above snippet I affect the worker to a local variable in case I would like to use it elsewhere in the component. Notably on ​onDestroy​ callback if I would clean up or propagate the destruction to the worker.

​PostMessage: window -> web worker

​To send a message from the window side to the web worker we need a reference to the loaded module. For such purpose we can use the variable I declared in previous chapter. e.g. we can send an empty message - empty object - right after the initialization.
const loadWorker = async () => { const SyncWorker = await import('$lib/my.worker?worker'); syncWorker = new SyncWorker.default(); syncWorker.postMessage({}); };
​Assuming everything goes according plan, the web worker should receive the message and log to the console the "Hello World 👋".
If you face the issue "SyntaxError: import declarations may only appear at top level of a module" while following this article, note that the use of web workers in development is only supported in selected browsers (see issue #4586 in ViteJS).

PostMessage: web worker -> window

​Messages are bidirectional - i.e. web worker can also send messages to the window side. To do so, ​postMessage​ is used as well. The only difference with previous transmission channel is that it does not need an object or module as reference to trigger the message.
onmessage = () => { console.log('Hello World 👋'); postMessage({}); }; export {};
​In above snippet I send an empty message object from the web worker to the window each time the worker receives a messages (#yolo).
​To intercept these messages on the window side, a function that fires every time such messages are send need to be registered. The loaded worker exposes an ​onmessage​ property for such purpose.
<script lang="ts"> import { onMount } from 'svelte'; let syncWorker: Worker | undefined = undefined; const onWorkerMessage = () => { console.log('Cool it works out 😃'); }; const loadWorker = async () => { const SyncWorker = await import('$lib/my.worker?worker'); syncWorker = new SyncWorker.default(); syncWorker.onmessage = onWorkerMessage; syncWorker.postMessage({}); }; onMount(loadWorker); </script>
​If I refresh my test browser and console, previous message "Hello World 👋" is still printed but, in addition, the new message "Cool it works out 😃" is added to the console as well.

​TypeScript

​Sending empty messages is cute for demo purpose but you might want to define some TypeScript definition to improve the code.
​What I generally do is defining some types for the requests and responses that are identified with a typed identificator and contains information next to them. e.g. I often use a variable named ​msg​ and a ​data​ object that effectively contains the information.
export interface PostMessageDataRequest { text: string; } export interface PostMessageDataResponse { text: string; } export type PostMessageRequest = 'request1' | 'start' | 'stop'; export type PostMessageResponse = 'response1' | 'response2'; export interface PostMessage<T extends PostMessageDataRequest | PostMessageDataResponse> { msg: PostMessageRequest | PostMessageResponse; data?: T; }
​Again here, no particular reason or best practice, just a thing I do to clean up my code 😄.
Thanks to these definitions, I can now set types for the ​onmessage​ function of the web worker.
import type { PostMessage, PostMessageDataRequest } from './post-message'; onmessage = ({ data: { data, msg } }: MessageEvent<PostMessage<PostMessageDataRequest>>) => { console.log(msg, data); const message: PostMessage<PostMessageDataRequest> = { msg: 'response1', data: { text: 'Cool it works out v2 🥳' } }; postMessage(message); }; export {};
​Likewise, types can be defined in the component.
<script lang="ts"> import { onMount } from 'svelte'; import type { PostMessage, PostMessageDataRequest, PostMessageDataResponse } from '../lib/post-message'; let syncWorker: Worker | undefined = undefined; const onWorkerMessage = ({ data: { msg, data } }: MessageEvent<PostMessage<PostMessageDataResponse>>) => { console.log(msg, data); }; const loadWorker = async () => { const SyncWorker = await import('$lib/my.worker?worker'); syncWorker = new SyncWorker.default(); syncWorker.onmessage = onWorkerMessage; const message: PostMessage<PostMessageDataRequest> = { msg: 'request1', data: { text: 'Hello World v2 🤪' } }; syncWorker.postMessage(message); }; onMount(loadWorker); </script>
​As I now use objects and modified the information that are transmitted, the messages printed in the console of my browser now reflects these changes.

​Cronjob

​As mentioned in the introduction, I use web worker to execute recurring tasks - to schedule cronjob. e.g in Papyrs, I use web worker jobs to synchronize the content of the blog posts that are edited and saved on the client side with the Internet Computer. Thanks to this approach, the user gets a smooth experience as the UI is not affected by any network communication.
​When I implement such a timer in a web worker, I always define "start" and "stop" functions. The first being called when the web worker is loaded on the window side and the second being called when the related component that uses it gets destroyed.
import type { PostMessage, PostMessageDataRequest } from './post-message'; onmessage = ({ data: { msg } }: MessageEvent<PostMessage<PostMessageDataRequest>>) => { switch (msg) { case 'start': startTimer(); break; case 'stop': stopTimer(); } }; let timer: NodeJS.Timeout | undefined = undefined; const print = () => console.log(`Timer ${performance.now()}ms ⏱`); const startTimer = () => (timer = setInterval(print, 1000)); const stopTimer = () => { if (!timer) { return; } clearInterval(timer); timer = undefined; }; export {};
​To schedule the time I use setInterval which repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. In above example, it log to the console every second.
​On the window side, I use ​postMessage​ again to trigger the two events.
<script lang="ts"> import { onDestroy, onMount } from 'svelte'; let syncWorker: Worker | undefined = undefined; const loadWorker = async () => { const SyncWorker = await import('$lib/my.worker?worker'); syncWorker = new SyncWorker.default(); syncWorker.postMessage({ msg: 'start' }); }; onMount(loadWorker); onDestroy(() => syncWorker?.postMessage({ msg: 'stop' })); </script>
After updating my browser one last time, I find out ​console.log​ that are effectively printed according the interval I defined in my web worker.

​Conclusion

​Web worker are pure coding joy to me 🤓.
To infinity and beyond
David​

For more adventures, follow me on Twitter
Made with Papyrs