Skip to content
easy-web-worker
Interactive example · Runtime

A function becomes the worker

Analyze text as you type inside a Worker created from a function, with its types inferred and no worker file to configure.

Try it hereLive example
Application preview

Loading the example…

textWorker.ts
import { createWorker } from 'easy-web-worker/createWorker';

/**
 * A worker with no file: the function below is its source.
 * It cannot use anything from outside, so everything it needs is defined inside.
 */
export const createTextWorker = () =>
  createWorker(() => {
    const words = (text: string) => text.toLowerCase().match(/[a-z0-9']+/g) ?? [];

    return {
      analyze: (text: string) => {
        const all = words(text);
        const counts = new Map<string, number>();

        all.forEach((word) => {
          if (word.length > 3) counts.set(word, (counts.get(word) ?? 0) + 1);
        });

        const frequent = Array.from(counts.entries())
          .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
          .slice(0, 5)
          .map(([word, times]) => ({ word, times }));

        return {
          words: all.length,
          unique: new Set(all).size,
          characters: text.length,
          readingSeconds: Math.round((all.length / 230) * 60),
          frequent,
        };
      },
    };
  });

export type TextWorker = ReturnType<typeof createTextWorker>;
export type TextAnalysis = Awaited<ReturnType<TextWorker['analyze']>>;
Ready. Run the example to begin.Reset returns this example to its initial state.
Take it with you

For small jobs, the worker can live next to the code that calls it.

01

Type in the box. Every change is analyzed inside a worker; the counts update when the answer arrives.

02

Paste a long text. Typing stays smooth, because the counting never runs on the main thread.

03

Type quickly. A call that is still running is canceled when a newer text replaces it.

Read the full guide

The code

createWorker receives a function instead of a file. The function returns the methods, and their types are inferred, so there is no type to export and import.

textWorker.ts
import { createWorker } from 'easy-web-worker/createWorker';

/**
 * A worker with no file: the function below is its source.
 * It cannot use anything from outside, so everything it needs is defined inside.
 */
export const createTextWorker = () =>
  createWorker(() => {
    const words = (text: string) => text.toLowerCase().match(/[a-z0-9']+/g) ?? [];

    return {
      analyze: (text: string) => {
        const all = words(text);
        const counts = new Map<string, number>();

        all.forEach((word) => {
          if (word.length > 3) counts.set(word, (counts.get(word) ?? 0) + 1);
        });

        const frequent = Array.from(counts.entries())
          .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
          .slice(0, 5)
          .map(([word, times]) => ({ word, times }));

        return {
          words: all.length,
          unique: new Set(all).size,
          characters: text.length,
          readingSeconds: Math.round((all.length / 230) * 60),
          frequent,
        };
      },
    };
  });

export type TextWorker = ReturnType<typeof createTextWorker>;
export type TextAnalysis = Awaited<ReturnType<TextWorker['analyze']>>;

The component calls the method on every change and cancels the previous call from the effect cleanup.

RuntimeDemo.tsx
import { useEffect, useRef, useState } from 'react';
import { unwrap } from 'easy-web-worker/createWorker';
import '../shared/demo.css';
import { createLog } from '../shared/log';
import { count } from '../shared/format';
import { createTextWorker, type TextAnalysis, type TextWorker } from './textWorker';

const log = createLog();

const sample =
  'Web Workers move expensive work away from the main thread. The page keeps scrolling while the worker works, and the worker answers when the work is done.';

export function RuntimeDemo() {
  const [text, setText] = useState(sample);
  const [analysis, setAnalysis] = useState<TextAnalysis | null>(null);
  const worker = useRef<TextWorker | null>(null);

  useEffect(() => {
    worker.current ??= createTextWorker();

    const task = worker.current.analyze(text);

    task.then(
      (result) => {
        setAnalysis(result);
        log.write(`analyze(text) resolved · ${count(result.words)} words · ${count(result.unique)} unique`);
      },
      () => {
        // a newer text replaced this call
      },
    );

    // typing again cancels the call that is still running
    return () => {
      task.cancel('Superseded by a newer text');
    };
  }, [text]);

  useEffect(
    () => () => {
      if (worker.current) void unwrap(worker.current).dispose();
      worker.current = null;
    },
    [],
  );

  return (
    <div className="demo">
      <section className="demo-card" aria-label="Text">
        <label>
          Type or paste some text
          <textarea rows={5} value={text} onChange={(event) => setText(event.target.value)} />
        </label>
        <p className="demo-caption">Every change is analyzed inside a worker that was created from a function, with no worker file.</p>
      </section>

      <section className="demo-card" aria-label="Analysis">
        <span>Computed in the worker</span>
        <dl className="demo-stats">
          <div className="demo-stat demo-stat--good">
            <dt>Words</dt>
            <dd data-testid="text-words">{analysis ? count(analysis.words) : '…'}</dd>
          </div>
          <div className="demo-stat">
            <dt>Unique</dt>
            <dd data-testid="text-unique">{analysis ? count(analysis.unique) : '…'}</dd>
          </div>
          <div className="demo-stat">
            <dt>Reading time</dt>
            <dd>{analysis ? `${analysis.readingSeconds} s` : '…'}</dd>
          </div>
        </dl>
        <pre className="demo-json" data-testid="text-frequent">
          {analysis ? analysis.frequent.map(({ word, times }) => `${word} × ${times}`).join('\n') || 'No repeated words yet.' : '…'}
        </pre>
      </section>
    </div>
  );
}

export const resetRuntimeDemo = () => {};

export const watchRuntimeDemo = log.watch;

Related documentation

Keep exploring

Another piece of the model.

↑↓ Navigate ↵ Open Esc Closeeasy-web-worker docs
Get started

Real Web Workers. Typed methods.
Cancelable promises.