جارٍ التحميل...
جارٍ التحميل...
The BetterDocs analytics dashboard parses 30-day docs traffic into ApexCharts on every state change — and it was visibly stuttering when the dataset got large. Moved the parsing into a Web Worker and the panel stopped jank-locking the input field. Notes on what belongs in a worker and what does not.
The BetterDocs analytics dashboard renders ApexCharts on every state change — page-view trends, search queries, top documents, the usual. With a small dataset it is fine, but on workspaces with a few months of history the chart-data preparation took 80-120ms per render. The user noticed this as a typing lag in the date-range filter — every keystroke recomputed the data, and every recomputation blocked the input. The fix was to move the data shaping into a Web Worker.
The browser's main thread has to do too many things at once. Paint the frame. Run your React render. Handle the mouse click. Parse the JSON response. If any of these takes more than 16ms, the user sees jank. Web Workers are the built-in answer — separate JavaScript threads that run in parallel and cannot block the UI.
A worker is most useful for pure CPU-bound work that can be expressed as a function — input in, output out, no DOM access. The overhead of postMessage plus structured cloning means tiny tasks are faster on the main thread; the win appears once a task costs more than 10-20ms.
Vite, Next.js, and Webpack 5 all support a native worker import syntax. No separate chunk configuration, no inline worker strings — just import the file with a ?worker suffix and you get a Worker constructor.
// worker.ts — runs in a separate thread
self.onmessage = (e: MessageEvent<string>) => {
const result = expensiveParse(e.data);
self.postMessage(result);
};
// main.ts — runs on the main thread
import MyWorker from './worker.ts?worker';
const worker = new MyWorker();
worker.onmessage = (e) => {
console.log('Got result:', e.data);
};
worker.postMessage(largeJsonString);معلومة
structuredClone happens at the postMessage boundary — objects are deep-copied between threads. For large ArrayBuffers, use the transferable flag to move ownership instead of copying.
Raw postMessage is clunky. Comlink wraps workers in a Proxy so you can call their methods like local async functions. This makes integrating workers with a React app feel natural.
// heavy-worker.ts
import { expose } from 'comlink';
const api = {
async searchLargeDataset(query: string, data: Record[]) {
return data.filter((r) =>
Object.values(r).some((v) => String(v).includes(query))
);
},
async parseCsv(text: string) {
return Papa.parse(text, { header: true }).data;
},
};
export type HeavyApi = typeof api;
expose(api);
// main.ts — use the worker like a normal async object
import { wrap } from 'comlink';
import HeavyWorker from './heavy-worker.ts?worker';
import type { HeavyApi } from './heavy-worker';
const worker = wrap<HeavyApi>(new HeavyWorker());
const results = await worker.searchLargeDataset('john', records);Wrap worker initialization in a useMemo or a custom hook to avoid recreating the worker on every render. Terminate workers on unmount to free the thread.
function useHeavyWorker() {
const worker = useMemo(() => wrap<HeavyApi>(new HeavyWorker()), []);
useEffect(() => {
return () => worker[releaseProxy]();
}, [worker]);
return worker;
}
function SearchBox({ data }: { data: Record[] }) {
const worker = useHeavyWorker();
const [query, setQuery] = useState('');
const [results, setResults] = useState<Record[]>([]);
useEffect(() => {
if (!query) return;
let cancelled = false;
worker.searchLargeDataset(query, data).then((res) => {
if (!cancelled) setResults(res);
});
return () => { cancelled = true; };
}, [query, data, worker]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultList items={results} />
</>
);
}For workloads that need true zero-copy sharing between threads — real-time audio processing, WebAssembly threads, high-frequency data streaming — SharedArrayBuffer with Atomics lets the main thread and workers read/write the same memory. The site must be cross-origin isolated (COOP/COEP headers) to enable it.
تحذير
SharedArrayBuffer requires cross-origin isolation via COOP and COEP headers. This breaks many third-party embeds (Stripe checkout, YouTube iframes). Check your third-party dependencies before opting in.
Interaction to Next Paint (INP) measures the delay between user input and visible response. Long tasks on the main thread are the primary cause of poor INP. Moving even one 80ms task to a worker can drop your p75 INP below the 200ms Good threshold. Profile with Chrome DevTools Performance panel to find the actual long tasks before optimizing blindly.
OffscreenCanvas lets workers render to a canvas element on the main thread. This is how modern data-viz tools and games keep the UI responsive while rendering complex scenes. Transfer the canvas control to the worker with transferControlToOffscreen().
// main.ts
const canvas = document.querySelector('canvas')!;
const offscreen = canvas.transferControlToOffscreen();
const worker = new RenderWorker();
worker.postMessage({ canvas: offscreen }, [offscreen]);
// render-worker.ts
self.onmessage = (e) => {
const canvas: OffscreenCanvas = e.data.canvas;
const ctx = canvas.getContext('2d')!;
// Render loop runs in the worker — does not block main thread
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// expensive drawing...
requestAnimationFrame(draw);
}
draw();
};After moving the BetterDocs analytics chart-data preparation into a Web Worker via Comlink, the input lag on the date-range filter went away — INP dropped from around 250ms to under 80ms on the workspaces with the largest datasets. The integration cost was minimal once Comlink was in place. Web Workers are one of the highest-leverage performance tools most developers still do not reach for. If your app has any computation over 50ms on a hot path, measure the win of moving it off the main thread.
المزيد في Performance
On sapan.dev the locale-detection logic lives at Vercel Edge — sub-30ms response from anywhere on the planet. On TubeOnAI we used Cloudflare Workers for auth-token validation. Notes on what genuinely belongs at the edge and what I have learned the hard way to keep regional.
Shipping sapan.dev across 16 locales including RTL Arabic surfaced every accessibility shortcut I had ever quietly made. Notes on what automated audits miss, what testing with real assistive tech actually catches, and the patterns I now reach for by default.
The WPDeveloper plugin suite serves 6M+ users across 180+ countries — meaning a lot of devices, a lot of network conditions, and a lot of CrUX data. Notes on what actually moved the Core Web Vitals needle on real production traffic and what was performance theater.