Fiber UI LogoFiberUI

useWorker

Run heavy JavaScript functions in a background Web Worker to prevent UI freezing. The perfect companion to useWasm.

Installation

npx shadcn@latest add https://r.fiberui.com/r/hooks/use-worker.json

A React hook for safely running heavy JavaScript functions off the main thread using Web Workers. Perfect for complex data parsing, huge array manipulations, or heavy math calculations that would otherwise freeze your UI.

Worker Scope limitations

The function passed to useWorker runs in an isolated environment. It cannot access DOM elements, window objects, or any external scope variables that aren't passed explicitly through arguments.

Features

  • Non-blocking - Keeps the main UI thread responsive by running functions in the background
  • Inline Worker - Translates your function into an inline Blob. No extra files required!
  • State Management - Returns handy loading, result, and error states
  • Type Safe - Fully generics-based typing for arguments and return types

Basic Usage

Heavy Math Processing

Heavy Math Processing

Calculating recursive Fibonacci without freezing the UI.

Try calculating Fibonacci(40). If this ran on the main thread, the UI would freeze completely until it finished.

40
"use client";

import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Loader2, Calculator } from "lucide-react";

// This function will seamlessly run in a background thread
// It must not use external variables from the React component
function calculateFibonacci(n: number): number {
    if (n <= 1) return n;
    return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}

export function Example1() {
    const { execute, result, loading, error } = useWorker(calculateFibonacci);
    const [num, setNum] = useState(40); // 40 is high enough to freeze main thread normally

    return (
        <Card className="mx-auto flex max-w-sm flex-col gap-6 p-6">
            <div className="flex items-center gap-3 border-b pb-4">
                <div className="bg-primary/10 text-primary rounded-lg p-2">
                    <Calculator className="h-5 w-5" />
                </div>
                <div>
                    <h3 className="font-bold">Heavy Math Processing</h3>
                    <p className="text-muted-foreground mt-0.5 text-xs">
                        Calculating recursive Fibonacci without freezing the UI.
                    </p>
                </div>
            </div>

            <div className="flex flex-col gap-3 text-sm">
                <p>
                    Try calculating Fibonacci({num}). If this ran on the main
                    thread, the UI would freeze completely until it finished.
                </p>

                <div className="flex gap-2">
                    <Button
                        variant="outline"
                        onClick={() => setNum((n) => Math.max(1, n - 1))}
                        isDisabled={loading}
                    >
                        -
                    </Button>
                    <div className="bg-muted/50 flex flex-1 items-center justify-center rounded-md border font-mono font-bold">
                        {num}
                    </div>
                    <Button
                        variant="outline"
                        onClick={() => setNum((n) => n + 1)}
                        isDisabled={loading}
                    >
                        +
                    </Button>
                </div>

                <Button
                    onClick={() => execute(num)}
                    isDisabled={loading}
                    className="mt-2 w-full"
                >
                    {loading ? (
                        <>
                            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                            Calculating...
                        </>
                    ) : (
                        "Calculate off-thread"
                    )}
                </Button>
            </div>

            {error && (
                <div className="rounded-md border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-500">
                    {error.message}
                </div>
            )}

            {result !== undefined && !loading && (
                <div className="flex flex-col items-center justify-center rounded-lg border border-green-500/20 bg-green-500/10 p-4 text-green-700 dark:text-green-400">
                    <span className="mb-1 text-xs font-bold uppercase tracking-wider">
                        Result
                    </span>
                    <span className="font-mono text-2xl font-black tracking-tighter">
                        {result.toLocaleString()}
                    </span>
                </div>
            )}
        </Card>
    );
}

Massive Array Sorting

Massive Array Sorting

Generating and sorting datasets asynchronously.

Records to Sort:500,000
Try typing here while sorting:
"use client";

import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Database, Loader2, ArrowRight } from "lucide-react";

