Multi-threaded React App using useWorker
Nilanth

Nilanth @nilanth

About: Full Stack Developer | ReactJS | Redux | NextJS | Laravel | PostgreSQL | AWS | Follow on Twitter for daily updates | Let’s make the web. Faster 🚀

Location:
India
Joined:
Jan 16, 2021

Multi-threaded React App using useWorker

Publish Date: Feb 3 '23
258 23

Process expensive and UI-blocking tasks in a separate thread using useWorker.

As we all know, Javascript is a single-threaded language. So, if we do any expensive task, It will block UI interaction. And the user needs to wait till it completes to perform any other action, which gives a bad UX experience.

To overcome and perform such tasks, Javascript has a solution called Web Workers, Which allows performing the expensive task in web browsers without blocking the user interface and makes the UX very smooth.

Let us see what is web workers first.

Web Workers

Web Worker is a script that runs in the background without affecting the user interface, as it runs in a separate thread instead of the main thread. So it won’t cause any blocking to the user interaction. Web workers are primarily used to perform expensive tasks in a web browser, like sorting large numbers of data, CSV export, image manipulation, etc.

worker-thread

Referring to the above image, we can see that an expensive task is performed parallel by a worker without blocking the main thread. When we perform the same in the main thread, it causes UI blocking.

What about the Concurrent mode in React?

React concurrent mode does not run tasks in parallel. It moves the non-urgent task to transition and perform the urgent task immediately. It uses the same main thread to process it.

As we see in the following image, urgent tasks are processed using context switching. For example, If a table is rendering a large dataset and the user tries to search for something, React switches the task to user search and processes it first as below.

task-switch

When using a worker for the same task, table rendering runs in a separate thread in parallel. Check the following image.

react-worker

useWorker

useWorker is a library to uses web worker APIs in a simple configuration with React Hooks. Which supports executing expensive tasks without blocking the UI, support promises instead of event listeners, and other notable feature are

  1. Timeout option to kill the worker
  2. Remote Dependencies
  3. Transferable
  4. Worker status

useWorker is a 3KB library

Why not JavaScript built-in web worker?

When using javascript web worker, we need to add some complex configurations to set up a worker with multiple lines of code. Using useWorker, we can simplify the worker setup. Let’s see the difference between the two in the following code blocks.

When using JS Web Worker in React App

// webworker.js

self.addEventListener("message", function(event) {
  var numbers = [...event.data]
  postMessage(numbers.sort())
});
Enter fullscreen mode Exit fullscreen mode

//index.js

var webworker = new Worker("./webworker.js");

webworker.postMessage([3,2,1]);

webworker.addEventListener("message", function(event) {
    console.log("Message from worker:", event.data); // [1,2,3]
});
Enter fullscreen mode Exit fullscreen mode

When using useWorker in React App

    // index.js

    const sortNumbers = numbers => ([...numbers].sort())
    const [sortWorker] = useWorker(sortNumbers);

    const result = await sortWorker([1,2,3])
Enter fullscreen mode Exit fullscreen mode

As I said earlier, useWorker() simplifies the configuration compared to the plain javascript worker.

Let’s integrate with React App and perform high CPU-intensive tasks to see the useWorker() in action.

Quick Start

To add useWorker() to a react project, use the following command

yarn add @koale/useworker

After installing the package, import the useWorker().

import { useWorker, WORKER_STATUS } from "@koale/useworker";

We import useWorker and WORKER_STATUS from the library. useWorker() hooks return workerFn and controller.

  1. workerFn is the function that allows running a function, with web workers.
  2. The controller consists of status and kill parameters. status parameter returns the status of the worker and the kill function used to terminate the current running worker.

Let’s see the useWorker() with an example.

Sorting a large Array using useWorker() and the main thread

