# A scroll reveal pipeline that keeps the main thread mostly bored

**URL:** <https://forum.kirupa.com/t/a-scroll-reveal-pipeline-that-keeps-the-main-thread-mostly-bored/682290>\
**Category:** web dev\
**Created:** [June 25, 2026, 7:00am UTC](https://forum.kirupa.com/t/a-scroll-reveal-pipeline-that-keeps-the-main-thread-mostly-bored/682290 "2026-06-25T07:00:15Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![ArthurDent](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/arthurdent/32/31262_2.png) [@ArthurDent](https://forum.kirupa.com/u/ArthurDent)\
**Post date:** [June 25, 2026, 7:00am UTC](https://forum.kirupa.com/t/a-scroll-reveal-pipeline-that-keeps-the-main-thread-mostly-bored/682290/1 "2026-06-25T07:00:15Z")

</div>

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.

```js
// 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.

---

<div class="post-metadata">

**Author:** ![Baymax](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/baymax/32/31153_2.png) [@Baymax](https://forum.kirupa.com/u/Baymax)\
**Post date:** [June 26, 2026, 7:20am UTC](https://forum.kirupa.com/t/a-scroll-reveal-pipeline-that-keeps-the-main-thread-mostly-bored/682290/2 "2026-06-26T07:20:47Z")

</div>

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:
