Create-react-app: Is it possible to use load webworkers?

Created on 15 Dec 2016  Â·  24Comments  Â·  Source: facebook/create-react-app

In the past I've used the web-worker loader in webpack, but with create-react-app, I get the no-webpack-loader-syntax error.

Is there another way that you recommend to do this? I can't find anything in your documentation about webworkers, and it seems there isn't another way to load them in webpack.

proposal

Most helpful comment

Okay, if anyone stumbles upon this thread, here’s a bit messy, but quick and easy way to get WebWorkers working.

UPDATE. As create-react-app continues to update its webpack confuguration, the code below will need updates as well. Specifically, the part which locates babel-loader configuration (const babelLoader = ...). Check getBabelLoader() from https://github.com/timarney/react-app-rewired/blob/master/packages/react-app-rewired/index.js to get an updated version of the code in question.

  1. Install react-app-rewired, worker-loader and lodash (the latter is here just for the sake of cloneDeep — feel free to replace it with anything you like) :
    npm install --save-dev react-app-rewired worker-loader lodash
  2. Create config-overrides.js file in the root directory of your application:
const lodashCloneDeep = require('lodash/cloneDeep');

module.exports = function override(config, env) {
    // Add worker-loader by hijacking configuration for regular .js files.

    const workerExtension = /\.worker\.js$/;

    const babelLoader = config.module.rules.find(
        rule => rule.loader && rule.loader.indexOf('babel-loader') !== -1
    );

    const workerLoader = lodashCloneDeep(babelLoader);

    workerLoader.test = workerExtension;
    workerLoader.use = [
        'worker-loader',
        { // Old babel-loader configuration goes here.
            loader: workerLoader.loader,
            options: workerLoader.options,
        },
    ];
    delete workerLoader.loader;
    delete workerLoader.options;

    babelLoader.exclude = (babelLoader.exclude || []).concat([workerExtension]);

    config.module.rules.push(workerLoader);

    // Optionally output the final config to check it.
    //console.dir(config, { depth: 10, colors: true });

    return config;
};
  1. Create workers by naming them MySomething.worker.js:
import myDoSomething from 'my-do-something';

onmessage = async function (message) { // eslint-disable-line no-undef
    console.log('Message received from main script', message.data);
    const workerResult = await myDoSomething(message.data);
    console.log('Posting message back to main script');
    postMessage(workerResult);
};
  1. Create instances of your worker:
import MySomethingWorker from './MySomething.worker.js';

const worker = new MySomethingWorker();
  1. Enjoy webworkers, stored separately from the main bundle (or inline — check worker-loader options) while being processed by Babel. :)

P.S. As a sidenote, a bit of feedback, as I’ve just began experimenting with create-react-app (having always created my own configurations before). I must say it feels that it kind of lost the second part of "convention over configuration" principle. That is, it almost entirely discarded the ability "to specify unconventional aspects of the application". For example, I had to install the aforementioned react-app-rewired just to get my own module resolve paths and LESS support — being unable to configure such basic things was quite a surprise.

All 24 comments

There is currently no support for compiling code for web workers from src directory. We are open to adding this support if you send a pull request demonstrating how it could be done with a "convention over configuration" approach.

That said you can always add any unprocessed files (including workers) to the public folder and then reference them like this. Beware that they won't be processed in production so you'll need to handle minifying and compiling them to ES5 (or your target) yourself.

We recognize this is suboptimal but this is really waiting for somebody to figure out how it should work, and send a pull request with a proof of concept.

I am not sure how ugly is this workaround, but I created an inline worker with a CRA app and worked like a charm, without any external file or configuration changes 🤓

const worker = () => {
  setInterval(() => {
    postMessage({foo: "bar"});
  }, 1000);
}

let code = worker.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));

const blob = new Blob([code], {type: "application/javascript"});
const worker = new Worker(URL.createObjectURL(blob));

worker.onmessage = (m) => {
  console.log("msg", m.data.foo);
};

@thiagoxvo - cool solution.
Tried it with a separate file - the only thing needed to add is self, not sure why, but postMessage is global in the worker file, and onmessage not. (using chrome)

working example:

// worker.js
const workercode = () => {

  self.onmessage = function(e) { // without self, onmessage is not defined
    console.log('Message received from main script');
    var workerResult = 'Received from main: ' + (e.data);
    console.log('Posting message back to main script');
    self.postMessage(workerResult); // here it's working without self
  }
};

let code = workercode.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));

const blob = new Blob([code], {type: "application/javascript"});
const worker_script = URL.createObjectURL(blob);

module.exports = worker_script;
//main.js
import worker_script from './worker';
var myWorker = new Worker(worker_script);

myWorker.onmessage = (m) => {
  console.log("msg from worker: ", m.data);
};
myWorker.postMessage('im from main');

There are two main approaches here:

  1. inline worker as blob. This approach taken by webworkify or webworkify for Rollup
  2. use separate files for each worker

1. Inline pattern

From my POV inline pattern comes from the fact that webpack, and browserify by default generate one file and one-file approach comes from pre-HTTP2-age. It is considered to be good practice to have less files for HTTP1, but this considered to be antipattern for HTTP2.

