Photo by Tobias Tullius on Unsplash
I share one trick a day until (probably not) the end of the COVID-19 quarantine in Switzerland, April 19th 2020. Twelve days left until hopefully better days.
I recently published Tie Tracker, a simple, open source and free time tracking app ⏱.
Among its features, the full offline mode was particularly interesting to develop. From an architectural point of view, I had to find a solution to compute, for statistical or exportation purposes, the many entries the users are potentially able to record without blocking the user interface.
That’s why I had the idea to solve my problem with the help of the Web Workers API.
The app is developed with Ionic + React, therefore let me share with you my recipe 😉.
Simulate A Blocked User Interface
Before trying Web Workers out, let’s first try to develop a small application which contains an action which actually block the user interface.
In the following component, we are handling two states, two counters. One of these is incremented on each button click while the other call a function incApple()
which loops for a while and therefore block the user interaction.
import {
IonContent,
IonPage,
IonLabel,
IonButton
} from '@ionic/react';
import React, {useState} from 'react';
import {RouteComponentProps} from 'react-router';
import './Page.css';
const Page: React.FC<RouteComponentProps<{ name: string; }>> = ({match}) => {
const [countTomato, setCountTomato] = useState<number>(0);
const [countApple, setCountApple] = useState<number>(0);
function incApple() {
const start = Date.now();
while (Date.now() < start + 5000) {
}
setCountApple(countApple + 1);
}
return (
<IonPage>
<IonContent className="ion-padding">
<IonLabel>Tomato: {countTomato} | Apple: {countApple}</IonLabel>
<div className="ion-padding-top">
<IonButton
onClick={() => setCountTomato(countTomato + 1)}
color="primary">Tomato</IonButton>
<IonButton
onClick={() => incApple()}
color="secondary">Apple</IonButton>
</div>
</IonContent>
</IonPage>
);
};
export default Page;
As you can notice in the following animated Gif, as soon as I start the “Apple counter”, the user interaction on the “Tomato counter” have no effects anymore, do not trigger any new component rendering, as the function is currently blocking the JavaScript thread.
Defer Work With Web Workers
Having the above example in mind, let’s try out Web Workers in order to defer our “Apple counter” function.
Web Workers
To easiest way to add a Web Worker to your application is to ship it as an asset. In the case of my Ionic React application, these find place in the directory public
, that’s we create a new file ./public/workers/apple.js
.
Before explaining the flow of the following code, two things are important to notice:
The application and the Web Workers are two separate things. They don’t share states, they don’t share libraries, they are separate and can communicate between them through messages only.
Web Workers do not have access to the GUI, to the
document
, to thewindow
.
If you are familiar with Firebase, you can kind of understand, to some extent, the Web Worker as your own private, not Cloud, but local functions.
The entry point of our web worker is onmessage
which is basically a listener to call triggered from our application. In the function we are registering, we are checking if a corresponding msg
is provided, this let us use a web worker for many purposes, and are also amending the current counter value before running the same function incApple()
as before. Finally, instead of updating the state directly, we are returning the value to the application through a postMessage
.
self.onmessage = async ($event) => {
if ($event && $event.data && $event.data.msg === "incApple") {
const newCounter = incApple($event.data.countApple);
self.postMessage(newCounter);
}
};
function incApple(countApple) {
const start = Date.now();
while (Date.now() < start + 5000) {}
return countApple + 1;
}
Interacting With The Web Workers
To interact with the web worker, we first need to add a reference point to our component.
const appleWorker: Worker = new Worker('./workers/apple.js');
Because we are communicating with the use of messages, we should then register a listener which would take care of updating the counter state when the web worker emits a result.
useEffect(() => {
appleWorker.onmessage = ($event: MessageEvent) => {
if ($event && $event.data) {
setCountApple($event.data);
}
};
}, [appleWorker]);
Finally we update our function incApple()
to call the web worker.
function incApple() {
appleWorker.postMessage({ msg: "incApple", countApple: countApple });
}
Tada, that’s it 🎉. You should now be able to interact with the GUI even if the “blocker code is running”. As you can notice in the following animated Gif, I am still able to increment my tomato counter even if the blocking loops is performed by the web worker.
The component altogether in case you would need it:
import {
IonContent,
IonPage,
IonLabel,
IonButton
} from '@ionic/react';
import React, {useEffect, useState} from 'react';
import {RouteComponentProps} from 'react-router';
import './Page.css';
const Page: React.FC<RouteComponentProps<{ name: string; }>> = ({match}) => {
const [countTomato, setCountTomato] = useState<number>(0);
const [countApple, setCountApple] = useState<number>(0);
const appleWorker: Worker = new Worker('./workers/apple.js');
useEffect(() => {
appleWorker.onmessage = ($event: MessageEvent) => {
if ($event && $event.data) {
setCountApple($event.data);
}
};
}, [appleWorker]);
function incApple() {
appleWorker
.postMessage({msg: 'incApple', countApple: countApple});
}
return (
<IonPage>
<IonContent className="ion-padding">
<IonLabel>Tomato: {countTomato} | Apple: {countApple}</IonLabel>
<div className="ion-padding-top">
<IonButton
onClick={() => setCountTomato(countTomato + 1)}
color="primary">Tomato</IonButton>
<IonButton
onClick={() => incApple()}
color="secondary">Apple</IonButton>
</div>
</IonContent>
</IonPage>
);
};
export default Page;
Summary
Web Workers is really an interesting concept. Tie Tracker let me experiment them and I am definitely going to use them again in future projects. Its code is open source and available on GitHub. If you have any feedback and even better, are interested to contribute, send me your best Pull Requests, that would be awesome 😎.
Stay home, stay safe!
David