Using Web Worker and Comlink in Next.js. JavaScript and TypeScript are supported.

Let's get Next.js to work with Web Worker or Comlink. This will prevent the screen from being slowed down by thought processing. The code supports Javascript and TypeScript. The code can be found here.

Workers

Image by 272447 from Pixabay

Improving Web Application Performance

Web Worker allows web content to run programs in a background thread. JavaScript has only one main thread, and the The screen freezes up and the user experience is severely degraded when heavy processing runs in There are several ways to get around that.

  • Improve the logic of heavy processing.
  • Cache the results of heavy processing.
  • Perform heavy processing on the server side.
  • Narrow down the areas to be re-rendered in React, Vue, etc.

There are other ways to do this, such as increasing the specs on the client side, but you can actually As those of you who have experienced application performance improvements know, there are limitations. If the process doesn't run fast enough, or if you're considering rewriting the application itself, you can use the and the sense of cost to the solution is greater. In this context, we may be forced to choose to maintain the status quo.

Another solution is Web Worker or WebAssembly will be used.

Both methods use a different thread than the main thread.

WebAssembly is a new kind of code execution that has become available in modern browsers Environment. The biggest advantage is the ability to use a low-level assembly language that runs at near-native performance. It's something you can handle. This is the CPython equivalent of Python; Python is often used in other languages It is sometimes said to be slower than The solution is to call the code compiled in C. This approach in itself can be quite effective, but it also requires familiarity with the C language. Now, WebAssembly can use C, of course, but it is also relatively easy to introduce WebAssembly as it can use AssemblyScript, which is similar to Rust and TypeScript.

In this article, we're going to take a closer look at Web Worker.

Web Worker

A worker thread running a web worker prevents the user interface from working The means of communication between the worker and the main thread is through It's about sending a message. The main thread and the workers generated in it communicate with each other via the event handler We'll fight over it.

Also, since there is a separate thread, the worker's code is in a separate file from the main code and is stored in File name is in the format of .worker.js or .worker.ts.

  1. In the main thread, a Worker defined in another file (../worker/test.worker.js) is created and a message is sent.

    var myWorker = new Worker("../worker/test.worker.js");
    myWorker.postMessage([10, 33]);
  2. In the worker, the onmessage event handler receives the message from the main thread, processes something, and then returns the message to the main thread with postMessage.

    onmessage = function (e) {
      console.log("Message received from main");
      var workerResult = "Result: " + e.data[0] * e.data[1];
    
      console.log("Posting message back to main");
      postMessage(workerResult);
    };
  3. If you define an onmessage handler in advance in the main thread, you can receive the message sent by the Worker.

    myWorker.onmessage = function (e) {
      result.textContent = e.data; // It would be 'Result: 330'
      console.log("Message received from worker");
    };
  4. When the worker is no longer needed (screen switching etc.), the worker thread is terminated immediately with the following code.

    myWorker.terminate();

That's it for a quick introduction to Web Workers. There are also useful classes such as SharedWorker and ServiceWorker. For details, refer to here.

Comlink

Web Worker is very useful and powerful, but the messaging mechanics alone make it a complex If you try to create an application, you will see a message based state in both the main and worker You will have to manage This can be a costly one.

Comlink was developed as a lightweight library for abstracting MessageChannel and postMessage in order to solve these problems.

Comlink allows you to treat Web Workers the same way you would treat a normal object. Let's look at a specific example.

  1. Define the process in Worker. Define an object that only has a method whose initial value increases by 0 and 1 and expose it through Comlink. This file is called worker.js.

    import * as Comlink from "comlink";
    
    const obj = {
      counter: 0,
      inc() {
        this.counter++;
      },
    };
    
    Comlink.expose(obj);
  2. Create a Worker in the main thread and wrap it in Comlink. And after checking the current count of Workers, increment it and check its value.

    import * as Comlink from "comlink";
    async function init() {
      const worker = new Worker("worker.js");
      // WebWorkers use `postMessage` and therefore work with Comlink.
      const obj = Comlink.wrap(worker);
      alert(`Counter: ${await obj.counter}`);
      await obj.inc(); // increase the count.
      alert(`Counter: ${await obj.counter}`);
    }
    init();

What you can see from this example is that we can now treat the Web Worker like a normal class. This will lower the bar for implementing it into the actual project.

Next, we'll introduce Web Worker and Comlink to the Next.js project. Let's try it.

Introduce Web Worker to Next.js

To introduce Web Worker, let's use Next.js Official Example.

This is to introduce worker-plugin into Next.js.

