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