npm.io
8.0.3 • Published 1 week ago

easy-web-worker

Licence
ISC
Version
8.0.3
Deps
1
Size
159 kB
Vulns
0
Weekly
0
Stars
26

easy-web-worker

Johnny Quesada

Web Workers are powerful. Now they're also easy.

Let easy-web-worker handle everything between the threads.

You keep the power of native Web Workers without dealing with all the complexity that usually comes with them.

easy-web-worker handles the plumbing between both sides: message routing, request matching, promises, errors, cancellation, progress, transferable objects, lifecycle management, and worker pools.

npm version Downloads License

Website · Documentation · Examples · API reference · Video Tutorial

Created by Johnny Quesada.


Start with a single worker

Imagine defining your worker like this:

export const worker = defineWorker(() => ({
  fibonacci,
}));

function fibonacci(n: number): number {
  return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

And calling it like this:

import type { worker as MathWorker } from './math.worker';
import mathWorkerUrl from './math.worker?worker&url';

/**
 * The type of the worker is inferred from it's declaration,
 * so everything is typed on the main thread.
 */
const mathWorker = createWorker<typeof MathWorker>(mathWorkerUrl);

const result = await mathWorker.fibonacci(40); // 102334155

The computation runs inside a real native Web Worker. The page keeps scrolling, typing and animating while it runs.

No manual postMessage. No message IDs. No response matching. No hand-written promise bridge.

Just functions.

See it live → Same task, two ways to run it. On the main thread the page freezes. In a Worker it never stops. It runs in your browser, with real Workers.


Built for work that should not block the page

Functions, not messages

Every key you return from defineWorker becomes a typed method on the main thread.

// The worker does the heavy lifting
const worker = defineWorker(() => ({
  countWords: (text: string) => text.trim().split(/\s+/).length,
}));
// The main thread remains 100% responsive
await text.countWords(article); // CancelablePromise<number>

How typed workers work →

Return a value, get a promise

A method resolves with what it returns and rejects with what it throws. async works the same way.

const worker = defineWorker(() => ({
  parseReport: async (url: string) => {
    const response = await fetch(url);

    if (!response.ok) throw new Error('Report not available');

    return buildReport(await response.text());
  },
}));

Return values and errors →

Cancellation that reaches the Worker

Every call returns a CancelablePromise. Canceling it tells the Worker to stop.

const task = math.findPrimes(50_000_000);

task.cancel('Canceled by user');

Run it live → · Read the guide

Progress on the same call

Long-running methods report progress through the promise they already returned.

await math.findPrimes(50_000_000).onProgress((percentage) => {
  progressBar.value = percentage;
});

Run it live → · Read the guide

Every core, one option

Set maxWorkers and the same object spreads calls across several native Workers.

const math = createWorker<MathWorker>(source, { maxWorkers: 4 });

// three calls, three threads, at the same time
await Promise.all([math.fibonacci(40), math.fibonacci(41), math.fibonacci(42)]);

Run it live → · Read the guide

Move large buffers instead of copying them

Pass transferable objects as the second argument. Ownership moves and nothing is cloned.

const blurred = await images.blur(buffer, [buffer]);

Run it live → · Read the guide

Any way you create a Worker

A Worker file, a native Worker, your own pool, or just a function. The API on top stays the same.

createWorker<MathWorker>(new URL('./math.worker.ts', import.meta.url)); // Worker file
createWorker<MathWorker>(workerUrl); // URL generated by your bundler
createWorker<MathWorker>(new Worker(...)); // existing Worker
createWorker<MathWorker>([worker1, worker2]); // existing pool
createWorker(() => ({ fibonacci })); // a function, no file at all

Every source, with the setup for each bundler →

No Worker file required

Pass a function instead of a file. Same methods, same inferred types, nothing to configure.

const math = createWorker(() => {
  const fibonacci = (n: number): number =>
    n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

  return { fibonacci };
});

await math.fibonacci(40); // 102334155, typed as number

Run it live → · Read the guide


Installation

npm install easy-web-worker
  • Any framework: built on the native Worker API of the browser, with no dependency on React, Vue, Angular, Svelte or any other UI library.
  • Promises: calls return a CancelablePromise from easy-cancelable-promise, installed with the package.

Quick Start

Your first typed worker
// math.worker.ts
import { defineWorker } from 'easy-web-worker/defineWorker';

const fibonacci = (n: number): number =>
  n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

// 1. Define it (in the Worker file)
const worker = defineWorker(() => ({ fibonacci }));

export type MathWorker = typeof worker;
// main.ts
import { createWorker } from 'easy-web-worker/createWorker';
import type { MathWorker } from './math.worker';

// 2. Create it (on the main thread)
const math = createWorker<MathWorker>(
  new URL('./math.worker.ts', import.meta.url),
  {
    workerOptions: { type: 'module' },
  }
);

// 3. Call it
const result = await math.fibonacci(40); // 102334155

The main thread imports only the type of the Worker. TypeScript infers both sides: math.fibonacci takes a number and returns a CancelablePromise<number>. Change the Worker, and the main thread stops compiling until it matches.

Shipping with a bundler? See Every way to create a worker for the production setup.

Prefer a step-by-step page? Getting started covers the same code, and Keep the page responsive runs it in your browser.

Progress and cancellation

Wrap a method with onMessage to receive the message of the call.

// math.worker.ts
const worker = defineWorker(({ onMessage }) => ({
  findPrimes: onMessage(async (limit: number, message) => {
    const primes: number[] = [];

    for (let candidate = 2; candidate <= limit; candidate++) {
      if (isPrime(candidate)) primes.push(candidate);

      if (candidate % 100_000 !== 0) continue;

      // give the Worker a moment to receive a cancellation
      await new Promise((resolve) => setTimeout(resolve));

      if (!message.isPending()) break;

      message.reportProgress((candidate / limit) * 100);
    }

    return primes;
  }),
}));
// main.ts
const task = math.findPrimes(50_000_000).onProgress((percentage) => {
  progressBar.value = percentage;
});

cancelButton.onclick = () => task.cancel('Canceled by user');

const primes = await task;

Progress and cancellation belong to the call itself, not to a second channel.

This exact pattern runs live in Progress and cancellation: start a long search, watch it advance, and cancel it.


Core Features Deep Dive

Typed workers with defineWorker and createWorker

Write the Worker as a set of methods and call them from the main thread like local async functions.

The Basics
// text.worker.ts
const worker = defineWorker(() => ({
  countWords: (text: string) => text.trim().split(/\s+/).length,

  findDuplicates: (lines: string[]) =>
    lines.filter((line, index) => lines.indexOf(line) !== index),
}));

export type TextWorker = typeof worker;
// main.ts
const text = createWorker<TextWorker>(source);

await text.countWords(article); // number
await text.findDuplicates(lines); // string[]

The contract between the threads lives next to the code that implements it. There is no second file of message types to keep in sync.

How the types cross the threads:

In the Worker On the main thread
() => string () => CancelablePromise<string>
(value: number) => number (payload: number, transfer?) => CancelablePromise<number>
async (ids: string[]) => User[] (payload: string[], transfer?) => CancelablePromise<User[]>
(value?: number) => number (payload?: number, transfer?) => CancelablePromise<number>

A method receives one payload. Send several values as an object or a tuple. Required payloads are enforced when TypeScript runs with strict or strictNullChecks.

What it replaces

fibonacci(40) on the main thread freezes the page for about a second. A Web Worker fixes that, but the native API gives you messages, not functions. This is the least you write by hand:

// fibonacci.worker.js
self.onmessage = ({ data: { id, n } }) => {
  self.postMessage({ id, result: fibonacci(n) });
};
// main.js
const worker = new Worker(new URL('./fibonacci.worker.js', import.meta.url));
const pending = new Map();
let nextId = 0;

worker.onmessage = ({ data: { id, result } }) => {
  pending.get(id)(result);
  pending.delete(id);
};

const fibonacci = (n) =>
  new Promise((resolve) => {
    const id = nextId++;

    pending.set(id, resolve);
    worker.postMessage({ id, n });
  });

And that is before errors, cancellation, progress, types, or a second method.

Return values and errors
// report.worker.ts
const worker = defineWorker(() => ({
  parseReport: async (url: string) => {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Report not available: ${response.status}`);
    }

    return buildReport(await response.text());
  },
}));
// main.ts
try {
  const report = await reports.parseReport('/reports/2026.csv');
} catch (error) {
  console.log((error as Error).message); // 'Report not available: 404'
}

An error rejects that one call. The Worker keeps serving the next one.

Three ways to write a method
const worker = defineWorker(({ onMessage }) => ({
  // 1. a plain function
  double: (value: number) => value * 2,

  // 2. onMessage: same thing, with the message and the event typed for you
  count: onMessage(async (to: number, message, event) => {
    message.reportProgress(50);

    return to;
  }),

  // 3. onMessage().handle: only the message, you complete it
  wait: onMessage<number, string>().handle((message, event) => {
    const timeoutId = setTimeout(
      () => message.resolve('done'),
      message.payload
    );

    message.onCancel(() => clearTimeout(timeoutId));
  }),
}));
Style Completes the call Types
Plain function With the returned value Inferred
onMessage((payload, message, event) => ...) With the returned value Inferred, message typed
onMessage<P, R>().handle((message) => ...) You do Explicit, message.resolve typed

All three are called the same way from the main thread. Use the third for timers, streams, event-based APIs, or to transfer a result.

If a method completes the message itself with resolve, reject or cancel, its return value is ignored.

The message

Available in onMessage, in .handle(...), and in the message API.

Member Purpose
payload Value sent from the main thread
resolve(result?, transfer?) Resolves the call
reject(reason?, transfer?) Rejects the call
cancel(reason?, transfer?) Cancels the call from inside the Worker
reportProgress(percentage, payload?, transfer?) Reports progress and keeps the call pending
onCancel, onResolve, onReject, onProgress Subscribe to each outcome; return unsubscribe
onFinalize Runs once the call ends, whatever the outcome
isPending(), getStatus() Current state of the call
Cancellation

The cancellation reaches the Worker, so the work stops there instead of finishing for nobody. React to it with message.onCancel(...) or check message.isPending().

Canceling twice, or after the call finished, does nothing.

A Worker that never pauses cannot receive a cancellation. See Stop work that never pauses.

Transferable objects
// main.ts
const buffer = await file.arrayBuffer();

const blurred = await images.blur(buffer, [buffer]);

Returned values are cloned. To transfer a result back, complete the call yourself:

// images.worker.ts
const worker = defineWorker(({ onMessage }) => ({
  blur: onMessage<ArrayBuffer, ArrayBuffer>().handle((message) => {
    const result = applyBlur(message.payload);

    message.resolve(result, [result]);
  }),
}));
unwrap: the controls behind the methods

The object returned by createWorker holds only your methods, so names like send, dispose or reboot stay free for your own API. unwrap reaches the EasyWebWorker instance behind it.

import { createWorker, unwrap } from 'easy-web-worker/createWorker';

const math = createWorker<MathWorker>(source);

await unwrap(math).cancelAll('Leaving the page');
await unwrap(math).reboot();
await unwrap(math).dispose();
EasyWebWorker method Purpose
send(payload?, transfer?) Sends a message to the default handler
sendToMethod<TResult, TPayload>(method, payload?, transfer?) Sends a message to a named method
cancelAll(reason?, { force? }) Cancels every pending call; force restarts instead of waiting
override(payload?, reason?, { force? }) Cancels the pending calls and sends a new message
overrideAfterCurrent(payload?, reason?) Keeps the current call, cancels the rest, sends a new message
reboot(reason?) Terminates the Workers, rejects pending calls, starts again
dispose() Cancels pending calls and releases the Workers

The same helper works inside the Worker file, where it returns the Worker-side controls:

import { defineWorker, unwrap } from 'easy-web-worker/defineWorker';

const worker = defineWorker(() => ({
  shutdown: () => unwrap(worker).close(),
}));

unwrap(worker).importScripts('https://example.com/library.js');

// handles messages sent without a method: send, override, overrideAfterCurrent
unwrap(worker).onMessage((message) => {
  message.resolve();
});

One name is not available for a method: then. The worker object is not a promise, so await math never sends a message.

Entry points

Import each side from its own entry, so the Worker bundle carries no main-thread code:

Entry Use it in Exports
easy-web-worker/defineWorker The Worker file defineWorker, unwrap, types
easy-web-worker/createWorker The main thread createWorker, unwrap, types

Everything is also exported from the root easy-web-worker entry.

On the website: Typed workers · Methods and the message · TypeScript · API reference


Every way to create a worker

Pick the source that fits how your project builds and serves its Workers. createWorker, createEasyWebWorker and new EasyWebWorker(...) accept all of them. A function means methods for createWorker and messages for the other two.

Source Best for Pool with maxWorkers reboot / force
URL of a Worker file Worker files in a bundled project
string path A Worker file you serve yourself
Existing Worker Bundlers that detect new Worker(new URL(...)) One Worker
Existing Worker[] Bringing your own pool The array is the pool
Function Workers with no file at all
Function array Workers composed from reusable pieces
A Worker file in development
const math = createWorker<MathWorker>(
  new URL('./math.worker.ts', import.meta.url),
  {
    workerOptions: { type: 'module' },
  }
);

type: 'module' is required when the Worker file uses import or export, which every defineWorker file does.

Vite: development and production

In development, Vite serves the TypeScript file. A production build needs the Worker URL that Vite generates:

import { createWorker } from 'easy-web-worker/createWorker';
import workerUrl from './math.worker?worker&url';
import type { MathWorker } from './math.worker';

// the Worker URL in production, the TypeScript file in development
const isProduction = import.meta.env.MODE === 'production';

const math = createWorker<MathWorker>(
  isProduction ? workerUrl : new URL('./math.worker.ts', import.meta.url),
  {
    workerOptions: { type: 'module' },
  }
);

The same source works with the message API:

import { EasyWebWorker } from 'easy-web-worker';

const worker = new EasyWebWorker(
  isProduction ? workerUrl : new URL('./TextDiff.worker.ts', import.meta.url),
  {
    workerOptions: { type: 'module' },
  }
);
A native Worker you create

webpack 5, Parcel and Vite all detect new Worker(new URL(...)) and bundle the file for you. Create the Worker that way and hand it over:

const math = createWorker<MathWorker>(
  new Worker(new URL('./math.worker.ts', import.meta.url), { type: 'module' })
);

The library cannot create that Worker again, so reboot and force are not available. Leave keepAlive at its default so the Worker is not terminated when idle.

Your own pool

Pass an array of native Workers and calls are distributed across them:

const createMathWorker = () =>
  new Worker(new URL('./math.worker.ts', import.meta.url), { type: 'module' });

const math = createWorker<MathWorker>([
  createMathWorker(),
  createMathWorker(),
  createMathWorker(),
]);
A file you serve yourself

A Worker script that is already built and public only needs its path:

const math = createWorker<MathWorker>('/workers/math.worker.js');
A function, with no file at all
const math = createWorker(() => ({
  double: (value: number) => value * 2,
}));

await math.double(21); // 42

The function returns the methods, exactly like the builder of defineWorker. See Runtime Workers for scope rules, parameters and external scripts.

On the website: Creating workers · Troubleshooting


Worker pools

One Worker runs one task at a time. A pool runs several at once, behind the same object.

Scale on demand
const math = createWorker<MathWorker>(source, {
  maxWorkers: 4,
});

// three calls, three threads, at the same time
const results = await Promise.all([
  math.fibonacci(40),
  math.fibonacci(41),
  math.fibonacci(42),
]);

The calling code does not change. It does not know whether one Worker or four are behind it.

Workers are created as concurrent calls arrive, so an idle application pays for none of them.

Warm up or release the pool
createWorker<MathWorker>(source, {
  maxWorkers: 4,
  warmUpWorkers: true, // create the whole pool up front, for the fastest first call
});

createWorker<MathWorker>(source, {
  maxWorkers: 4,
  keepAlive: false, // terminate idle Workers...
  terminationDelay: 5_000, // ...after five seconds without work
});
Configuration
Option Default Purpose
maxWorkers 1 Maximum number of native Workers
warmUpWorkers true for one Worker Creates the pool during initialization
keepAlive Same as warmUpWorkers Keeps idle Workers alive
terminationDelay 1000 ms Wait before terminating idle Workers
workerOptions {} Native WorkerOptions, such as { type: 'module' }
onWorkerError Rethrows Receives the uncaught errors of the Worker
scripts [] Scripts imported into a runtime Worker
primitiveParameters [] Static values for a runtime Worker, in context

The same options apply to createWorker, createEasyWebWorker and new EasyWebWorker(...).

On the website: Worker pools · Live example: one worker or three


Runtime Workers: a function as the Worker

Give createWorker a function instead of a file. No extra file, no bundler configuration.

The Basics
import { createWorker } from 'easy-web-worker/createWorker';

const math = createWorker(({ onMessage }) => {
  const fibonacci = (n: number): number =>
    n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

  return {
    fibonacci,

    countTo: onMessage(async (limit: number, message) => {
      for (let step = 1; step <= limit; step++) {
        message.reportProgress((step * 100) / limit);
      }

      return limit;
    }),
  };
});

const result = await math.fibonacci(40); // 102334155

The function is the builder of defineWorker: it receives onMessage, returns the methods, and their types are inferred. There is no type to export or import, because both sides are in the same file.

Everything else works the same: cancellation, progress, transferable objects, unwrap, and pools with maxWorkers.

Worker scope

The function becomes the source of a real Worker, so it cannot use variables from outside.

const greeting = 'Hello';

createWorker(() => ({
  // `greeting` does not exist inside the Worker
  greet: (name: string) => `${greeting} ${name}`,
}));

Everything the Worker needs must be defined inside the function, imported as a script, sent as a payload, or passed through primitiveParameters.

Parameters and the scope of the Worker

The second parameter is the global scope of the Worker. Static values arrive in context.primitiveParameters:

const worker = createWorker(
  (_helpers, context) => {
    const [prefix] = context.primitiveParameters;

    return {
      label: (text: string) => `${prefix} ${text}`,
    };
  },
  {
    primitiveParameters: ['Result:'] as [string],
  }
);

await worker.label('complete'); // 'Result: complete'
The message API inside the function

The function also receives easyWorker, the worker instance behind the methods. Everything the message API offers stays available, so existing runtime Workers can move over at their own pace.

const worker = createWorker<{
  double: (value: number) => number;
  triple: (value: number) => number;
}>(({ easyWorker }) => {
  // named handler, message style
  easyWorker.onMessage<number, number>('double', (message) => {
    message.resolve(message.payload * 2);
  });

  // handler for messages sent without a method
  easyWorker.onMessage((message) => {
    message.resolve();
  });

  return {
    triple: (value: number) => value * 3,
    shutdown: () => easyWorker.close(),
  };
});

await worker.double(21); // 42
await worker.triple(21); // 63

Only the returned methods are inferred. Handlers registered through easyWorker are described in the generic, as above, or called with unwrap(worker).sendToMethod(...).

Import scripts

External libraries load with the scripts option and appear in the scope of the Worker:

const text = createWorker(
  (_helpers, context) => {
    // the script adds `Diff` to the scope of the Worker
    const { diffWords } = context.Diff as {
      diffWords: (before: string, after: string) => unknown[];
    };

    return {
      compare: ({ before, after }: { before: string; after: string }) =>
        diffWords(before, after),
    };
  },
  {
    scripts: ['https://cdn.jsdelivr.net/npm/diff@9.0.0/dist/diff.min.js'],
  }
);

const changes = await text.compare({ before, after });

No Worker file is required, and the script does not become part of your application bundle.

Compose a Worker from several functions

Pass an array and every function runs in the same Worker. They share its scope, and the methods they return are merged.

const worker = createWorker([
  // reusable piece: leaves a helper in the scope of the Worker
  (_helpers, context) => {
    context.round = (value: number) => Math.round(value * 100) / 100;
  },

  () => ({
    double: (value: number) => value * 2,
    describe: () => 'first',
  }),

  (_helpers, context) => {
    const round = context.round as (value: number) => number;

    return {
      average: (values: number[]) =>
        round(values.reduce((total, value) => total + value, 0) / values.length),

      // repeated method: the last function wins
      describe: () => 'last',
    };
  },
]);

await worker.double(21); // 42
await worker.average([1, 2, 2]); // 1.67
await worker.describe(); // 'last'

Types are merged the same way: worker.describe takes the signature of the last function. Write the array inline so each function keeps its own type.

Runtime Workers with the message API

createEasyWebWorker builds a Worker from a function too, with messages instead of methods:

import { createEasyWebWorker } from 'easy-web-worker';

const worker = createEasyWebWorker<number, number>(({ onMessage }) => {
  onMessage((message) => {
    message.resolve(message.payload * 2);
  });
});

await worker.send(21); // 42

The first generic is the payload of send(). The second is the value passed to message.resolve(). It also accepts an array of functions that share the scope of the Worker, to compose a Worker from reusable pieces.

On the website: Runtime workers · Live example: a worker without a file


The message API with StaticEasyWebWorker

defineWorker is built on a message API that you can use directly, in Worker files and in runtime Workers.

The Basics
// worker.ts
import { createStaticEasyWebWorker } from 'easy-web-worker/createStaticEasyWebWorker';

const { onMessage } = createStaticEasyWebWorker<number, number>((message) => {
  message.resolve(message.payload * 2);
});

onMessage<string, string>('uppercase', (message) => {
  message.resolve(message.payload.toUpperCase());
});
// main.ts
import { createEasyWebWorker } from 'easy-web-worker';

const worker = createEasyWebWorker<number, number>(source);

await worker.send(21); // default handler
await worker.sendToMethod<string, string>('uppercase', 'hello'); // result type first
Typed methods for an existing Worker

A Worker written with the message API can still be called with typed methods. Describe them:

const worker = createWorker<{ uppercase: (text: string) => string }>(source);

await worker.uppercase('hello');

On the website: Message API · API reference


Advanced Patterns

Latest request wins

For search, previews and autocomplete, an older request is worthless once a newer one exists.

const search = createEasyWebWorker<string, SearchResult[]>(searchBody);

input.oninput = async () => {
  // cancels what is pending, then sends the new message
  const results = await search.override(input.value, 'Superseded');

  render(results);
};

overrideAfterCurrent lets the call in progress finish and replaces only what is waiting behind it.

Stop work that never pauses

A Worker stuck in a synchronous loop cannot receive a cancellation. Restart it instead:

import { unwrap } from 'easy-web-worker/createWorker';

// terminates the Workers, rejects the pending calls, starts fresh ones
await unwrap(math).cancelAll('Taking too long', { force: true });
Clean up when the owner goes away
await unwrap(math).dispose(); // typed workers
await worker.dispose(); // EasyWebWorker

dispose cancels the pending calls, revokes the generated Worker URL when there is one, and terminates the Workers.

On the website: Cancellation and progress · Testing · Troubleshooting


Documentation and examples

Everything in this README runs for real on the easy-web-worker website: real Web Workers, in your browser, against the same files that are published to npm.

See it before you install it
Live example What you will see
Keep the page responsive The same computation on the main thread and in a Worker. One freezes the page, the other does not.
Progress and cancellation A long search that reports how far it is, and stops inside the Worker when you cancel it.
Worker pool Three searches with one Worker and then with three. Same answers, sooner.
Transferable buffers A large buffer moved to the Worker instead of copied.
Worker without a file A Worker created from a function, with no extra file and no bundler setup.

Every example shows its full source next to the running result.

Then go deeper
Guide What you will find
Getting started Define your first worker and call it from the main thread.
Guides Typed workers, cancellation, progress, transferables, pools and bundler setup.
API reference defineWorker, createWorker, unwrap, the message and the configuration.
Testing Test worker logic as plain functions, and the calling code without a Worker.
Troubleshooting Symptoms, causes and fixes.
Platform and versions Browser support and what changes when upgrading.

Built to grow with your application

Without easy-web-worker With easy-web-worker
postMessage() protocols Functions
Manual request IDs Automatic routing
Response matching Promises
Custom error transport Throw / catch
Custom cancellation protocol task.cancel()
Custom progress messages .onProgress()
Hand-maintained TypeScript contracts Inferred types
Manual Worker orchestration Built-in pools
Manual lifecycle handling Warm-up, keep-alive and idle termination

The Worker is still a real Web Worker, and the browser still provides the isolation and the extra thread. easy-web-worker removes what you would otherwise build around it.

Write Worker code like an API, call it like normal asynchronous JavaScript, and scale it without rebuilding the communication layer.


Get Started Now

npm install easy-web-worker

Then in your Worker file:

import { defineWorker } from 'easy-web-worker/defineWorker';

const fibonacci = (n: number): number =>
  n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);

const worker = defineWorker(() => ({ fibonacci }));

export type MathWorker = typeof worker;

And in your app:

import { createWorker } from 'easy-web-worker/createWorker';
import type { MathWorker } from './math.worker';

const math = createWorker<MathWorker>(source);

console.log(await math.fibonacci(40)); // 102334155

Continue with the guides and interactive examples to build your next worker.


Collaborators


Built by Johnny Quesada

easy-web-worker exists because using another thread should not require building a messaging framework first.

If it makes your codebase simpler, consider starring the project. It helps other developers discover it.

Star on GitHub · Read the docs · Run the examples · Report an issue

Earlier resources

Demos and walkthroughs from previous versions, kept for reference. For the current API, start with the documentation website.

Keywords