2

I've got some heavy canvas code that I want to offload into a WebWorker. When I follow the examples in this page and I pass the path to the worker typescript file into the constructor New Worker("./PaintCanvas.ts") the code successfully compiles but when it runs the worker doesn't seem to have found the code correctly because it throws an error saying Uncaught SyntaxError: Unexpected token '<' and the file it appears to be attempting to execute is actually my index.html file.
This is the component that I'm trying to run the worker from:

import React, { RefObject } from 'react';
//eslint-disable-next-line import/no-webpack-loader-syntax
import * as workerPath from "file-loader?name=[name].js!./PaintCanvas";
import './Canvas.css';

interface IProps {

}

interface IState {

}

class Canvas extends React.Component<IProps, IState> {
    private canvasRef: RefObject<HTMLCanvasElement>;
    private offscreen?: OffscreenCanvas;
    private worker?: Worker;

    constructor(props: IProps) {
        super(props);
        this.canvasRef = React.createRef();

        this.resizeListener = this.resizeListener.bind(this);

        window.addEventListener('resize', this.resizeListener);
    }

    componentDidMount() {
        let canvas = this.canvasRef.current;
        if (canvas) {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            this.offscreen = canvas.transferControlToOffscreen();
            this.worker = new Worker("./PaintCanvas.ts");
            this.worker.postMessage(this.offscreen, [this.offscreen]);
        }

    }

    resizeListener() {
        let canvas = this.canvasRef.current;
        if (canvas) {
            canvas.width = window.innerWidth > canvas.width ? window.innerWidth : canvas.width;
            canvas.height = window.innerHeight > canvas.height ? window.innerHeight : canvas.height;
            this.offscreen = canvas.transferControlToOffscreen();
            this.worker = new Worker("./PaintCanvas.ts");
            this.worker.postMessage(this.offscreen, [this.offscreen]);
        }
    }

    render() {
        return (
            <>
                <canvas className="noiseCanvas" ref={this.canvasRef}/>
                <div className="overlay">
                </div>
            </>
        );
    }
}

export default Canvas;

And this is the worker that I'm trying to load:

export default class PaintCanvas extends Worker {
    private canvas?: OffscreenCanvas;
    private intervalId?: number;
    private frame: number;
    private frameSet: number;
    private noiseData: ImageData[][];
    private noiseNum: number[];
    private overlayFrame: number[];
    private overlayData: Uint8ClampedArray[][];
    private overlayNum: number[];
    private workers: (Worker|undefined)[];

    constructor(stringUrl: string | URL) {
        super(stringUrl);
        this.frame = 0;
        this.frameSet = 0;
        this.noiseData = [[], [], []];
        this.noiseNum = [0, 0, 0];
        this.overlayFrame = [0, 0, 0];
        this.overlayData = [[], [], []];
        this.overlayNum = [0, 0, 0];

        this.workers = [undefined, undefined, undefined];
    }

    onmessage = (event: MessageEvent) => {
        this.canvas = event.data;
        this.frame = 0;
        this.frameSet = 0;
        this.noiseData = [[], [], []];
        this.noiseNum = [0, 0, 0];
        this.overlayFrame = [0, 0, 0];
        this.overlayData = [[], [], []];
        this.overlayNum = [0, 0, 0];
        if (this.workers[0]) {
            this.workers[0].terminate();
        }
        if (this.workers[1]) {
            this.workers[1].terminate();
        }
        if (this.workers[2]) {
            this.workers[2].terminate();
        }
        this.makeNoise(0);
        this.makeNoise(1);
        this.makeNoise(2);
        if (this.intervalId) {
            window.clearInterval(this.intervalId);
        }
        this.intervalId = window.setInterval(this.paintNoise, 100);
    }

