DEV Community

Cover image for Exploring Web Workers in React with Vite: The Key for Better Performance
Francisco Mendes
Francisco Mendes

Posted on • Updated on

Exploring Web Workers in React with Vite: The Key for Better Performance

Introduction

Web Workers are a powerful and flexible tool that can enhance the functionality of your application. In this article, we will explore how to incorporate a Web Worker into a React application and compare the performance of an expensive function when it is executed in the main thread versus in a Web Worker.

By the end of this tutorial, you will have a better understanding of how Web Workers can improve the performance of your React app.

Assumed knowledge

The following would be helpful to have:

  • Basic knowledge of React
  • Basic knowledge of Web Workers

Getting Started

To streamline the process of configuring the Web Worker and simplify communication between the app and the Web Worker, we will be using the comlink library.

Project Setup

Run the following command in a terminal:



yarn create vite app-sw --template react-ts
cd app-sw


Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:



yarn install comlink
yarn install -D vite-plugin-comlink


Enter fullscreen mode Exit fullscreen mode

The first change to be made is in vite.config.ts where we are going to import the comlink plugin and add it to the vite configuration:



// @/vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { comlink } from "vite-plugin-comlink";

export default defineConfig({
  plugins: [react(), comlink()],
  worker: {
    plugins: [comlink()],
  },
});


Enter fullscreen mode Exit fullscreen mode

The next step is to go to vite-env.d.ts and add the reference to the vite plugin we installed:



// @/src/vite-env.d.ts

/// <reference types="vite/client" />
/// <reference types="vite-plugin-comlink/client" /> πŸ‘ˆ added this


Enter fullscreen mode Exit fullscreen mode

Next, we'll create a file called utils.ts and define two functions inside it. The first function, called blockingFunc(), will be computationally expensive and will easily block the main thread.



// @/src/utils.ts

export const blockingFunc = () => {
  new Array(100_000_000)
    .map((elm, index) => elm + index)
    .reduce((acc, cur) => acc + cur, 0);
};

// ...


Enter fullscreen mode Exit fullscreen mode

The randomIntFromInterval() function generates a random integer within a specified range. For example, you might use this function to generate a random number between 1 and 10, like so: randomIntFromInterval(1, 10).



// @/src/utils.ts

export const blockingFunc = () => {
  new Array(100_000_000)
    .map((elm, index) => elm + index)
    .reduce((acc, cur) => acc + cur, 0);
};