// Self-contained massive array sorter using a fake dataset
const sortMassiveArray = (
    size: number,
): { count: number; timeTaken: number; sample: any[] } => {
    const start = performance.now();

    // Generate massive array
    const data = Array.from({ length: size }).map((_, i) => ({
        id: i,
        name: `User ${Math.random().toString(36).substring(7)}`,
        score: Math.floor(Math.random() * 100000),
    }));

    // Perform an expensive sort operation
    data.sort((a, b) => b.score - a.score);

    const timeTaken = Math.round(performance.now() - start);

    // Return metadata and top 5 scores
    return {
        count: data.length,
        timeTaken,
        sample: data.slice(0, 5),
    };
};

export function Example2() {
    const { execute, result, loading } = useWorker(sortMassiveArray);
    const [arraySize, setArraySize] = useState(500000);

    return (
        <Card className="mx-auto flex max-w-md flex-col gap-6 p-6">
            <div className="flex items-center gap-3 border-b pb-4">
                <div className="rounded-lg bg-blue-500/10 p-2 text-blue-500">
                    <Database className="h-5 w-5" />
                </div>
                <div>
                    <h3 className="font-bold">Massive Array Sorting</h3>
                    <p className="text-muted-foreground mt-0.5 text-xs">
                        Generating and sorting datasets asynchronously.
                    </p>
                </div>
            </div>

            <div className="flex flex-col gap-4">
                <div className="flex items-center justify-between text-sm">
                    <span className="text-muted-foreground font-medium">
                        Records to Sort:
                    </span>
                    <span className="bg-muted rounded px-2 py-1 font-bold tabular-nums">
                        {arraySize.toLocaleString()}
                    </span>
                </div>

                <div className="flex gap-2">
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => setArraySize(100000)}
                        className="flex-1"
                    >
                        100K
                    </Button>
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => setArraySize(500000)}
                        className="flex-1"
                    >
                        500K
                    </Button>
                    <Button
                        variant="outline"
                        size="sm"
                        onClick={() => setArraySize(1000000)}
                        className="flex-1"
                    >
                        1M
                    </Button>
                </div>

                <Button
                    onClick={() => execute(arraySize)}
                    isDisabled={loading}
                    className="mt-2 bg-blue-600 text-white hover:bg-blue-700"
                >
                    {loading ? (
                        <>
                            <Loader2 className="mr-2 h-4 w-4 animate-spin" />{" "}
                            Sorting Data In Background...
                        </>
                    ) : (
                        "Start Worker Process"
                    )}
                </Button>

                {/* Keep UI interactive during processing */}
                <div className="mt-2 rounded border bg-zinc-100 p-2 text-center text-xs dark:bg-zinc-900">
                    Try typing here while sorting:{" "}
                    <input
                        type="text"
                        className="bg-background ml-2 w-24 border px-1 outline-none"
                        placeholder="Responsive!"
                    />
                </div>
            </div>

            {result && !loading && (
                <div className="animate-in slide-in-from-bottom-2 fade-in mt-2 flex flex-col gap-2">
                    <div className="text-muted-foreground mb-1 flex items-center justify-between text-xs font-bold uppercase tracking-wider">
                        <span>Top 5 Results</span>
                        <span className="bg-primary text-primary-foreground rounded-full px-2 py-0.5 lowercase">
                            {result.timeTaken}ms
                        </span>
                    </div>

                    <div className="flex flex-col gap-1 rounded-lg bg-zinc-950 p-2 font-mono text-sm text-zinc-300">
                        {result.sample.map((s, i) => (
                            <div
                                key={s.id}
                                className="flex justify-between border-b border-zinc-800 px-2 py-1 last:border-0"
                            >
                                <span>
                                    {i + 1}. {s.name}
                                </span>
                                <span className="text-green-400">
                                    {s.score}
                                </span>
                            </div>
                        ))}
                    </div>
                </div>
            )}
        </Card>
    );
}

Heavy JSON Parsing

Heavy JSON Parsing