    makeNoise(index: number) {
        if (this.canvas) {
            const width = this.canvas.width;
            const height = this.canvas.height;
            this.workers[index] = new Worker("./FillCanvas.ts");
            if (this.workers[index]) {
                this.workers[index]!.onmessage = (event) => {
                    if (this.overlayNum[index] < 4 || !event.data[0]) {
                        this.overlayData[index].push(event.data);
                        this.overlayNum[index]++;
                        if (this.overlayNum[index] < 4) {
                            this.workers[index]!.postMessage([width, height]);
                        } else {
                            this.workers[index]!.postMessage([width, height, new Uint8ClampedArray(width * height * 4), this.overlayData[index][0]]);
                            this.overlayFrame[index]++;
                        }
                    } else {
                        if (event.data[0]) {
                            this.noiseData[index].push(new ImageData(event.data[0], width, height));
                            this.noiseNum[index]++;
                            if (this.noiseNum[index] < 30) {
                                this.workers[index]!.postMessage([width, height, event.data[1], this.overlayData[index][Math.ceil(this.overlayFrame[index] / 4) % 4]]);
                                this.overlayFrame[index]++;
                            } else {
                                this.workers[index] = undefined;
                            }
                        }
                    }
                }
                this.workers[index]!.postMessage([width, height]);
            }
        }
    }

    paintNoise() {
        if (this.noiseNum[0] > 10) {
            this.frame++;
            if (this.frame % this.noiseNum[this.frameSet % 3] === 0) {
                this.frameSet++;
            }
            if (this.canvas) {
                let ctx = this.canvas.getContext("2d");
                if (ctx) {
                    ctx.putImageData(this.noiseData[this.frameSet % 2][this.frame % this.noiseNum[this.frameSet % 2]], 0, 0);
                }
            }
        }
    }
}

As you can see my worker will also be creating its own workers once this works properly.
You should also notice the workerPath import at the top. I tried implementing the top answer from this similar stackoverflow question from a few years ago, and that did make the code itself visible but it wouldn't work as long as the code remains a typescript module. Instead I tried just instantiating the worker using the constructor of the class, eg new PaintCanvas(), but that still required a url and it still only finds the index.html file.
I also tried putting the worker file in the react public folder and using the public url to reference it. The tsconfig file automatically noticed this and added the file to the "include" section, which I thought looked promising, but it still just tried to execute index.html.
So my question is, is there an idiomatic non-hacky way to implement Web Workers in typescript in react or should I use one of the hacky methods I've seen elsewhere? I know that the Web Worker API is newly matured so hopefully the workarounds that a lot of the answers I'm finding online offer up aren't necessary anymore.

2
  • The browser cannot run TypeScript directly. It must be compiled to JavaScript first and the non-JS elements stripped out of it. How are you building the rest of your project? Commented Mar 16, 2020 at 2:53
  • @SeanVeira I used create-react-app to create the app so I believe that I'm using webpack to build the project. I know that the browser can't run typescript directly but all the rest of my code is typescript and that's running just fine. Commented Mar 16, 2020 at 10:52

2 Answers 2

4

The problem with import * as workerPath from "file-loader?name=[name].js!./PaintCanvas"; is that file-loader doesn't transform the file you reference, it simply copies it over to your output directory, so you're dealing with an untranspiled file in that case.

In the this.worker = new Worker("./PaintCanvas.ts"); webpack doesn't see the import as an import and so your code is run as-is and the ./PaintCanvas.ts URL hits your web server and it serves up whatever it wants since there isn't a static asset there (in this case, I'm assuming webpack-dev-server it's hitting the catch-all HTML for the index page).

What you need to do is pull in worker-loader so you can still have your code pass through the rest of the pipeline, but properly load it as a web worker:

import PaintCanvasWorker from 'worker-loader!./PaintCanvas';

// ... later ...
this.worker = new PaintCanvasWorker();
Sign up to request clarification or add additional context in comments.

1 Comment

Yeah, ok. Worker-loader was one of the hacky ways that I didn't want to have to use, but if that's the only answer at the moment then that's fine.
2

2022 UPDATE:

Webpack 5 has greatly simplified this task with their new resolve syntax:

// src/App.tsx
// --snip--

const worker = new Worker(new URL('./path/to/worker', import.meta.url));
worker.onmessage = (e: MessageEvent<string>) => {
    console.log('Received from worker:', e.data);
};
worker.postMessage('I love dogs');

// --snip--
// src/worker.ts
const self = globalThis as unknown as DedicatedWorkerGlobalScope;

self.onmessage = (e: MessageEvent<string>) => {
    console.log('Worker received:', e.data)
    self.postMessage(e.data + ' and cats');
};

Output:

> Worker received: I love dogs
> Received from worker: I love dogs and cats

2 Comments

What is import.meta.url in this scenario? Where is that defined? I'm getting undefined when using it.
@JeffreyTillwick It's part of Webpack 5: webpack.js.org/api/module-variables/#importmetaurl, you won't have it unless you're using Webpack

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.