Use Web Workers in your Vue.js Components for Max Performance

Alex Jover Morales

Alex Jover Morales

Mar 31, 2020
4 min read
Share on Twitter or LinkedIn

In the first vuedose VueDose tip ever I revealed a trick to improve performance on large lists. Yay, that was a great start!.

However not all comes into that case.

Sometimes you need to use a component that is heavy to create and render, usually because they perform complex tasks. I came up into that case yesterday.

I was creating a page using StoryBlok. They have the amazing feature of creating a rich-text field that content managers can use in order to enter any kind of formatted text such as lists, images, block quotes, bold, italics...

When you get the rich-text content from the StoryBlok API, it has its own structure. In order to render that data into HTML, you must use the richTextResolver.render(content) method of storyblok-js-client.

We can encapsulated this functionality into a RichText.vue component. A basic implementation would be:

<template>
  <div v-html="contentHtml"></div>
</template>

<script>
  export default {
    props: ["content"],
    computed: {
      contentHtml() {
        return this.$storyapi.richTextResolver.render(content);
      }
    }
  };
</script>

Note: $storyapi it's an instance of the StoryBlok Client that comes from StoryBlok Nuxt.js module, which is what I'm using, but that's not relevant for the purpose of this article

Nothing too fancy so far. Just... here comes the surprise.

It seems like that render it's a heavy task, which was starting to be noticable when rendering several of this components with a decent amount of content.

Now imagine this situation: you have a list of text-rich components in your page, and a dropdown to filter by. When you change your dropdown filter, you re-fetch all content given that filter, and the list re-renders.

Here's where you could notice the heavyness of richTextResolver.render: the dropdown was lagging to close after selecting it's value.

The reason is that the default JavaScript execution runs in the main thread, which is UI-blocking.

Problem understood. So... how can we fix it?

Easy: using a Web Worker for the rich-text rendering task.

Note: I'm not diving into Web Workers, but check its docs for more info.

Web Workers run in a separate thread and they're not UI-blocking, perfect for our case. We're not diving into web workers, but check its docs for more info.

Keep in mind that web workers run in their own context, and by default we cannot access external contexts. But we need to access the storyblok-js-client npm module. For that, Webpack comes to the rescue with worker-loader.

First, install it by running `npm install -D worker-loader. Then you'd need to configure it. In Nuxt.js you'd do it in nuxt.config.js in this way:

build: {
  extend(config, { isDev, isClient }) {
    config.module.rules.push({
      test: /\.worker\.js$/,
      use: { loader: "worker-loader" }
    });
  }
}

With this configuration, all *.worker.js files will be processed by worker-loader.

Let's create a render-html.worker.js:

import StoryblokClient from "storyblok-js-client";

let storyClient = new StoryblokClient({});

// When the parent theard requires it, render the HTML
self.addEventListener("message", ({ data }) => {
  const result = storyClient.richTextResolver.render(data);
  self.postMessage(result);
});

That's a basic implementation of a worker. You need to listen to message event, which is the way you'll communicate to it from your Vue.js app. Then you can get the data of the event, render it with storyblok-js-client and send up the result self.postMessage.

Let's update the RichText.vue component to use the service worker:

<template>
  <div v-html="contentHtml"></div>
</template>

<script>
  import Worker from "./render-html.worker.js";

  // Create the worker instance
  const worker = new Worker();

  export default {
    props: ["content"],
    data: () => ({
      contentHtml: ""
    }),
    mounted() {
      // Update the state with the processed HTML content
      worker.onmessage = ({ data }) => {
        this.contentHtml = data;
      };
      // Call the worker to render the content
      worker.postMessage(this.content);
    }
  };
</script>

# The result

Do you want to know how much performance improvement we achieved by this? Sure, Web Performance doesn't make sense if you don't measure it.

In fact, I have an article for you to learn and undestand how to meassure performance in Vue.js components. So make sure to read it to better understand the following test.

I've meassured this component with a medium-size on my mac, 6x throttle.

The results are: 20.65x faster on component's render and 1.39x on patch.

If you don't know what render and patch mean, it's explained in this article

That's it for today's tip!

Don't miss out anything about Vue, your favourite framework.

Subscribe to receive all the articles we publish in a concise format, perfect for busy devs.

Related Articles

Naming Webpack Chunks on Lazy Loaded Routes in Vue.js

Learn in this micro tip how you can name webpack chunks generated by lazy loaded routes

Jorge Baumann

Jorge Baumann

Apr 19, 2020

Lazy load images using v-lazy-image Vue.js component

Learn to improve the web performance of your vue.js application

Alex Jover Morales

Alex Jover Morales

Apr 16, 2019

Dynamic Imports in Vue.js for better performance

Tutorial on code splitting using dynamic import in vue.js applications in order to have better performance.

Paul Melero

Paul Melero

Apr 15, 2019

Sponsors

VueDose is proudly supported by its sponsors. If you enjoy it, consider supporting it to ensure the project maintainability.

Silver
Learning Partner