Use web workers with Vue

2020 March 22

In this article I describe how to use web workers with VueJS.

A starter project is available on my github repository

Web workers

Web workers allow us to run scripts in background threads. Maybe you are like me and you thought "why not use async and await instead ?". You can't because javascript is single threaded, even when using async and await.

This means that even a heavy load asynchronous task will hog all the resources and prevent the browser from rendering properly, leading to the dreaded "A script on this page may be busy ...".

There are some limitations to using web workers :

  • First, workers execute a script. Your worker code has to be written in a separate file.
  • Secondly, workers cannot access the DOM, you will have to give it everything it needs when starting the worker and update the DOM yourself when the worker has finished.
  • Finally: code run in a worker is synchronous. To get around that we will use javascript Promises.

Starter vue project

Let's start by creating a Vue project with vue-cli. Install it with npm install -g @vue/cli if you haven't already.

We create our project with:

vue create starter-worker-vue

We choose the default presets and wait for the project to be created. Then we can move in the newly created directory cd starter-worker-vue. We can run npm run serve to verify that the project creation was successful.

Worker plugin

To use web workers with ease, we will use the Google Chrome Labs worker plugin. Install it with:

npm install -D worker-plugin

The webpack configuration has to be updated to use the plugin. To do so we create a file vue.config.js at the root of our project with the following content:

vue.config.js
const WorkerPlugin = require('worker-plugin')
module.exports = {
  configureWebpack: {
    output: {
      globalObject: "this"
    },
    plugins: [
      new WorkerPlugin()
    ]
  }
};

Our plugin is configured let's try using it.

Worker code

We create a folder named long-sleep-worker in src with two files: index.js and longSleepWorker.js.

The longSleepWorker.js file contains the worker logic. In our case, we will just sleep for a given time and then reverse an array given as a parameter:

longSleepWorker.js
// Demo how to import in the worker
import _ from 'lodash'

// Function to simulate a long work
// and demo the use of async / await in a worker
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

addEventListener("message", async event => {
    let sleepTime = event.data.message.sleep
    let arrayToReverse = event.data.message.array
    // Sleep
    await sleep(sleepTime);
    // Reverse
    let reversedArray = _.reverse(arrayToReverse)
    // Send the reversed array
    postMessage(reversedArray)
});

I added an import to Lodash in the worker code to show you that it is possible to use imports. Install lodash in your project with npm install --save lodash.

The index.js file declares our module. Here we export the function send and the worker itsef.

index.js
const worker = new Worker('./longSleepWorker.js', { type: 'module' });

const send = message => worker.postMessage({
  message
})

export default {
  worker,
  send
}

The function send will be useful to start the worker and pass some parameters. The worker will be used to set up a listener to detect when the worker has finished its work. In our case we know that the worker has finished when it sends us the reversed array thanks to the function postMessage.

Vue code

Our worker code is written. Let's integrate it to our Vue application. We create a new component src/components/WorkerDemo.vue to use our web worker and we add it to our application in src/App.vue.

In the script we import WorkerDemo:

App.vue
import HelloWorld from './components/HelloWorld.vue'
import WorkerDemo from './components/WorkerDemo.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    WorkerDemo
  }
}

In the template we replace the HelloWorld by our new component:

App.vue
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <WorkerDemo />
  </div>

To use our web worker we need to import the module long-sleep-worker we created earlier. We can then start the worker by sending it a payload:

import longSleepWorker from '@/long-sleep-worker'

...

longSleepWorker.send({sleep: 500, array: [1,2,3]})

To detect when the worker has finished its work we will need to set up a listener with worker.onmessage:

longSleepWorker.worker.onmessage = event => {
  let message = event.data
  if (message.status="finished") {
    // do something
  } else {
    // maybe update the progress bar ...
  }
}

We set up the listener in the mounted() lifecycle hook. This listener could also be used to update a progress bar !

In the end our WorkerDemo.vue file looks like this:

WorkerDemo.vue
<template>
  <div >
    <div>Sleep time (ms)</div>
    <input v-model="sleepTime" />
    <div>Array to reverse</div>
    <input v-model="arrayToReverse" />
    <div>
    <button @click="startWorker">Start worker</button>
    </div>
    <div>Worker message</div>
    <div>{{workerMessage}}</div>
  </div>
</template>

<script>
import longSleepWorker from '@/long-sleep-worker';

export default {
  data() {
    return {
      workerMessage: "No message yet",
      sleepTime: 1000,
      arrayToReverse: "1,2,3,4"
    }
  },
  mounted() {
    longSleepWorker.worker.onmessage = event => {
      this.workerMessage = event.data
    }
  },
  methods: {
    startWorker() {
      longSleepWorker.send({
        sleep: this.sleepTime,
        array: this.arrayToReverse.split(',')
      })
    }
  }
}
</script>

Testing it

Our code is now complete. We can start the server with npm run serve and click on the button Start worker to verify that our code was executed.

A repository with the starter code is available here.

Resources

I used and studied the following resources while writing this article: