# How to replicate data from the main thread to a web worker using ImmerJS

See how Flux used Immer patches and Web Workers to keep browser-based PCB layout responsive while reducing data transfer.

Written by [Greg Dingle](#)

Published Nov 2026 - 5 min read

[![Abstract image of main thread to web worker data transfer](https://flux.seobdtools.com/p/assets/blogs/immerjs/main.webp)](#)

### Introduction

Web Workers are an established browser technology for running Javascript tasks in a background thread. They're the gold standard for executing long-running, CPU-intensive tasks in the browser. At Flux.ai, we successfully harnessed Web Workers, paired with ImmerJS patches, to minimize data transfer and deliver an ultra-fast user experience. This post will take you through our journey of using Web Workers and ImmerJS for data replication in our web-based EDA tool.

### The Data Transfer Problem

Flux.ai, an innovative web-based EDA tool, needs to compute the layout of thousands of electronic components simultaneously for its unique PCB layouting system. This process must adhere to user-defined rules. Our initial prototype revealed that layouting could take several seconds, leading us to explore the capabilities of Web Workers to parallelize this process and unblock the UI.

At bottom, the [web worker API](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/postMessage) is extremely simple. A single method, postMessage, sends data to a web worker, and the same postMessage method is used to send data back to the main thread. We use a popular abstraction layer on top of postMessage, [Comlink](https://github.com/GoogleChromeLabs/comlink), developed several years ago by Google, that makes it possible to call one of your functions in a web worker as if it existed in the main thread. Newer, better or similar abstractions [may exist](https://npmtrends.com/comlink). We did learn in using Comlink that it can easily blow up your JavaScript bundle size.

The trouble with using a web worker in a pure [RPC style](https://en.wikipedia.org/wiki/Remote_procedure_call) is that you most likely have a lot of data to pass through postMessage which is as slow as JSON.stringify, as a [rule of thumb](https://nolanlawson.com/2016/02/29/high-performance-web-worker-messages/). This was definitely true in our case. We calculated that it would take 100ms at our desired level of scale just to transfer the layouting data each way, eating into the benefit of a parallel web worker.

### Exploring SharedArrayBuffer for Data Transfer

A potential solution to the data transfer problem could be using **SharedArrayBuffer**, recommended for use with web workers. However, SharedArrayBuffer "represents a generic raw binary data buffer" meaning that a) it is of fixed size and b) it does not accept JS objects, strings, or other typical application data. Our investigations led us to conclude that the performance benefits were offset by the encoding and decoding costs in **SharedArrayBuffer**. One hope for the future is a [Stage 3 ECMAScript proposal](https://github.com/tc39/proposal-resizablearraybuffer) for growable ArrayBuffers.

### Replicating State with ImmerJS Patches

We decided instead to populate our web worker with all the data on initial load of a Flux document (while the user is already waiting) and update it with changes as they happened. An added benefit of this approach is that the functions designed to run inside the web worker can also be run in the main thread with the flip of a global variable. You might want to do this for [Jest](https://jestjs.io/) tests, for example, which do not support web workers by default.

We got our changes in document data from [ImmerJS](https://github.com/immerjs/immer), something we were already using as part of [Redux Toolkit](https://redux-toolkit.js.org/). Immer is an extremely popular library that enables [copy-on-write](https://en.wikipedia.org/wiki/Copy-on-write) for built-in data types via a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). A lesser-known feature of Immer is [Patches](https://immerjs.github.io/immer/patches). The function produceWithPatches will return a sequence of patches that represent the changes to the original input.

We created a function that wraps produceWithPatches and assigns the patches back into the document for downstream use.

```typescript
//
// in helpers.ts
//
export function withPatches(
  document: IDocumentState,
  mutationFn: Parameters[1]
): IDocumentState {
  const [newDocument, forward] = produceWithPatches(document, mutationFn);
  if (forward.length === 0) {
    return newDocument;
  } else {
    return produce(newDocument, (draftDoc) => {
      draftDoc.latestPatchSeq = forward;
    });
  }
}

//
// in reducers.ts
//
const documentReducer = (
    state: IDocumentState | null = documentInitialState,
    action: IDocumentReduxAction,
): IDocumentState | null => {
    if (!state) {
        // note, we don't create patches on the first load of the document
        if (action.type === Actions.loadDocumentActions.successType) {
            return action.response
        }
        return state;
    } else {
        return withPatches(
            state,
            (draftState) => {
                if (isAnyOf(Actions.setSubjectProperties)(action)) {
                // ... do mutations
            }
        )
    }
}
```

With the patches in hand, we completed our data flow from the main thread to the web worker and back. The main thread calls the worker functions from middleware after every global state change. In Flux, we use [redux-observable](https://redux-observable.js.org/) middleware.

![](https://flux.seobdtools.com/p/assets/blog-content/646617759d6ef7943d726695_dataflow.png)

### Web Worker Code Samples

In the code, the relevant functions look like this, assuming you are using Comlink.  

```typescript
//
// in LayoutEngineInMain.ts
//
import Comlink from "comlink-loader!./LayoutEngineInWorker";
import { Patch } from "immer";

const comlink = new Comlink();

export async function setInitialDocumentState(
  documentState: DocumentState
): void {
  comlink.setInitialDocumentState(documentState);
}

export function applyDocumentPatches(patches: Patch[]): Patch[] {
  const layoutPatches = comlink.applyDocumentPatches(patches);
  // apply these patches to the global state in middleware
  return layoutPatches;
}

//
// in LayoutEngineInWorker.ts
//
import { Patch, applyPatches } from "immer";
import { LayoutEngine, DocumentState } from "./LayoutEngine";

let documentState: DocumentState | undefined;

export function setInitialDocumentState(state: DocumentState): void {
  documentState = state;
}

export function applyDocumentPatches(patches: Patch[]): Patch[] {
  if (documentState === undefined) {
    throw new Error("First call setInitialDocumentState");
  }
  documentState = applyPatches(documentState, patches);
  return new LayoutEngine(documentState).recomputeLayout();
}
```

### Results of Web Worker Data Replication

The result of our use of Web Workers and ImmerJS patches was a significant reduction in workload on every document change and the ability for users to continue interacting with the application during a large re-layout - a priceless benefit in our web-based EDA tool.

### Boosting Speed with ImmerJS

For extra speed in our web worker, we forked the Immer **applyPatches** function. The original version was too slow for our needs. So, we adapted **applyPatches** to skip the draft step and mutate the target object in-place, resulting in a 10X speedup.

In conclusion, Web Workers and ImmerJS have proven to be powerful tools for efficient data replication in Javascript, particularly in the context of our web-based EDA tool, Flux.ai. They offer a potent combination for handling complex, CPU-intensive tasks, and improving user experience through faster data transfer and processing.

‍

## About the author

Greg Dingle

Software Engineer at Flux

Building the future with friends

Let’s start building

Build your big idea together with Flux. All you need to do is describe what you want to make, or what problem you want to solve. Flux will work with you to make it a reality.

![Close-up photo of a PCB made with Flux, with an ESP32 module, buttons, and a microSD slot.](https://flux.seobdtools.com/p/assets/blogs/miscellaneous/sidebar-cta-pcb.webp) [Try it Free](https://flux.seobdtools.com/signup)

## Related Content

[

![Teal isometric illustration of an SDR circuit board in an open enclosure, with a regulator feedback divider highlighted for inspection](https://flux.seobdtools.com/p/assets/blogs/first-spin-debug-imported-altium-board/main.webp)

### Debug an Imported Altium Board From the First-Spin Symptom

3 Min Read](https://flux.seobdtools.com/p/blog/first-spin-debug-imported-altium-board)[

![Teal technical illustration of two circuit boards in open enclosures, with an exponential step-response curve and a low-pass response diagram](https://flux.seobdtools.com/p/assets/blogs/spice-simulation-imported-altium-cadence-boards/main.webp)

### Run SPICE on Your Imported Altium and Cadence Boards

4 Min Read](https://flux.seobdtools.com/p/blog/spice-simulation-imported-altium-cadence-boards)[

![a space-themed background with stars and a floating astronaut helmet to the left. Large, bold text reads '6 Prompting Tips to Get the Most Out of Copilot', indicating a focus on maximizing the use of a digital assistant or tool.](https://flux.seobdtools.com/p/assets/blogs/copilot-6-prompting-tips/main.webp)

### 6 Prompting Tips to Get the Most Out of Copilot

6 Min Read](https://flux.seobdtools.com/p/blog/6-prompting-tips-to-get-the-most-out-of-copilot)[

![Isometric teal line drawing on a dark background: a circuit board and its open enclosure on a workbench, with connected checkpoints that lead from a plan to a finished device](https://flux.seobdtools.com/p/assets/blogs/ai-adoption-hardware-teams-customer-success/main.webp)

### AI Adoption Is a People Problem: How Flux Customer Success Gets Hardware Teams to ROI

7 Min Read](https://flux.seobdtools.com/p/blog/ai-adoption-hardware-teams-customer-success)[

![A typescript code displayed on a screen](https://flux.seobdtools.com/p/assets/blogs/nouncheckedindexed/main.webp)

### Convert your Typescript codebase to No Unchecked Indexed Access

5 Min Read](https://flux.seobdtools.com/p/blog/convert-your-typescript-codebase-to-no-unchecked-indexed-access)[

![Glowing teal isometric line art of an isolated AC/DC power board with a transformer across a slot, next to a metal enclosure, on a dark background](https://flux.seobdtools.com/p/assets/blogs/creepage-and-clearance-pcb/main.webp)

### Creepage vs. Clearance: A Worked Example on an Isolated Power Board

10 Min Read](https://flux.seobdtools.com/p/blog/creepage-and-clearance-pcb)

See All (6)

## Let’s start building.

Let’s build your idea together. All you need to do is describe what you want to make, or what problem you want to solve. Flux will work with you to make it a reality.

[Try it Free](https://flux.seobdtools.com/signup)

 ![Close-up of a Flux-designed printed circuit board](https://flux.seobdtools.com/p/assets/homepage-v2/mission-visual.webp)