First, create a SortingArray component and add the following code

    //Sorting.js

    import React from "react";
    import { useWorker, WORKER_STATUS } from "@koale/useworker";
    import { useToasts } from "react-toast-notifications";
    import bubleSort from "./algorithms/bublesort";


    const numbers = [...Array(50000)].map(() =>
      Math.floor(Math.random() * 1000000)
    );

    function SortingArray() {
      const { addToast } = useToasts();
      const [sortStatus, setSortStatus] = React.useState(false);
      const [sortWorker, { status: sortWorkerStatus }] = useWorker(bubleSort);
      console.log("WORKER:", sortWorkerStatus);
      const onSortClick = () => {
        setSortStatus(true);
        const result = bubleSort(numbers);
        setSortStatus(false);
        addToast("Finished: Sort", { appearance: "success" });
        console.log("Buble Sort", result);
      };

      const onWorkerSortClick = () => {
        sortWorker(numbers).then((result) => {
          console.log("Buble Sort useWorker()", result);
          addToast("Finished: Sort using useWorker.", { appearance: "success" });
        });
      };

      return (
        <div>
          <section className="App-section">
            <button
              type="button"
              disabled={sortStatus}
              className="App-button"
              onClick={() => onSortClick()}
            >
              {sortStatus ? `Loading...` : `Buble Sort`}
            </button>
            <button
              type="button"
              disabled={sortWorkerStatus === WORKER_STATUS.RUNNING}
              className="App-button"
              onClick={() => onWorkerSortClick()}
            >
              {sortWorkerStatus === WORKER_STATUS.RUNNING
                ? `Loading...`
                : `Buble Sort useWorker()`}
            </button>
          </section>
          <section className="App-section">
            <span style={{ color: "white" }}>
              Open DevTools console to see the results.
            </span>
          </section>
        </div>
      );
    }

    export default SortingArray;
Enter fullscreen mode Exit fullscreen mode

Here we have configured the useWorker and passed the bubleSort function to perform the expensive sorting using worker.

Next, add the following code toApp.js component and import the SortingArray component.

    //App.js

    import React from "react";
    import { ToastProvider } from "react-toast-notifications";
    import SortingArray from "./pages/SortingArray";
    import logo from "./react.png";
    import "./style.css";

    let turn = 0;

    function infiniteLoop() {
      const lgoo = document.querySelector(".App-logo");
      turn += 8;
      lgoo.style.transform = `rotate(${turn % 360}deg)`;
    }


    export default function App() {

      React.useEffect(() => {
        const loopInterval = setInterval(infiniteLoop, 100);
        return () => clearInterval(loopInterval);
      }, []);

      return (
        <ToastProvider>
          <div className="App">
            <h1 className="App-Title">useWorker</h1>
            <header className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <ul>
                <li>Sorting Demo</li>
              </ul>
            </header>
            <hr />
          </div>
          <div>
            <SortingArray />
          </div>
        </ToastProvider>
      );
    }
Enter fullscreen mode Exit fullscreen mode

We have added the react logo to the App.js component, which rotates for every 100ms to visually represent the blocking and non-blocking UI when performing expensive tasks.

When running the above code, we can see two buttons Normal Sort and Sort using useWorker().

Next, Click the Normal Sort button to sort the array in the main thread. We can see the react logo stops rotating for a few seconds. Due to the UI rendering being blocked by the sorting task, the logo resumes rotating after the sorting is completed. This is due to both tasks being processed in the main thread. Check the following gif

normal-sorting

Let’s check the performance profiling of this using the chrome performance recording.

performance-normal

We can see the Event: click task has taken 3.86 seconds to complete the process and it has blocked the main thread for 3.86 seconds.

Next, let’s try the Sort using useWorker() option. When clicking it we can see the react logo still rotates without interruption. As the useWorker performs the sorting in the background without blocking the UI. Which makes the UX very smooth. Check the following gif

worker-sorting

You can also see the worker status as RUNNING, SUCCESS in the console using the sortWorkerStatus.

Let's see the performance profiling results for this approach

main-thread

worker-thread

As we can see, the first image represents that there is no long-running process in the main thread. In the second image, we can see the sorting task is handled by a separate worker thread. So the main thread is free from blocking tasks.

You can play around with the entire example in the following sandbox.

When to use worker

  1. Image Processing
  2. Sorting or Processing large data sets.
  3. CSV or Excel export with large data.
  4. Canvas Drawing
  5. Any CPU-intensive tasks.

Limitation in useWorker

  1. The web worker doesn’t have access to the window object and document.
  2. While the worker is running we can’t call it again until it’s finished, or until it is killed. To get around this we can create two or more instances of the useWorker() hook.
  3. The web worker cannot return the function because the response is serialized.
  4. Web Workers are limited by the available CPU core and memory of the end user machine.

Conclusion

Web Worker allows using multi-threading in react apps for performing expensive tasks without blocking the UI. Whereas useWorker allows using the web worker APIs in a simplified hooks approach in react apps. Workers shouldn’t be overused, we should use it only if necessary or it will increase the complexity of managing the workers.

Need to learn more? Feel free to connect on Twitter :)

You can support me by buying me a coffee ☕

eBook

Debugging ReactJS Issues with ChatGPT: 50 Essential Tips and Examples

ReactJS Optimization Techniques and Development Resources

Twitter Realtime Followers Count

Twiter Stats

