Skip to content

Worker mode and performance ​

What this page covers ​

With workerMode on, the tree algorithms run in a Web Worker: the main thread only renders, while structural changes, searches and checks are sent to the worker as commands, and results come back as "visible row snapshots". It fits two situations:

  • very large input, where the first build would occupy the main thread;
  • frequent structural changes, where repeated edits keep triggering MPTT recomputation.

The cost is one extra postMessage round trip per interaction. On small datasets the main-thread mode is actually faster — which is why workerMode is off by default.

Parameters ​

propTypeDefaultDescription
workerModebooleanfalseRun the tree algorithms in a Web Worker
chunkedBuildbooleanfalseWrite nodes to WASM in batches; only effective when outputIdOnly is true and no filterFn is set
buildBatchSizenumber2000Nodes per batch (used when chunkedBuild is on)

Metrics ​

getBuildMetrics() ​

Returns per-phase timings of the latest build (milliseconds): inputBridgeMs, inputYieldMs, finalizeMs, layoutBridgeMs, cacheAssemblyMs, cacheYieldMs, layoutReleaseMs, cacheIndexMs, cacheRefreshMs, shownIndicesMs, selectionStatesMs, totalMs.

When asking "why is the first frame slow", read totalMs first and then walk the list above to find which phase it is.

getWorkerMetrics() ​

FieldMeaning
enabledWhether the worker is actually in use
pendingCommands / commandsSent / snapshotsReceivedQueue depth and cumulative counters
lastRoundTripMsMain-thread-observed round trip
lastWorkerResponseMsTime spent inside the worker
lastWorkerSerializeMsVisible-row serialization time
structuralBatches / structuralOperations / lastBatchSizeHow consecutive structural operations were coalesced

Command merging: consecutive structural changes are not sent one by one — they are merged into a batch within a short frame window, so structuralOperations is normally larger than structuralBatches, which is exactly the problem it solves.

Usage ​

vue
<VueGiantTree :tree="rows" root="root" worker-mode v-model="selected" />
ts
const metrics = treeRef.value?.getWorkerMetrics()
if (metrics?.enabled) {
  console.log('round trip', metrics.lastRoundTripMs, 'ms', 'batches', metrics.structuralBatches)
}

Benchmarks (release WASM, local Chromium) are in the repository's performance gate archive.

Things to watch out for ​

These are all measured; ignoring them tends to produce "fast locally, slow in production".

Use shallowRef for large arrays ​

The component walks and serialises the whole tree. If the host keeps it in a deep-reactive ref, every property read goes through a Proxy get trap that lazily creates a proxy per nested object — measured on 100,000 nodes:

FormJSON.stringify(100,000 objects)
raw array17.1ms
ref(...).value277.2ms
shallowRef(...).value19.6ms

The component calls toRaw() internally, so both work, but shallowRef is cheaper — tree data is only ever replaced wholesale.

ts
// Recommended
const rows = shallowRef(buildRows())
// Not recommended
const rows = ref(buildRows())

Let the dataset size decide whether to use the Worker ​

Measured with release WASM on local Chromium:

DatasetworkerMode: falseworkerMode: true
10,000total 22.8ms, no blockingroundTrip 42.7ms, no blocking
100,000total 242ms, main thread blocked 249msroundTrip 53.4ms, no blocking
1,000,000total 2697ms, main thread blocked 2749msroundTrip 584ms

Below 10,000 the main thread is faster (one less round trip); above ~100,000 prefer the Worker. First enabling it loads the worker script and the WASM once; it is reused afterwards.

Replace data by changing tree, not by remounting with :key ​

vue
<!-- Recommended: the Worker and WASM instance are reused; ~600ms for 1M nodes -->
<VueGiantTree :tree="rows" worker-mode />

<!-- Not recommended: every rebuild destroys and recreates the Worker -->
<VueGiantTree :key="reloadKey" :tree="rows" worker-mode />

Replacing tree goes through the binary streaming channel (UTF-8 + transfer), while a remount has to re-initialise the Worker.

outputIdOnly / filterFn disable chunkedBuild ​

chunkedBuild only applies when outputIdOnly === true and no filterFn is used. Otherwise the component falls back to the whole-tree JSON path and the main thread is occupied in one go. An inputBridgeMs of 0 in getBuildMetrics() means the chunked path was not taken.

Try it ​

Both the workerMode switch and the two metric methods live in the full playground: turn on "workerMode", then press "getBuildMetrics / getWorkerMetrics" and the panel shows enabled=true with real round-trip times.