A scroll reveal pipeline that keeps the main thread mostly bored

Hey folks, I’m wiring up a scroll + viewport reveal thing for a docs page and I’m trying to keep the main thread free while a worker crunches which sections should be “active” for sticky/parallax bits; the failure mode I keep dodging is janky scroll when DOM reads sneak in.

// main.js
const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    worker.postMessage({
      id: e.target.dataset.id,
      visible: e.isIntersecting,
      ratio: e.intersectionRatio,
      top: e.boundingClientRect.top,
      vh: innerHeight,
    });
  }
}, { threshold: [0, 0.25, 0.5, 0.75, 1] });

document.querySelectorAll('[data-reveal]').forEach(el => io.observe(el));

const worker = new Worker(new URL('./reveal-worker.js', import.meta.url), { type: 'module' });
worker.onmessage = ({ data }) => {
  const el = document.querySelector(`[data-id="${data.id}"]`);
  if (!el) return;
  el.classList.toggle('is-revealed', data.reveal);
  el.style.setProperty('--parallax', data.parallax);
};

// reveal-worker.js
self.onmessage = ({ data }) => {
  const reveal = data.visible && data.ratio > 0.2;
  const parallax = Math.max(-1, Math.min(1, (data.top / data.vh) - 0.5));
  postMessage({ id: data.id, reveal, parallax });
};

Neat part is the concurrency boundary: the worker does the math, the main thread just flips classes/CSS vars, and IntersectionObserver is the only “measurement” I let in.

1 Like

IntersectionObserver is a nice “bouncer” here because it batches the geometry work for you, but you’re still doing one postMessage per entry, per callback. On a long docs page that can turn into a lot of tiny cross-thread chatter, which is its own kind of jank even if you never touch layout. I’d be tempted to buffer the entries for a frame and send one message with an array (or even just send the minimal fields you actually use), then let the worker chew through the batch and return a batch back. I found a related kirupa. com article that can help you go deeper into this topic: