How do you draw the line between a Web Worker and the main thread without a message spaghetti mess?

What’s up everyone? I’m trying to move some heavier work (filtering/sorting a big dataset plus a little formatting) into a Web Worker so the UI stays snappy, but I keep tripping over where state should live and how chatty the message passing should be.

If you’ve done this in a real app, what’s your rule of thumb for deciding what data gets shipped to the worker vs kept on the main thread, especially when the failure mode is either copying huge payloads (slow/memory) or building a brittle mini-protocol of messages (hard to evolve)?

Treat the worker like a little “simulation server”: keep the big dataset and the filter/sort/index state in the worker, and only send the main thread tiny “queries” (filter params, sort key, page window) plus tiny “results” (row IDs + maybe the 50 visible rows). That’s the part that keeps you out of message spaghetti, because your protocol becomes basically setData once, then query forever. For the “copying huge payloads” pain, the trick is to not copy it repeatedly: send it once (or stream chunks), then avoid shipping full objects back and forth. Are you able to represent the data as typed arrays/transferable buffers, or is it a pile of nested JS objects? That one detail changes how clean this gets.

main thread for interaction state, worker for dataset work.

selection, hover, scroll position, expanded rows — keep those on the UI side. the worker can own the big list, filtering, sorting, and any cached indexes so it isn’t pretending to be a second app.

my rule is pretty boring: send intent across, not whole objects. “filter by these ids” is fine. “here’s the entire table again” is where it gets messy fast.

on formatting, i’d start with raw values and only move it into the worker if profiling shows it’s actually hot. strings are cheap until they aren’t, and then they get weirdly expensive in bulk.

What you wrote is basically the clean line I’ve landed on too: UI state stays UI-side, worker owns “the model” + derived stuff (sort/filter/indexes), and the messages are just intents + small deltas.

If you want to keep it from turning into message spaghetti, the trick is to make it one request/response shape with an id and a type, and treat the worker like a little state machine: main thread says “setFilter / setSort / setViewportRange”, worker replies with “here are the row ids for the current view + total count” (not the whole table). kirupa has a solid primer on the worker messaging basics here: https://www.kirupa.com/html5/web_workers.htm