New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to use load webworkers? #1277

Closed
pedalpete opened this Issue Dec 15, 2016 · 24 comments

Comments

Projects
None yet
@pedalpete

pedalpete commented Dec 15, 2016

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.

@gaearon

This comment has been minimized.

Member

gaearon commented Dec 15, 2016

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.

@thiagoxvo

This comment has been minimized.

thiagoxvo commented Feb 28, 2017

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);
};

@gaearon gaearon added this to the 1.0.0 milestone Feb 28, 2017

@yonatanmn

This comment has been minimized.

yonatanmn commented Apr 8, 2017

@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');
@stereobooster

This comment has been minimized.

Contributor

stereobooster commented May 1, 2017

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

@stereobooster

This comment has been minimized.

Contributor

stereobooster commented May 24, 2017

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

@packetstracer

This comment has been minimized.

packetstracer commented Jun 1, 2017

@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

@ralphbarton

This comment has been minimized.

ralphbarton commented Jun 22, 2017

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?

@gaearon

This comment has been minimized.

Member

gaearon commented Jun 26, 2017

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.

@gaearon gaearon closed this Jun 26, 2017

@speicus

This comment has been minimized.

speicus commented Jul 9, 2017

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.

@RassaLibre

This comment has been minimized.

RassaLibre commented Oct 31, 2017

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.

@brianchirls

This comment has been minimized.

brianchirls commented Oct 31, 2017

@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.

@speicus

This comment has been minimized.

speicus commented Oct 31, 2017

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.

@RassaLibre

This comment has been minimized.

RassaLibre commented Nov 1, 2017

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 :-)

@packetstracer

This comment has been minimized.

packetstracer commented Nov 1, 2017

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

@danielpox

This comment has been minimized.

danielpox commented Nov 19, 2017

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.

@RassaLibre

This comment has been minimized.

RassaLibre commented Nov 20, 2017

@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 :-)

@RassaLibre

This comment has been minimized.

RassaLibre commented Nov 20, 2017

@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.

@danielpox

This comment has been minimized.

danielpox commented Nov 20, 2017

@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.

@dansiviter

This comment has been minimized.

dansiviter commented Dec 29, 2017

@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)

@packetstracer

This comment has been minimized.

packetstracer commented Jan 12, 2018

@speicus works for me, with @brianchirls modification

thanks!!

@aswincsekar

This comment has been minimized.

aswincsekar commented Jan 26, 2018

@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?

@viankakrisna

This comment has been minimized.

Contributor

viankakrisna commented Jan 26, 2018

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

@janvorisek

This comment has been minimized.

janvorisek commented Apr 17, 2018

@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.

@iansu

This comment has been minimized.

Collaborator

iansu commented Apr 17, 2018

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.

@facebook facebook locked as resolved and limited conversation to collaborators Apr 17, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.