Keep your app smooth and responsive with web workers
Matt Levy
Posted on July 20, 2021
How do you keep your web app smooth and responsive?
How do you ensure a steady and sufficiently high frame rate? How do you ensure the UI responds to user interactions with minimal delay?
These are key factors in making your app feel polished and high-quality.
The web is single-threaded which makes it hard to write smooth and responsive apps. JavaScript was designed to run in-step with the browser's main rendering loop which means that a small amount of slow JavaScript code can prevent the browser's rendering loop from continuing.
Web applications are expected to run on all devices, from the latest iPhone to a cheap smart phone. How long your piece of JavaScript takes to finish depends on how fast the device is that your code is running on.
Web workers can be an important and useful tool in keeping your app smooth and responsive by preventing any accidentally long-running code from blocking the browser from rendering. Web Workers are a simple means for apps to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
Web workers have not been widely adopted and there isn’t a lot of guidance on architecture for workers. It can be hard to identify which parts of your app will work in a worker. In addition, due to the asynchronous way of communicating with a worker, adoption of workers requires some architectural adjustments in your app.
Application state in a web worker
A key architecture change you can make in your app is to manage data in a worker and keep rendering updates to a minimum.
By moving your application state to a worker means you move complex business logic and data loading/processing there too.
State can be managed effectively in a web worker as proxies and fetch are available - both essential APIs for loading data and reactivity.
To communicate with the user interface, data is sent between workers and the main UI thread via a system of messages — both sides send their messages using the postMessage()
method, and respond to messages via the onmessage
event handler.
A typical store for managing application state in the UI thread could look like this.
import { createAppState } from 'https://cdn.skypack.dev/ficusjs@3'
const store = createAppState('worker.test', {
initialState: {
text: 'hello world'
},
setText (text) {
this.state.text = text
}
})
Lets create an equivalent web worker store for managing application state.
Create a worker file worker.js
:
// import the web worker friendly app state creator function
importScripts('https://unpkg.com/@ficusjs/state@1.2.0/dist/worker-app-state.iife.js')
// Initialize the store using the ficusjs.createAppState function
const store = globalThis.ficusjs.createAppState({
initialState: {
text: 'hello world'
},
setText (text) {
this.state.text = text
}
})
// a function for communicating with the UI thread
function postState () {
globalThis.postMessage(Object.assign({}, store.state))
}
// subscribe to store changes
store.subscribe(postState)
// listen for actions to invoke in the store
globalThis.onmessage = function (e) {
const { actionName, payload } = e.data
store[actionName](payload)
}
// post the initial state to any components
postState()
UI components
Create components that are only responsible for rendering UI using the withWorkerStore
function to extend the component with the worker.
import { createCustomElement, withWorkerStore } from 'https://cdn.skypack.dev/ficusjs@3'
import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers@3/htm'
createCustomElement('example-worker-component',
withWorkerStore(new Worker('./worker.js'), {
renderer,
onButtonClick () {
this.dispatch('setText', 'This is a test')
},
render () {
return html`
<section>
<p>${this.state ? html`${this.state.text}` : ''}</p>
<button type="button" onclick="${this.onButtonClick}">Dispatch worker action</button>
</section>
`
}
})
)
The withWorkerStore
function provides a this.state
property within the component as well as a this.dispatch()
method for invoking store actions.
It also makes the component reactive to store changes as well as handling automatic store subscriptions based on the component lifecycle hooks.
It will also refresh computed getters when store state changes.
Conclusion
The investment in workers can provide you with a way to make your app smooth and responsive as well as supporting the range of devices that your app is accessed from.
To view documentation on using web workers with FicusJS, visit https://docs.ficusjs.org/app-state/web-workers/
Posted on July 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.