Parse and transform large datasets in the background.

Ready to process 50MB of raw logs

"use client";

import { useWorker } from "@repo/hooks/performance/use-worker";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { FileDown, FileJson, Loader2, Link } from "lucide-react";

// Mock raw API response payload
const rawDataString = `
[
    {"id": "a1", "timestamp": "2024-03-12T10:00:00Z", "type": "auth", "status": 200, "meta": {"ip": "192.168.1.1", "agent": "Mozilla"}},
    {"id": "b2", "timestamp": "2024-03-12T10:01:15Z", "type": "query", "status": 500, "meta": {"queryTime": "45ms"}},
    {"id": "c3", "timestamp": "2024-03-12T10:05:30Z", "type": "auth", "status": 401, "meta": {"ip": "10.0.0.5"}}
]
`;

// Simulate fetching and parsing a huge JSON string
const parseAndTransformData = (
    jsonString: string,
): { typeCounts: Record<string, number>; errors: number; items: any[] } => {
    // Artificial delay to simulate huge file processing
    const start = Date.now();
    while (Date.now() - start < 1500) {}

    // Parse the data
    const data = JSON.parse(jsonString);

    // Transform and aggregate the data
    const typeCounts: Record<string, number> = {};
    let errors = 0;

    const transformed = data.map((item: any) => {
        // Aggregate types
        typeCounts[item.type] = (typeCounts[item.type] || 0) + 1;

        // Count errors
        if (item.status >= 400) errors++;

        // Return flattened format
        return {
            id: item.id,
            time: new Date(item.timestamp).toLocaleTimeString(),
            type: item.type.toUpperCase(),
            error: item.status >= 400,
        };
    });

    return { typeCounts, errors, items: transformed };
};

