Zero Config  ·  TypeScript-Native  ·  Under 2KB

Stop Janking. Start FluxWorking.

Offload any heavy computation to a background thread with one line of code. No separate files. No postMessage wiring. No type safety lost.

See Live Demo View on GitHub
<2KBGzipped bundle
0Extra files needed
16msJank threshold
100%Type-safe API
// The Problem

The Worker Barrier is
killing your UX.

Every heavy calculation runs on the same thread as your UI. The standard fix requires separate files, manual wiring, and zero type safety — so most developers just skip it.

the-old-way.ts — too much boilerplate
// ① Separate file you must maintain
// worker.ts
self.addEventListener('message', (e) => {
  const result = e.data * e.data;
  self.postMessage(result);
});

// ② Manual wiring in main.ts — no types, no promises
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(42);
worker.addEventListener('message', (e) => {
  console.log(e.data); // any type. no autocomplete. good luck.
});
// ③ Don't forget to .terminate() or you have a memory leak. Forever.
// The Solution

One function.
Zero friction.

FluxWorker serializes your function, spawns a Worker from a Blob URL, and returns a fully-typed Promise. The Worker terminates itself when done.

fluxworker-usage.ts
import { createWorker, jankMonitor } from 'fluxworker';

// Any pure function — no changes needed
const heavyFib = (n: number): number => {
  if (n <= 1) return n;
  return heavyFib(n - 1) + heavyFib(n - 2);
};

// Wrap it — that's literally it
const workerFib = createWorker(heavyFib);
//    ^ TypeScript infers: (n: number) => Promise<number> 🎯

// Call it just like before — but async
const result = await workerFib(42);
// UI never froze. Worker terminated. Blob URL revoked. Clean.

// Optional: dev-mode jank detection
if (import.meta.env.DEV) jankMonitor.enable();
// Live Demo

Feel the freeze.
Then feel the fix.

Both buttons compute fib(42) — a ~2 second CPU task. Watch the bouncing ball. That ball is your UI thread.

Without FluxWorker Main Thread

Thread: Shared with your computation
Ball freezes while fib(42) runs.

Waiting — click Run to start.
With FluxWorker Background Thread

Thread: Free — UI stays live
Ball keeps moving while fib(42) runs.

Waiting — click Run to start.
JANK MONITOR — fluxworker/dev
// Features

Everything it needs.
Nothing it doesn't.

A micro-library that solves one problem with ruthless focus.

Dynamic Worker Spawning
Converts any function to a Blob URL and spawns a Worker on the fly. No build plugins, no separate files, no Webpack config.
Full Type Inference
createWorker(fn) returns a typed async version of your function — arguments and return type inferred automatically.
Jank Monitor
Dev-mode tool that measures execution time. If any function blocks the main thread for >16ms it logs an actionable warning with the fix.
Smart Fallback
If Workers aren't available (SSR, older browsers), FluxWorker silently executes locally. Your code doesn't change. It doesn't throw.
Auto Lifecycle Management
Every Worker is terminated and its Blob URL revoked immediately after resolution or rejection. Zero memory leaks on every exit path.
SharedArrayBuffer Ready
Pass typed arrays via SharedArrayBuffer for zero-copy data transfer — essential for ML inference, image processing, and audio pipelines.
// vs The Competition

How FluxWorker
stacks up.

Compared against Comlink, the current community standard.

FeatureFluxWorkerComlinkRaw Worker
Zero separate filesYesNeeds worker fileNeeds worker file
Bundle size<2KB~5KB0KB
Promise / async-awaitNativeVia ProxyManual callback
TypeScript inferredAutomaticManual typesNone
Auto cleanup on doneYesManual .terminate()Manual
Jank detectionBuilt-inNoneNone
SSR / no-Worker fallbackYesThrowsThrows
SharedArrayBufferYesYesYes
// Architecture

Four layers.
One clean abstraction.

Every call through createWorker passes through this pipeline.

01 · Serialize
Serializer Engine
fn.toString() wrapped in Worker boilerplate, converted to a Blob URL
02 · Bridge
Communication Bridge
Unique request ID per call — postMessage in, result out, no cross-talk
03 · Guard
Safety Proxy
try/catch inside the Worker — crashes reject the Promise, never hang the app
04 · Clean
Lifecycle Manager
worker.terminate() + URL.revokeObjectURL() on every resolve and reject path
// Source

The entire library.
Under 80 lines.

This is the real implementation. No magic. No dependencies.

src/index.ts — full source
type AnyFn = (...args: any[]) => any;
export type WorkerFn<T extends AnyFn> =
  (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>>;

export const jankMonitor = {
  _enabled: false,
  enable()  { this._enabled = true; },
  disable() { this._enabled = false; },
  measure<T>(name: string, callFn: () => T): T {
    if (!this._enabled) return callFn();
    const t0 = performance.now();
    const result = callFn();
    const ms = performance.now() - t0;
    if (ms > 16) console.warn(`[FluxWorker] "${name}" blocked ${ms.toFixed(1)}ms — wrap it.`);
    return result;
  }
};

export function createWorker<T extends AnyFn>(fn: T): WorkerFn<T> {
  if (typeof Worker === 'undefined')
    return ((...args) => { try { return Promise.resolve(fn(...args)); }
                            catch(e) { return Promise.reject(e); } }) as WorkerFn<T>;

  return function(...args) {
    return new Promise((resolve, reject) => {
      // ① Serialize
      const src = `const fn=${fn.toString()};self.onmessage=e=>{try{const r=fn(...e.data.args);r?.then?r.then(v=>self.postMessage({id:e.data.id,result:v})).catch(err=>self.postMessage({id:e.data.id,error:String(err)})):self.postMessage({id:e.data.id,result:r})}catch(err){self.postMessage({id:e.data.id,error:String(err)})}};`;
      const url    = URL.createObjectURL(new Blob([src], {type:'application/javascript'}));
      const worker = new Worker(url);
      const id     = Math.random().toString(36).slice(2);
      // ② Bridge
      worker.postMessage({ id, args });
      // ③ Guard + ④ Clean
      const done = (ok: boolean, val: any) => {
        worker.terminate(); URL.revokeObjectURL(url);
        ok ? resolve(val) : reject(new Error(val));
      };
      worker.onmessage  = e => done(!e.data.error, e.data.error ?? e.data.result);
      worker.onerror    = e => done(false, e.message);
    });
  } as WorkerFn<T>;
}
// Roadmap

Three phases.
Ship after Phase 3.

The build plan — commit after each phase.

Phase 1
MVP
Core createWorker — The Loop
Serializer Engine + Communication Bridge. Goal: square a number off the main thread, verify the round-trip end-to-end.
Shipped
Phase 2
Types
TypeScript Generics + Jank Monitor
WorkerFn<T> generic so your IDE autocompletes arguments. jankMonitor for dev-time warnings. Smart SSR fallback.
Shipped
Phase 3
Demo
The Show-Off Demo — Make People Feel It
Side-by-side demo with a real visual indicator. Deploy to Vercel. Share the link. This page is that demo.
You're looking at it