So let's get started.

  1. Create a new project.

    • Javascript

      npx create-next-app
      # or
      yarn create next-app
    • TypeScript

      npx create-next-app --example with-typescript
      # or
      yarn create next-app --example with-typescript
  2. Install worker-plugin.

    npm install worker-plugin
    # or
    yarn add worker-plugin
  3. Add the following code to next.config.js in the root. Make the variable self available to Global.

    const WorkerPlugin = require("worker-plugin");
    
    module.exports = {
      webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
        if (!isServer) {
          config.plugins.push(
            new WorkerPlugin({
              // use "self" as the global object when receiving hot updates.
              globalObject: "self",
            })
          );
        }
        return config;
      },
    };
  4. When using TypeScript, add webworker to libs of tsconfig.json. This will correctly recognize the notation of Web Worker.

    {
        "compilerOptions": {
            :
            "lib": ["dom", "es2017", "webworker"],
            :
        }
    }
  5. Define the Worker in workers/standard.worker.js or workers/standard.worker.ts. Here we are calling a method to calculate the pi.

    import pi from "../utils/pi";
    
    addEventListener("message", (event) => {
      console.log("worker event message", event.target, event.type);
      postMessage(pi(event.data));
    });

    or

    import pi from "../utils/pi";
    
    addEventListener("message", (event: MessageEvent) => {
      console.log("worker event message", event.target, event.type);
      postMessage(pi(event.data));
    });
  6. If pages/index.jsx or pages/index.tsx has a Describe the code that calls the worker. Since this is about event handling, the useRef hook is used to define a worker and It is. Then, create a worker. As arguments, you can specify the path and the option to load it as a module. Define various events and you're done.

// for standard
const [latestMessage, setLatestMessage] = React.useState("");
const workerRef = React.useRef<Worker>();

React.useEffect(() => {
  // Standard worker
  workerRef.current = new Worker("../workers/standard.worker", {
    type: "module",
  });
  workerRef.current.onmessage = (evt) =>
    setLatestMessage(`WebWorker Response => ${evt.data}`);

  return () => {
    workerRef.current?.terminate();
  };
}, []);

const handleWork = React.useCallback(async () => {
  workerRef.current?.postMessage(100000);
}, []);

Introduce Comlink to Next.js

  1. Install Comlink.

    npm install comlink
    # or
    yarn add comlink
  2. The installation of worker-plugin, the setting of next.config.js, and the setting of tsconfig.json when using TypeScript are the same as for Web Worker.

  3. Create a Worker in workers/comlink.worker.ts. Here, as an asynchronous process, API to get random words is called. We just define the object and expose it via Comlink.

    import * as Comlink from "comlink";
    
    export interface WorkerApi {
      getName: typeof getName;
    }
    
    const workerApi: WorkerApi = {
      getName,
    };
    
    async function getName() {
      const res = await fetch(
        "https://random-word-api.herokuapp.com/word?number=1"
      );
      const json = await res.json();
      return json[0];
    }
    
    Comlink.expose(workerApi);

    or

    import * as Comlink from "comlink";
    
    const workerApi = {
      getName,
    };
    
    async function getName() {
      const res = await fetch(
        "https://random-word-api.herokuapp.com/word?number=1"
      );
      const json = await res.json();
      return json[0];
    }
    
    Comlink.expose(workerApi);
  4. The code to call Worker is described in pages/index.jsx or pages/index.tsx. This is the same until the generation of Web Worker. There is a difference in how to use Worker after wrapping it with Comlink after creating it. In the case of Comlink, you can treat it as a normal object instead of an event.

    // for comlink
    const [comlinkMessage, setComlinkMessage] = React.useState("");
    const comlinkWorkerRef = React.useRef<Worker>();
    const comlinkWorkerApiRef = React.useRef<Comlink.Remote<WorkerApi>>();
    
    React.useEffect(() => {
      // Comlink worker
      comlinkWorkerRef.current = new Worker("../workers/comlink.worker", {
        type: "module",
      });
      comlinkWorkerApiRef.current = Comlink.wrap<WorkerApi>(
        comlinkWorkerRef.current
      );
      return () => {
        comlinkWorkerRef.current?.terminate();
      };
    }, []);
    
    const handleComlinkWork = async () => {
      const msg = await comlinkWorkerApiRef.current?.getName();
      setComlinkMessage(`Comlink response => ${msg}`);
    };

    or

    // for comlink
    const [comlinkMessage, setComlinkMessage] = React.useState("");
    const comlinkWorkerRef = React.useRef();
    const comlinkWorkerApiRef = React.useRef();
    
    React.useEffect(() => {
      // Comlink worker
      comlinkWorkerRef.current = new Worker("../workers/comlink.worker", {
        type: "module",
      });
      comlinkWorkerApiRef.current = Comlink.wrap(comlinkWorkerRef.current);
      return () => {
        comlinkWorkerRef.current.terminate();
      };
    }, []);
    
    const handleComlinkWork = async () => {
      const msg = await comlinkWorkerApiRef.current.getName();
      setComlinkMessage(`Comlink response => ${msg}`);
    };

Finally

The code can be found here.

That was a brief explanation, but it was a good idea to build it on a complex platform like Next.js or React. I think you'll encounter a lot of problems if you try to deploy a web worker on top of a package that I hope you can avoid that part of the story if you read it alone.

See you!

Updated at: Wed Jul 01 2020

© 2020-presentTerms|Privacy