A Simple Pattern For Adding a Web Worker in React

A web worker is a JavaScript script executed from an HTML page that runs in the background, independently of scripts that may also have been executed from the same HTML page according...

Introduction

A web worker is a JavaScript script executed from an HTML page that runs in the background, independently of scripts that may also have been executed from the same HTML page according to the World Wide Web Consortium. Web Workers require an origin so you cant open an HTML file you need a server to get started. Otherwise, you get the following error:
Uncaught DOMException: Failed to construct 'Worker': Script at ... cannot be accessed from origin 'null'
https://github.com/mdn/simple-web-worker

Framework for adding simple web workers in react:

src/
├── index.css
├── index.js
├── components
│   └── App
│       └── App.js
├── utils
│   └── workerUtils.js
└── workers
    └── countToHighNumberWorker.js
    └── filterReallyLargeDataWorker.js
    └── putEverythingOnSeperateThread1.js
    └── putEverythingOnSeperateThread2.js

Dependencies: Workerize-Loader

Each folder in the folder structure has a responsibility:

  1. workers - contains logic that you want to offload to a web worker and return a result.
  2. utils/workerUtils.js - manages the web workers dispatching 1 worker or many workers and cleaning up web workers.
  3. components or pages - render to the browser, call the web worker update the browser with the result from the web worker when ready.

So what does this look like ?

import './App.css';
import loopWorker from 'workerize-loader!../../workers/loopWorker'; // eslint-disable-line import/no-webpack-loader-syntax
import { callWebWorker, callWebWorkerHandleNestedData } from '../../utils/workerUtils'

const App = () => {
  const handleWebWorkerExampleClick = (e) => {
    e.preventDefault()
    // example with useState passed as a callback
    callWebWorker(loopWorker, "countToBillion", setResultText)

    // example with console.log passed as a callback additional params like indexToStartCounting get sent to the worker
    const indexToStartCounting = 0
    callWebWorker(loopWorker, "countToBillion", console.log, indexToStartCounting)

    // note: you could also fire a network request when the worker completes to an analytic service 
    // or other api to track calculated or filtered information without blocking the main thread.
    
    // example to handle nested data and console log the result of the filterDataWorker web worker
    const largeDataObject = { a: 'text', b: { c: 'more text', d: 'last item '} }
    callWebWorkerHandleNestedData(loopWorker, "countToBillion   ", console.log, largeDataObject)
  }
  const [resultText, setResultText] = useState('')
  
  return ( 
           <div className="App">
             <button onClick={handleWebWorkerExampleClick}> trigger web worker</button>
             <h1>{resultText}</h1>
           <div>
         );
}
export default App;

Where is the magic?

Everything is simplified and managed by utils/workerUtils.js.

// returns a promise, also cleans itself up!
export const callWebWorker = (passedInstance, method, callback, ...rest) => {
  // 1) callback determines what to do after the web worker completes
  // 2) ...rest allows as many arguments as needed to be passed to the worker/logicFile.js
  let instance = new passedInstance();
  return instance[method].apply(null, rest)
    .then(x => {
      callback(x);
      return x;
    })
    .then(_ => instance.terminate())
    .catch(_ => instance.terminate());
}

// same as above but handles nested data.
export const callWebWorkerHandleNestedData = (passedInstance, method, callback, ...rest) => {
  // 1) JSON.parse all of the variables using rest.map in the worker function!
  const fixedData = rest.map(x => JSON.stringify(x))
  let instance = new passedInstance();
  return instance[method].apply(null, fixedData)
    .then(x => {
      callback(x);
      return x;
    })
    .then(_ => instance.terminate())
    .catch(_ => instance.terminate());
}

Getting test suite to pass with the new changes for create react app

Source: https://github.com/developit/workerize-loader#with-webpack-and-jest

Add the following to your package.json:

"jest": {
    "moduleNameMapper": {
      "workerize-loader(\\?.*)?!(.*)": "identity-obj-proxy"
    },
    "transform": {
      "workerize-loader(\\?.*)?!(.*)": "<rootDir>/workerize-jest.js",
      "^.+\\.[jt]sx?$": "babel-jest",
      "^.+\\.[jt]s?$": "babel-jest"
    }
},

Create a file workerize-jest.js in your project's root directory (where the package.json is):

module.exports = {
    process(src, filename) {
      return `
        async function asyncify() { return this.apply(null, arguments); }
        module.exports = function() {
          const w = require(${JSON.stringify(filename.replace(/^.+!/, ''))});
          const m = {};
          for (let i in w) m[i] = asyncify.bind(w[i]);
          return m;
        };
      `;
    }
};

The JBS Quick Launch Lab

Free Qualified Assessment

Quantify what it will take to implement your next big idea!

Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.

Get Your Assessment