Also allowing execution of blobs in web workers with CSP considered to be security breach: it allows web workers to get around same-origin restrictions.

2. Separate files

2.1. Make a way to provide more than one entry for webpack configuration

maybe configuration in package.json?

webworkers: ["pdfworker.js"]

See other fields in package.json

2.2. Use some kind of webpack plugin, which will generate separate file for each worker

TODO: google for options

Thoughts?

PS webworkify seems to be incompatible with CRA

What do you think of *.webworker.js as convention over configuration? Inspiration https://twitter.com/dan_abramov/status/865895057802571776

@yonatanmn your code didn't worked for me, in newer versions of React (^15.5.4) using self will throw an error and the code won't compile. (Error thrown: Unexpected use of 'self' no-restricted-globals)

So I modified your solution just using let instead of self, and works for me:

const workercode = () => {

  let onmessage = (e) => {
    console.log('Message received from main script %s ', e.data);
    console.log('Posting message back to main script');
    postMessage('Received from main: ' + (e.data));
  };

};


let code = workercode.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));

const blob = new Blob([code], {type: "application/javascript"});
const MyWorker = URL.createObjectURL(blob);


export default MyWorker;

main.js file remains the same

I am creating a React app in which I need some intensive client-side computation to happen in a separate execution thread, i.e. in a Web Worker. This computation uses the math js module.

Reading this discussion has been helpful for understanding why this is non-trivial and why my initial attempts at using the “web-worker loader” (for webpack) weren't going to work.

I made a start with the “inline worker as blob” approach, initially offered by @thiagoxvo, but am a bit puzzled by how best to load modules within the Worker itself, this now being outside of Webpack’s control.

The Mozilla documentation for Web Workers states:

Worker threads have access to a global function, importScripts(), which lets them import scripts. It accepts zero or more URIs as parameters to resources to import...

So for example, in my worker I call:

importScripts("http://cdnjs.cloudflare.com/ajax/libs/mathjs/3.13.3/math.min.js");

If I call this in an inline-blob worker, I get an error

error 'importScripts' is not defined no-undef

However, by adding the unprocessed JS files to my public folder as suggested by @gaearon (instructions in issue 1574) – it seems I have a solution.

But this approach involves including math js twice in two different ways, firstly using ES6 modules (inside the main thread of the React App, as normal) and also using a CDN for within the Web Worker. It means I will be developing one batch of JS in /src - which does get compiled, and another in /public which does not. Does anyone know of a better solution?

I am going to close this because it seems like this is a relatively rare use case and might justify a custom build setup anyway. Whether it's done by ejecting or by separating the common piece into an independently compiled package for multiple targets.

Okay, if anyone stumbles upon this thread, here’s a bit messy, but quick and easy way to get WebWorkers working.

UPDATE. As create-react-app continues to update its webpack confuguration, the code below will need updates as well. Specifically, the part which locates babel-loader configuration (const babelLoader = ...). Check getBabelLoader() from https://github.com/timarney/react-app-rewired/blob/master/packages/react-app-rewired/index.js to get an updated version of the code in question.

  1. Install react-app-rewired, worker-loader and lodash (the latter is here just for the sake of cloneDeep — feel free to replace it with anything you like) :
    npm install --save-dev react-app-rewired worker-loader lodash
  2. Create config-overrides.js file in the root directory of your application:
const lodashCloneDeep = require('lodash/cloneDeep');

module.exports = function override(config, env) {
    // Add worker-loader by hijacking configuration for regular .js files.

    const workerExtension = /\.worker\.js$/;

    const babelLoader = config.module.rules.find(
        rule => rule.loader && rule.loader.indexOf('babel-loader') !== -1
    );

    const workerLoader = lodashCloneDeep(babelLoader);

    workerLoader.test = workerExtension;
    workerLoader.use = [
        'worker-loader',
        { // Old babel-loader configuration goes here.
            loader: workerLoader.loader,
            options: workerLoader.options,
        },
    ];
    delete workerLoader.loader;
    delete workerLoader.options;

    babelLoader.exclude = (babelLoader.exclude || []).concat([workerExtension]);

    config.module.rules.push(workerLoader);

    // Optionally output the final config to check it.
    //console.dir(config, { depth: 10, colors: true });

    return config;
};
  1. Create workers by naming them MySomething.worker.js:
import myDoSomething from 'my-do-something';

onmessage = async function (message) { // eslint-disable-line no-undef
    console.log('Message received from main script', message.data);
    const workerResult = await myDoSomething(message.data);
    console.log('Posting message back to main script');
    postMessage(workerResult);
};
  1. Create instances of your worker:
import MySomethingWorker from './MySomething.worker.js';

const worker = new MySomethingWorker();
  1. Enjoy webworkers, stored separately from the main bundle (or inline — check worker-loader options) while being processed by Babel. :)

