DEV Community

Matt Levy for Ficus

Posted on

Keep your app smooth and responsive with web workers

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
  }
})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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>
      `
    }
  })
)
Enter fullscreen mode Exit fullscreen mode

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/

Top comments (0)