Capítulo 14 de 54

Chapter 14: Worker Row Models Guide (Experimental)

Core Idea

An experimental plugin (experimental-worker-plugin) that moves the expensive row-model stages (filtering, grouping+aggregation, sorting) into a Web Worker so recomputation never blocks the main thread — for the narrow case of 100,000+ client-side rows where server-side processing genuinely isn't an option.

Key Concepts

  • Not for typical tables. This is explicitly a proof-of-concept, API may change or be removed, and it's "probably only suitable for extreme use cases." Most large-table problems are better solved with server-side manual* processing, pagination, and virtualization first — reach for this only when the full dataset must live client-side (local-first apps, offline caches, analytics grids) and row-count × comparator cost exceeds a frame budget (~100k+ rows for basic sorts, fewer for expensive custom filter/sort functions).
  • Architecture: a shared config module (columns + tableFeatures) is imported by both the main thread and a worker entry file; the worker runs a headless "shadow table" executing the real row-model pipeline and posts back compact results (index permutations, or serialized row trees when grouping is active). The UI keeps rendering the previous rows while a computation is in flight.
  • Everything in the shared module must be thread-portable: accessorKey columns (or accessors defined in that same module), registry functions (sortFns/filterFns/aggregationFns), no closures over app state.
  • Offloadable stages: createWorkerRowModel(tableWorker, stage) where stage is 'filtered' | 'grouped' | 'sorted' | 'expanded'. Offloaded stages must be a contiguous prefix of the pipeline — offloading filtered alone is fine, but offloading sorted while filtering stays on the main thread would silently skip the filter (a dev-mode warning fires on this mismatch). Never offload expanded (changes every toggle); keep pagination main-thread always (it just slices a page).
  • Loading/timing state lives at table.state.workerRowModels: .isPending, .lastComputeMs, .lastRoundTripMs.
  • Constraints: flat data only (no getSubRows); processing-affecting options (custom globalFilterFn, etc.) must be passed to initTableWorker in the shared config, not just to the table hook; grouping aggregates need both rowAggregationFeature and columnGroupingFeature in the shared features and only compute for columns with an explicit aggregationFn/aggregatedCell; custom aggregation results must survive the browser's structured-clone algorithm; grand totals via column.getAggregationValue() always run main-thread, not in the worker.
  • Failure mode: if the worker fails to load or throws, the plugin logs an error, freezes on the last good result, and stops updating — it does not crash the table. tableWorker.terminate() is a manual escape hatch (nothing auto-terminates yet) and self-heals on the next read.
  • Works with SSR: without a Worker global (server), the table renders unprocessed rows; the client takes over post-hydration.
  • Available from every framework adapter's /experimental-worker-plugin subpath, fully tree-shakable (unused = unshipped).

Code Examples

// tableConfig.ts — imported by BOTH app and worker, must be thread-portable
export const sharedFeatures = tableFeatures({
  rowSortingFeature, columnFilteringFeature, rowPaginationFeature,
  workerRowModelsFeature,
  filteredRowModel: createFilteredRowModel(),
  sortedRowModel: createSortedRowModel(),
  paginatedRowModel: createPaginatedRowModel(), // stays on main thread
  sortFns, filterFns,
})

// table.worker.ts — the entire worker file
import { initTableWorker } from '@tanstack/react-table/experimental-worker-plugin'
initTableWorker({ features: sharedFeatures, columns })

// App.tsx — swap offloaded stages for worker-backed ones
const tableWorker = createTableWorker({
  createWorker: () => new Worker(new URL('./table.worker.ts', import.meta.url), { type: 'module' }),
})
const features = {
  ...sharedFeatures,
  filteredRowModel: createWorkerRowModel(tableWorker, 'filtered'),
  sortedRowModel: createWorkerRowModel(tableWorker, 'sorted'),
}
const table = useTable({ features, columns, data }) // otherwise a completely normal table
  • What it demonstrates: the table hook usage is unchanged — the only difference is which factory produced filteredRowModel/sortedRowModel.

Key Takeaways

  1. Default to server-side processing or plain client-side row models; this plugin is a last resort for genuinely huge client-only datasets, not a performance knob to reach for early.
  2. Offloaded stages must be a contiguous pipeline prefix — mixing offloaded and main-thread stages out of order silently produces wrong results.
  3. Selection, expansion, and pagination remain main-thread state regardless — the worker only ever touches filtering/grouping/sorting.

Connects To

  • Row Models Guide: the pipeline this plugin offloads pieces of.
  • Client-Side vs Server-Side Guide: the first thing to try before reaching for this plugin.