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.
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:
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.
maybe configuration in package.json?
webworkers: ["pdfworker.js"]
See other fields in package.json
TODO: google for options
Thoughts?
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 locatesbabel-loader
configuration (const babelLoader = ...
). CheckgetBabelLoader()
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.
npm install --save-dev react-app-rewired worker-loader lodash
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;
};
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);
};
import MySomethingWorker from './MySomething.worker.js';
const worker = new MySomethingWorker();
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.
Most helpful comment
Okay, if anyone stumbles upon this thread, here’s a bit messy, but quick and easy way to get WebWorkers working.
npm install --save-dev react-app-rewired worker-loader lodash
config-overrides.js
file in the root directory of your application:MySomething.worker.js
: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.