Skip to content

Heavy sorting without blocking the UI: Worker Kit + Virtual Scroller Kit

A list of 100,000+ rows isn't a problem by itself — Virtual Scroller Kit only renders the visible rows. The problem starts once that list is also sorted or filtered on every change: Array.prototype.sort on an array that size is synchronous, main-thread-blocking work, and async/await doesn't save you here — the sort loop itself still runs on the same thread that's drawing the UI.

Worker Kit moves the sort itself onto a real Worker thread, and VirtualList renders the already-sorted result.

The worker

ts
// sort-rows.worker.ts
import { defineWorkerHandler } from 'vue-worker-kit/worker'

interface Row {
  id: number
  text: string
  score: number
}

export default defineWorkerHandler(async (data: Row[], ctx) => {
  return data.sort((a, b) => b.score - a.score)
})

The component

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useWorker } from 'vue-worker-kit'
import { VirtualList } from 'vue-virtual-scroller-kit'

interface Row {
  id: number
  text: string
  score: number
}

const { run, isRunning } = useWorker<typeof import('./sort-rows.worker')>(
  () => new Worker(new URL('./sort-rows.worker.ts', import.meta.url), { type: 'module' }),
)

const rows = ref<Row[]>([])

async function resort(source: Row[]) {
  // sorted: Row[] — inferred from sort-rows.worker.ts, no generic needed
  rows.value = await run(source)
}
</script>

<template>
  <p v-if="isRunning">Sorting…</p>
  <VirtualList :items="rows" :estimated-item-size="48" style="height: 600px">
    <template #default="{ item }">
      <div style="padding: 12px 16px; border-bottom: 1px solid #eee">{{ item.text }}</div>
    </template>
  </VirtualList>
</template>

Why this pair, and not either one alone

VirtualList on its own doesn't speed up the sort — it only speeds up rendering. Without the worker, the UI still freezes for the duration of sort(); the list is just long and fast afterward. The worker on its own isn't necessary for small lists either — postMessage and structural cloning have a real cost, and for a 500-row list, sorting on the main thread finishes faster. The pair earns its keep at the scale where the problem is real: tens or hundreds of thousands of rows, re-sorted often (clicking a column header, typing into a search box).

What's next