export function Example3() {
    const { execute, result, loading, error } = useWorker(
        parseAndTransformData,
    );
    const [viewRaw, setViewRaw] = useState(false);

    return (
        <Card className="mx-auto flex w-full max-w-lg flex-col gap-6 bg-zinc-50 p-6 dark:bg-zinc-900/40">
            <div className="flex items-center justify-between">
                <div className="flex items-center gap-3">
                    <div className="rounded-lg bg-purple-500/10 p-2 text-purple-600">
                        <FileJson className="h-5 w-5" />
                    </div>
                    <div>
                        <h3 className="font-bold">Heavy JSON Parsing</h3>
                        <p className="text-muted-foreground mt-0.5 text-xs">
                            Parse and transform large datasets in the
                            background.
                        </p>
                    </div>
                </div>
                <Button
                    variant="outline"
                    size="icon"
                    onClick={() => setViewRaw(!viewRaw)}
                >
                    {viewRaw ? (
                        <Link className="h-4 w-4" />
                    ) : (
                        <FileDown className="h-4 w-4" />
                    )}
                </Button>
            </div>

            {viewRaw ? (
                <div className="max-h-48 overflow-auto rounded-md border border-zinc-800 bg-zinc-950 p-4 font-mono text-xs text-emerald-400">
                    <pre>{rawDataString.trim()}</pre>
                </div>
            ) : (
                <div className="flex flex-col gap-4">
                    {!result ? (
                        <div className="text-muted-foreground bg-background flex flex-col items-center justify-center rounded-xl border border-dashed py-8 text-center">
                            <p className="mb-4 text-sm font-medium">
                                Ready to process 50MB of raw logs
                            </p>
                            <Button
                                onClick={() => execute(rawDataString)}
                                isDisabled={loading}
                                className="bg-purple-600 text-white hover:bg-purple-700"
                            >
                                {loading ? (
                                    <>
                                        <Loader2 className="mr-2 h-4 w-4 animate-spin" />{" "}
                                        Decoding & Parsing...
                                    </>
                                ) : (
                                    "Start worker job"
                                )}
                            </Button>
                        </div>
                    ) : (
                        <div className="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-4">
                            <div className="grid grid-cols-2 gap-3">
                                <div className="bg-background flex flex-col items-center justify-center rounded-xl border p-4 text-center shadow-sm">
                                    <span className="text-3xl font-black text-purple-600 dark:text-purple-400">
                                        {result.items.length}
                                    </span>
                                    <span className="text-muted-foreground mt-1 text-xs font-bold uppercase tracking-wider">
                                        Rows Parsed
                                    </span>
                                </div>
                                <div className="bg-background flex flex-col items-center justify-center rounded-xl border p-4 text-center shadow-sm">
                                    <span className="text-3xl font-black text-red-500">
                                        {result.errors}
                                    </span>
                                    <span className="text-muted-foreground mt-1 text-xs font-bold uppercase tracking-wider">
                                        Errors Found
                                    </span>
                                </div>
                            </div>

                            <div className="bg-background overflow-hidden rounded-xl border text-sm">
                                <div className="bg-muted border-b px-4 py-2 text-xs font-semibold">
                                    Processed Logs Preview
                                </div>
                                <div className="max-h-32 divide-y overflow-auto">
                                    {result.items.map((item: any) => (
                                        <div
                                            key={item.id}
                                            className="hover:bg-muted/50 flex items-center justify-between px-4 py-2 transition-colors"
                                        >
                                            <div className="flex items-center gap-3">
                                                <span className="font-mono text-xs">
                                                    {item.id}
                                                </span>
                                                <span
                                                    className={`rounded px-2 py-0.5 text-[10px] font-bold ${item.type === "AUTH" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/30" : "bg-orange-100 text-orange-700 dark:bg-orange-900/30"}`}
                                                >
                                                    {item.type}
                                                </span>
                                            </div>
                                            <span
                                                className={`font-mono text-xs ${item.error ? "font-bold text-red-500" : "text-muted-foreground"}`}
                                            >
                                                {item.error ? "FAILED" : "OK"}
                                            </span>
                                        </div>
                                    ))}
                                </div>
                            </div>

                            <Button
                                variant="outline"
                                onClick={() => execute(rawDataString)}
                                isDisabled={loading}
                                className="w-full"
                            >
                                {loading ? "Re-processing..." : "Process Again"}
                            </Button>
                        </div>
                    )}
                </div>
            )}

            {error && (
                <div className="rounded-lg border border-red-500/20 bg-red-500/10 p-3 text-sm font-medium text-red-500">
                    {error.message}
                </div>
            )}
        </Card>
    );
}

Error Handling Bounds

Error Handling Bounds

If a Web Worker crashes or throws an exception, it is caught safely by the hook without crashing the main React tree.

Console Output
> Waiting for execution...
"use client";

import { useWorker } from "@repo/hooks/performance/use-worker";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { AlertCircle, TerminalSquare } from "lucide-react";

// Functions in workers must throw standard Javascript Errors
const faultyWorkerFunction = (shouldCrash: boolean) => {
    // Artificial delay to mimic processing
    const start = Date.now();
    while (Date.now() - start < 800) {}

    if (shouldCrash) {
        throw new Error("Out of memory exception during AST traversal module.");
    }

    return "Process completed perfectly.";
};

