DEV Community

Cover image for Improve Table Speed in React by Using Web Workers for Filters
Ravgeet Dhillon
Ravgeet Dhillon

Posted on

Improve Table Speed in React by Using Web Workers for Filters

TL;DR: I implemented a Web Worker–powered filtering system in a React data table component to eliminate UI lag and improve responsiveness when working with large datasets. Here's how and why.

The Problem: Filtering Slows the UI

Our application relies heavily on a custom <NewTable /> component that supports:

  • Nested (hierarchical) rows

  • Column-based filtering

  • Custom filters

  • Server-side pagination

As datasets grew into the thousands of rows, filtering became noticeably sluggish. The culprit? All filtering logic ran on the main UI thread, blocking React’s render cycle and causing the interface to freeze temporarily.

Goal

Move heavy data-filtering logic off the main thread using Web Workers, without breaking existing functionality or developer experience.

The Solution: Asynchronous Filtering with Web Workers

We built a pipeline that:

  1. Serializes hierarchical data into a flat structure

  2. Sends that data to a dedicated Web Worker

  3. Worker filters based on column & custom filters

  4. Sends back the result

  5. Updates the UI reactively and shows a loader while waiting

Implementation Breakdown

1. Set up search.worker.js

We created a Web Worker dynamically using a blob:

const searchWorker = () => {
  onmessage = async (e) => {
    importScripts("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js");
    importScripts("https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js");

    // ... logic for deep filtering, date handling, and custom filters ...

    postMessage({ success, result });
  };
};

let code = searchWorker.toString();
code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"));
const blob = new Blob([code], { type: "application/javascript" });
const workerScript = URL.createObjectURL(blob);

export default workerScript;
Enter fullscreen mode Exit fullscreen mode

2. New Utility Function

A new async utility offloads data filtering:

const asyncFilterDataWithColumnAndCustomFilters = async (
  data,
  columns,
  columnFilters,
  customFilters,
  setData,
  setIsSearching,
) => {
  const searchWorker = new window.Worker(searchWorkerScript);

  const convertedData = data.map(row => ({
    id: row.id,
    values: columns.map(col => col.selector?.(row) ?? row[col.id]),
    items: (row.items || []).map(item => ({
      id: item.id,
      values: columns.map(col => col.selector?.(item) ?? item[col.id]),
    })),
  }));

  searchWorker.postMessage({ data, convertedData, columns, columnFilters, customFilters });

  searchWorker.onmessage = (e) => {
    setData(e.data.result);
    setIsSearching(false);
    searchWorker.terminate();
  };

  searchWorker.onerror = (err) => {
    console.error("Worker error", err.message);
    setIsSearching(false);
    searchWorker.terminate();
  };
};
Enter fullscreen mode Exit fullscreen mode

3. Hook Update

We extended the existing table filter hook:

export const useFilterDataWithColumnAndCustomFilters = ({
  data,
  columns,
  columnFilters,
  customFilters,
  setIsSearching,
}) => {
  const [filteredData, setFilteredData] = useState({ data: [], diff: 0 });

  useEffect(() => {
    asyncFilterDataWithColumnAndCustomFilters(
      data,
      columns,
      columnFilters,
      customFilters,
      setIsSearching,
      setFilteredData
    );
  }, [data, columnFilters, customFilters]);

  return { filteredData: filteredData.data, diff: filteredData.diff };
};
Enter fullscreen mode Exit fullscreen mode

4. Loading Indicator

We updated the table’s loading prop:

<NewTable
  isLoading={isLoading || isSearching}
  // ...
/>
Enter fullscreen mode Exit fullscreen mode

Bonus: What the Worker Can Handle

  • BOOLEAN, DATE, DATETIME, and NUMBER types

  • Interval filters (e.g. date ranges)

  • Null checks: eq: "null" and ne: "null"

  • Multi-select picklists

  • Filters nested rows and their parents

Benefits

  • UI never freezes during filtering

  • Filters run faster, even on large datasets

  • Code is modular and easy to maintain

  • Great user experience with real-time filtering feedback

Key Takeaways

  • Use Web Workers to offload expensive tasks in the browser.

  • Normalize complex data structures before sending to the worker.

  • Gracefully handle worker errors and show meaningful UI states.

  • It's surprisingly easy to integrate with React.

Final Thoughts

Offloading filtering to a Web Worker has been one of the most impactful performance wins for our frontend in recent times. If you're working with large tables or slow filters, give workers a shot — your users (and frame rate) will thank you.

Want help integrating something similar into your React app?

Feel free to connect with me.

I wrote this blog post for my company, CloudAnswers.

Top comments (0)