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

Achieve Max Performance loading your images with v-lazy-image

Don't you know what to do to improve your web performance? Learn Progressive Image Loading, a technique used by Spotify, Medium and Netflix.

Alex Jover Morales

Alex Jover Morales

Aug 30, 2021

Use Responsive Images with v-lazy-image

Is Web Performance a priority for you? Then read this article! You'll learn how to lazy load images with srcset and picture tag using v-lazy-image.

Alex Jover Morales

Alex Jover Morales

Aug 23, 2021

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

Sponsors

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

Silver
Learning Partner