P.S. As a sidenote, a bit of feedback, as I’ve just began experimenting with create-react-app (having always created my own configurations before). I must say it feels that it kind of lost the second part of "convention over configuration" principle. That is, it almost entirely discarded the ability "to specify unconventional aspects of the application". For example, I had to install the aforementioned react-app-rewired just to get my own module resolve paths and LESS support — being unable to configure such basic things was quite a surprise.

The solution from @speicus does not seem to be working anymore. I get:

workerLoader.test = workerExtension;
                  ^

TypeError: Cannot set property 'test' of undefined

I will try to dig deep in the loader files and see if I can make it work.

@RassaLibre and @speicus:

I was able to get it working by making the following modifications:

    const workerExtension = /\.worker\.js$/;

    function isBabelLoader(rule) {
        return rule.loader && rule.loader.indexOf('babel-loader') !== -1;
    }

    function findBabelLoader(rule) {
        if (isBabelLoader(rule)) {
            return rule;
        }

        if (Array.isArray(rule.use) && rule.use.find(isBabelLoader)) {
            return rule;
        }

        return Array.isArray(rule.oneOf) && rule.oneOf.find(isBabelLoader);
    }

    function searchRules(rules) {
        for (let i = 0; i < rules.length; i++) {
            const babelRule = findBabelLoader(rules[i]);
            if (babelRule) {
                return babelRule;
            }
        }

        return {};
    }

    const babelLoader = searchRules(config.module.rules);

Note that it's not thoroughly tested on many configurations, so please be careful.

The example above worked with the old version of create-react-app. Since then, the configuration of config.module.rules got updated. To locate babel-loader configuration now, use the snippet provided by @brianchirls. Or better yet, fetch getBabelLoader() from https://github.com/timarney/react-app-rewired/blob/master/packages/react-app-rewired/index.js.

Updated my initial comment with this info.

I have decided to eject the app and added worker-loader. It took me 5 mins and I don't regret doing so. Thank you guys for replying so quickly though :-)

@RassaLibre what did you do? added babel's workers-loader? I need a solution for working loaders too. Thanks!

Based on the previous answers, I wrote a helper class to make things a bit easier when working with Web Workers in React.

The helper class looks like so:

// WebWorker.js

export default class WebWorker {
    constructor(worker) {
        let code = worker.toString();
        code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"));

        const blob = new Blob([code], { type: "application/javascript" });
        return new Worker(URL.createObjectURL(blob));
    }
}

Then, you write your worker code like this:

// MyWorker.js

// @args: You can pass your worker parameters on initialisation
export default function MyWorker(args) {
    let onmessage = e => { // eslint-disable-line no-unused-vars
        // Write your code here...

        postMessage("Response");
    };
}

To create a Web Worker instance, write like so:

// App.js

// WebWorker helper class
import WebWorker from './utils/WebWorker';
// Your web worker
import MyWorker from './MyWorker';

// Worker initialisation
const workerInstance = new WebWorker(MyWorker);
const workerInstanceWithParams = new WebWorker(new MyWorker("foo"));

// Communication with worker
workerInstance.addEventListener("message", e => console.log(e.data), false);
workerInstance.postMessage("bar");

Using this method, you simply utilise the WebWorker helper class, and there's no need for Webpack loaders or anything like that. It worked for me and solved my problems. Hopefully we won't have to do workarounds like these in the future.

@packetstracer Exactly! Eject, add workers-loader and then follow the docs for workers-loader to add your own worker. I was done in 5 mins with a minimum effort :-)

@danielpox I tried this solution too but I think I had some troubles with it, it just did not compile properly so I eventually gave up and ejected the app.

btw. don't be afraid of ejecting, everything stays exactly the same, you just have the power to change/add stuff to the webpack config so if you know webpack, you are just fine.

@RassaLibre I see. What errors did you get?
I had already ejected for the purpose of worker-loader, but that just got messed up. The docs of it are rather unclear, so I used the examples here to my own solution, explained above.

@danielpox I've just tried it and, although I don't get any compilation issues, on debugging the code variable just has "" in it, therefore the created Blob is effectively an empty object. It appears it's unable to convert the MyWorker.js object back into a String and unable to locate onmessage.

(Chrome:63.0.3239.84, react-scripts:1.0.17)

@speicus works for me, with @brianchirls modification

thanks!!

@speicus works in dev but in production build its not working. In fact it shows like the web worker is not there at all. Any suggestion what I might be missing?

maybe this library will help? https://github.com/developit/workerize

@danielpox You solution is the easiest for me that works. Interestingly your method only works when you create worker instances without params like:

const workerInstance = new WebWorker(MyWorker);

Using it with new it creates just empty blob

const workerInstance = new WebWorker(new MyWorker());

This is not a big deal. We can pass "default" parameters later as a message.

We are currently working to add support for WebWorkers: #3660. If you have any comments, use-cases, feedback, etc. please post it in that issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alleroux picture alleroux  Â·  3Comments

jnachtigall picture jnachtigall  Â·  3Comments

DaveLindberg picture DaveLindberg  Â·  3Comments

wereHamster picture wereHamster  Â·  3Comments

adrice727 picture adrice727  Â·  3Comments