export const randomIntFromInterval = (min: number, max: number): number => {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

// ...


Enter fullscreen mode Exit fullscreen mode

Still in this file, we will create the instance of the web worker that will be used in the app, which we will name workerInstance.



// @/src/utils.ts

export const blockingFunc = () => {
  new Array(100_000_000)
    .map((elm, index) => elm + index)
    .reduce((acc, cur) => acc + cur, 0);
};

export const randomIntFromInterval = (min: number, max: number): number => {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

// worker instance
export const workerInstance = new ComlinkWorker<typeof import("./sw/worker")>(
  new URL("./sw/worker", import.meta.url)
);


Enter fullscreen mode Exit fullscreen mode

Before we can use workerInstance, we need to define our worker. As shown in the previous code snippet, let's create a folder called sw/ and a file called worker.ts inside it.



// @/src/sw/worker.ts

/// <reference lib="webworker" />
declare const self: DedicatedWorkerGlobalScope;

import { blockingFunc } from "../utils";

export const someRPCFunc = () => {
  blockingFunc();
};


Enter fullscreen mode Exit fullscreen mode

As you can see in the code snippet, the content inside it can be executed in the worker. We imported the blockingFunc() function as a named export called someRPCFunc().

What is someRPCFunc()? It's a method that can be remotely invoked through our worker instance using RPC (Remote Procedure Call), meaning it can be called from the main thread to the web worker.

Finally, we need to go to App.tsx to put everything we created to use. First, we need to import the necessary items:



// @/src/App.tsx
import { useCallback, useState } from "react";

import { workerInstance, blockingFunc, randomIntFromInterval } from "./utils";

// ...


Enter fullscreen mode Exit fullscreen mode

Now, we'll define three functions that will serve as our callbacks. The first will utilize the web worker to call the costly function. The second will run the expensive function within the main thread. The third will generate a random number and save it in the component's state. Once these functions are defined, we can bind them to their respective buttons.



// @/src/App.tsx
import { useCallback, useState } from "react";

import { workerInstance, blockingFunc, randomIntFromInterval } from "./utils";

export const App = () => {
  const [random, setRandom] = useState<number>(0);

  const workerCall = useCallback(async () => {
    await workerInstance.someRPCFunc();
  }, []);

  const normalFuncCall = useCallback(() => {
    blockingFunc();
  }, []);

  const randomIntHandler = useCallback(() => {
    setRandom(randomIntFromInterval(1, 100));
  }, []);

  return (
    <section>
      <button onClick={workerCall}>Worker Call</button>
      <button onClick={normalFuncCall}>Main Thread Call</button>
      <button onClick={randomIntHandler}>Random Int {random}</button>
    </section>
  );
};


Enter fullscreen mode Exit fullscreen mode

If you've been following along with the steps in the article, you should be able to achieve a result similar to this one:

gif

Expected behavior

When executing on the main thread, there should be a slight lag with the interaction of the random number button. However, when executing in the web worker there shouldn't be any delay because the execution is done in a different thread.

Conclusion

I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

Github Repo

Top comments (8)

Collapse
 
oleksandrdanylchenko profile image
oleksandr-danylchenko

For the proper Vite setup you also need to add the comlink() plugin under the worker prop:

export default defineConfig({
  plugins: [
    react(),
    comlink(),
    ...
  ],
  worker: {
    plugins: [comlink()],
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode

See "Install" section - npmjs.com/package/vite-plugin-comlink

Collapse
 
oleksandrdanylchenko profile image
oleksandr-danylchenko

Also make sure that you don't create circular dependency between the worker file and the file with ComlinkWorker instance. I'll lead to endless network requests

Collapse
 
tbroyer profile image
Thomas Broyer

You seem to be conflating Web Workers and Service Workers

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks a lot Thomas, you're right, I ended up being wrong. I already made the adjustments in the article.

Collapse
 
tbroyer profile image
Thomas Broyer

I think part of your introduction is wrong, wrt β€œthey are essentially empty when they are first created, and it's up to the developer to define their behavior using instructions and code”; this is true for Service Workers, not for Web Workers.

Thread Thread
 
franciscomendes10866 profile image
Francisco Mendes

Thanks for the feedback, I just finished tweaking it.

Collapse
 
pavelzubov profile image
Pavel Zubov

Your app doesn't build

> web-worker@0.0.0 build
> tsc && vite build

vite v3.2.4 building for production...
transforming (15185) node_modules/comlink/dist/esm/comlink.mjs
<--- Last few GCs --->

[42167:0x7fa957200000]   103775 ms: Mark-sweep 4032.5 (4135.8) -> 4022.0 (4138.1) MB, 5144.0 / 0.1 ms  (average mu = 0.193, current mu = 0.073) task scavenge might not succeed
[42167:0x7fa957200000]   109179 ms: Mark-sweep 4036.5 (4138.6) -> 4026.7 (4143.1) MB, 5296.5 / 0.0 ms  (average mu = 0.114, current mu = 0.020) task scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0x102f02515 node::Abort() (.cold.1) [/usr/local/bin/node]
 2: 0x101c03989 node::Abort() [/usr/local/bin/node]
 3: 0x101c03aff node::OnFatalError(char const*, char const*) [/usr/local/bin/node]
 4: 0x101d832c7 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
 5: 0x101d83263 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
 6: 0x101f24975 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/local/bin/node]
 7: 0x101f289bd v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [/usr/local/bin/node]
 8: 0x101f2529d v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/local/bin/node]
 9: 0x101f227bd v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node]
10: 0x101fb01ed v8::internal::ScavengeJob::Task::RunInternal() [/usr/local/bin/node]
11: 0x101c7164b node::PerIsolatePlatformData::RunForegroundTask(std::__1::unique_ptr<v8::Task, std::__1::default_delete<v8::Task> >) [/usr/local/bin/node]
12: 0x101c700e7 node::PerIsolatePlatformData::FlushForegroundTasksInternal() [/usr/local/bin/node]
13: 0x1025cf5fb uv__async_io [/usr/local/bin/node]
14: 0x1025e336c uv__io_poll [/usr/local/bin/node]
15: 0x1025cfb81 uv_run [/usr/local/bin/node]
16: 0x101b380af node::SpinEventLoop(node::Environment*) [/usr/local/bin/node]
17: 0x101c44f21 node::NodeMainInstance::Run(int*, node::Environment*) [/usr/local/bin/node]
18: 0x101c44b79 node::NodeMainInstance::Run(node::EnvSerializeInfo const*) [/usr/local/bin/node]
19: 0x101bcef68 node::Start(int, char**) [/usr/local/bin/node]
20: 0x11528851e 
sh: line 1: 42167 Abort trap: 6           vite build
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bo87844752 profile image
π–‰π–Žπ–›π–Šπ–—

You can't have the worker instance inside Utils otherwise you create a circular dependency.