More Blogs

  1. Use Vite for React Apps instead of CRA
  2. Twitter Followers Tracker using Next.js, NextAuth and TailwindCSS
  3. Don't Optimize Your React App, Use Preact Instead
  4. How to Reduce React App Loading Time By 70%
  5. Build a Portfolio Using Next.js, Tailwind, and Vercel with Dark Mode Support
  6. No More ../../../ Import in React
  7. 10 React Packages with 1K UI Components
  8. 5 Packages to Optimize and Speed Up Your React App During Development
  9. How To Use Axios in an Optimized and Scalable Way With React
  10. 15 Custom Hooks to Make your React Component Lightweight
  11. 10 Ways to Host Your React App For Free
  12. How to Secure JWT in a Single-Page Application

Comments 23 total

  • Al - Naucode
    Al - NaucodeFeb 3, 2023

    Great article, you got my follow, keep writing!

  • Rense Bakker
    Rense BakkerFeb 4, 2023

    Love this explanation of how to use web workers with React, very concise!

  • Marcelo Arias
    Marcelo AriasFeb 4, 2023

    Wow! I didn't know someone can do that. Thanks for sharing it 😁

  • Dan D
    Dan DFeb 5, 2023

    One thing Web Workers can't do is access the DOM. You had made references in your article on how Web Workers can help with rendering large tables, if they're returning DOM elements this isn't possible.

  • Abdul Raffy
    Abdul RaffyFeb 5, 2023

    Even if you are in learning stage or you are a Js pro, you are always learning new things day by day. I didn't know this. Thanks for sharing.

  • Derek Rosenzweig
    Derek RosenzweigFeb 5, 2023

    Haven't had to use them yet, but if it becomes a necessity I'll definitely check out this new package. Good stuff!

  • Sebastian Frey
    Sebastian FreyFeb 5, 2023

    When working with WebWorkers I would recommend using Comlink: github.com/GoogleChromeLabs/comlink . This library makes WebWorkers, iFrames and co. much more enjoyable, flexible and on top it's only 1.1kb of additional package size.

  • Alex Gu
    Alex GuFeb 5, 2023

    Nice content, thanks.

  • Maria 🍦 Marshmallow
    Maria 🍦 MarshmallowFeb 5, 2023

    I'd also add a communication overhead as one of the limitations of useWorker, because communication between the main thread and the worker thread can be slow, so it's important to be mindful of the amount and frequency of communication.

    In general, I really enjoyed reading this post! Thanks a lot Nilanth for such an informative content ☺️

  • Code of Relevancy
    Code of RelevancyFeb 6, 2023

    Thanks for sharing

  • Anzor B
    Anzor BFeb 6, 2023

    @mariamarsh one thing to note, is that this library does use transferable objects for things like ArrayBuffer, OffScreenCanvas, ImageBitmap, etc. This means the main thread transfers ownership of the object to the worker thread so the overhead is almost non existent (no serialization/deserialization is needed). This also means that you cannot access the object you passed in while the worker is processing it.
    github.com/alewin/useWorker/blob/m...

    For any other type of objects the overhead is very real, like passing a large JSON (had to deal with this myself). This is why the demo conveniently uses an array of numbers 🙂

  • tomthomas
    tomthomasFeb 7, 2023

    React Native is single-threaded in nature. In its rendering process, rather than have multiple processes occur at the same time (multithreading), other components have to wait when one component is being rendered.

  • Mr. Linxed
    Mr. LinxedFeb 7, 2023

    What about async/promises stuff?

  • kengkreingkrai
    kengkreingkraiFeb 9, 2023

    Thanks for sharing

  • Ivan
    IvanFeb 9, 2023

    You can also check the number of cores via hardware conecurrency to check whether it pays off spawning workers - single-core CPUs might not be able to run more than one thread

  • chris-czopp
    chris-czoppFeb 9, 2023

    WebWorker is great tool but I'm not sold for useWorker. It might be my misunderstanding, but it looks like the worker code is kept in the main bundle.

  • The Open Coder
    The Open CoderFeb 10, 2023

    Wow, this is a fantastic article! I love how you've explained the concept of Web Workers and how it can be used to perform expensive tasks in a separate thread without blocking the UI. The use of the useWorker library to simplify the configuration of web workers is particularly impressive. The comparison between the use of the plain JavaScript worker and useWorker in React apps is very clear and easy to understand. I can't wait to try out useWorker in my next project! Thank you for sharing this valuable information!

  • bap
    bapMar 7, 2023

    wow, it's so good

  • xcmk123
    xcmk123Jul 25, 2023

    Nice post...

Add comment