Off-main-thread work
Heavy work — markdown rendering, syntax highlighting, image processing, JSON normalisation — does not have to block the main thread. This workflow uses Concurrency pools and reactive metrics from Reactive / Devtools.
1. Define a worker task
// src/workers/highlight.ts
import { createReactiveTaskPool } from '@bquery/bquery/concurrency';
interface HighlightInput { source: string; lang: string }
interface HighlightOutput { html: string }
export const highlighter = createReactiveTaskPool<HighlightInput, HighlightOutput>(
async ({ source, lang }) => {
// Runs inside the worker. Import third-party libs lazily so the main
// thread does not pull them.
const { highlight } = await import('https://esm.sh/shiki@1');
const html = await highlight(source, { lang });
return { html };
},
{ concurrency: 2, maxQueue: 16 }
);createReactiveTaskPool returns a pool with reactive signal mirrors (pending$, size$, state$, paused$, metrics$) you can read inside any effect(). metrics$ is a Signal<PoolMetrics> exposing { completed, failed, avgRuntimeMs, p95RuntimeMs }.
2. Call it from a component
import { defineComponent, html, safeHtml, slotText, useAsync, useEffect } from '@bquery/bquery/component';
import { sanitizeHtml, trusted } from '@bquery/bquery/security';
import { highlighter } from '../workers/highlight';
defineComponent('code-block', {
props: { lang: { type: String, default: 'ts' } },
state: { source: '', highlighted: '', loading: true },
connected() {
this.setState('source', slotText(this));
const { data, loading } = useAsync(() =>
highlighter.run({
source: this.getState('source'),
lang: this.getProp<string>('lang'),
})
);
useEffect(() => {
this.setState('loading', loading.value);
this.setState('highlighted', data.value?.html ?? '');
});
},
render({ props, state }) {
if (state.loading || !state.highlighted) {
return html`
<pre data-lang=${props.lang}>
<code>${state.source}</code>
</pre>
`;
}
return safeHtml`
<pre data-lang="${props.lang}">
${trusted(sanitizeHtml(state.highlighted))}
</pre>
`;
},
});useAsync schedules the task off the render path and exposes loading so the component shows raw source first and upgrades to highlighted markup when the worker returns.
3. Surface metrics
import { effect } from '@bquery/bquery/reactive';
import { highlighter } from './workers/highlight';
effect(() => {
const { completed, failed, avgRuntimeMs, p95RuntimeMs } = highlighter.metrics$.value;
console.debug(
'[highlight]',
'pending', highlighter.pending$.value,
'size', highlighter.size$.value,
'completed', completed,
'failed', failed,
`${avgRuntimeMs.toFixed(1)}ms (p95 ${p95RuntimeMs.toFixed(1)}ms)`
);
});Wire the same signals into a tiny status bar or a Devtools timeline mark via recordEvent('mark', 'highlight:burst', { source: 'pool', payload: { pending: highlighter.pending$.value } }).
4. Backpressure and graceful pause
// Pause during a busy interaction (e.g. scroll), resume when idle
import { effect } from '@bquery/bquery/reactive';
import { useScroll } from '@bquery/bquery/media';
const scroll = useScroll();
let paused = false;
effect(() => {
const scrolling = scroll.value.isScrolling;
if (scrolling && !paused) { highlighter.pause(); paused = true; }
if (!scrolling && paused) { highlighter.resume(); paused = false; }
});
// Drain before navigation
await highlighter.onIdle();pause() lets in-flight tasks finish before holding the queue; resume() releases the queue; onIdle() resolves once both queue and in-flight count reach zero.
5. Shared memory (when applicable)
For image / audio buffers, use createSharedBuffer(byteLength) to avoid copy costs. This requires the page to be crossOriginIsolated:
import { createSharedBuffer } from '@bquery/bquery/concurrency';
const shared = createSharedBuffer(1024 * 1024);
// Pass the shared buffer as part of the task input (extend `HighlightInput`
// to include `shared?: SharedArrayBuffer` if your handler reads from it).
await highlighter.run({ source: '…', lang: 'ts' });What you exercised
- Reactive pools — pool signal mirrors (
pending$,size$,metrics$, …) feed UIs and observability layers without polling. - Transferables —
withTransferables(value)discoversTransferablevalues inside a payload so they can be moved viapostMessageinstead of cloned. - Concurrency control —
{ concurrency, maxQueue }keep interaction snappy; per-run priority is a number onrunOptions.priority. - Pause / resume / idle — graceful backpressure without dropping work.
Constraints
- Worker bodies are serialised via
new Function(...); your CSP must allow'unsafe-eval'. See the Security concept page for guidance. createSharedBufferrequires COOP/COEP headers andcrossOriginIsolated === true.
Next steps
- Visualise pool metrics over time with a Devtools timeline subscription.
- Combine with the Streaming SSR workflow to offload server-side markdown rendering to a pool.
- Persist long jobs across navigations with a shared worker (advanced — see Concurrency for the lifecycle contract).