export function Example4() {
    const { execute, result, loading, error } = useWorker(faultyWorkerFunction);

    return (
        <Card className="mx-auto flex max-w-md flex-col gap-6 border-red-500/20 bg-red-500/5 p-6">
            <div className="flex items-start gap-4">
                <div className="mt-1 rounded-xl bg-red-500/20 p-3 text-red-600 dark:text-red-400">
                    <AlertCircle className="h-6 w-6" />
                </div>
                <div>
                    <h3 className="text-lg font-bold text-red-600 dark:text-red-300">
                        Error Handling Bounds
                    </h3>
                    <p className="text-foreground/80 mt-1 text-sm">
                        If a Web Worker crashes or throws an exception, it is
                        caught safely by the hook without crashing the main
                        React tree.
                    </p>
                </div>
            </div>

            <div className="flex gap-3">
                <Button
                    onClick={() => execute(false)}
                    isDisabled={loading}
                    variant="outline"
                    className="flex-1"
                >
                    Run Safe Task
                </Button>

                <Button
                    onClick={() => execute(true)}
                    isDisabled={loading}
                    variant="destructive"
                    className="flex-1"
                >
                    Force Crash Script
                </Button>
            </div>

            <div className="bg-background mt-2 overflow-hidden rounded-md border">
                <div className="bg-muted/40 flex items-center justify-between border-b px-3 py-2">
                    <span className="text-muted-foreground text-xs font-semibold uppercase">
                        Console Output
                    </span>
                    <TerminalSquare className="text-muted-foreground h-3 w-3" />
                </div>
                <div className="flex min-h-[100px] flex-col justify-center bg-zinc-950 p-4 font-mono text-xs text-zinc-300">
                    {loading ? (
                        <span className="animate-pulse text-blue-400">
                            Running worker thread...
                        </span>
                    ) : error ? (
                        <div className="wrap-break-word flex flex-col gap-1 text-red-400">
                            <span className="font-bold">
                                &gt; Exception Caught in thread:
                            </span>
                            <span>{error.message}</span>
                        </div>
                    ) : result ? (
                        <span className="text-green-400">&gt; {result}</span>
                    ) : (
                        <span className="text-zinc-600">
                            &gt; Waiting for execution...
                        </span>
                    )}
                </div>
            </div>
        </Card>
    );
}

API Reference

Hook Signature

function useWorker<TArgs extends any[], TReturn>(
    workerFunction: (...args: TArgs) => TReturn,
): UseWorkerReturn<TArgs, TReturn>;

Parameters

ParameterTypeDescription
workerFunction(...args: TArgs) => TReturnA completely self-contained function to execute.

Return Value

PropertyTypeDescription
execute(...args: TArgs) => voidInvokes the worker function with the provided args
resultTReturn | undefinedThe output returned by the worker function
loadingbooleantrue while the worker is busy
errorError | undefinedDetailed error object if the worker throws an error

Hook Source Code

import { useState, useEffect, useRef, useCallback } from "react";

/**
 * Run heavy JavaScript functions in a background Web Worker.
 *
 * @param workerFunction The function that will be executed in the worker. Should be self-contained.
 */
export function useWorker<TArgs extends any[], TReturn>(
    workerFunction: (...args: TArgs) => TReturn,
) {
    const [result, setResult] = useState<TReturn | undefined>(undefined);
    const [loading, setLoading] = useState<boolean>(false);
    const [error, setError] = useState<Error | undefined>(undefined);
    const workerRef = useRef<Worker | null>(null);

    // Initialize worker
    useEffect(() => {
        const code = `
            self.onmessage = async function(e) {
                try {
                    const fn = ${workerFunction.toString()};
                    const result = await fn(...e.data);
                    self.postMessage({ status: 'SUCCESS', result });
                } catch (error) {
                    self.postMessage({ status: 'ERROR', error: error.message });
                }
            }
        `;

        const blob = new Blob([code], { type: "application/javascript" });
        const url = URL.createObjectURL(blob);
        const worker = new Worker(url);

        workerRef.current = worker;

        worker.onmessage = (e) => {
            if (e.data.status === "SUCCESS") {
                setResult(e.data.result);
                setError(undefined);
            } else {
                setError(new Error(e.data.error));
            }
            setLoading(false);
        };

        worker.onerror = (e) => {
            setError(new Error(e.message));
            setLoading(false);
        };

        return () => {
            worker.terminate();
            URL.revokeObjectURL(url);
        };
    }, [workerFunction]);

    const execute = useCallback((...args: TArgs) => {
        if (!workerRef.current) return;
        setLoading(true);
        setError(undefined);
        workerRef.current.postMessage(args);
    }, []);

    return { execute, loading, result, error };
}