2HAASStart a project
All posts

Tech Tips

JavaScript performance: keep the main thread free

A page can load fast and still feel slow. Batch DOM updates, debounce noisy events and break up long tasks to keep the interface responsive.

The 2HAAS team2 min read

Loading speed gets most of the attention, but users also judge a page by what happens after it loads. They tap a filter and nothing moves for half a second. The cause is nearly always the same: JavaScript is holding the main thread.

The browser uses one thread for your scripts, for layout, for painting and for handling input. While a script runs, a tap has to wait. Any task longer than 50 ms counts as a long task, and Interaction to Next Paint (INP), one of the Core Web Vitals, measures the delay users feel. Under 200 ms is good.

Touch the DOM less

Reading a layout value such as offsetWidth right after changing a style forces the browser to recalculate layout immediately. Inside a loop, that happens on every pass.

// Slow: layout is recalculated on every iteration
for (const item of items) {
  item.style.width = container.offsetWidth / 2 + "px";
}

// Fast: read once, then write
const half = container.offsetWidth / 2 + "px";
for (const item of items) {
  item.style.width = half;
}

Read first, write second. Keep references to elements you use repeatedly instead of querying for them again, and update only the part of the page that changed.

Debounce noisy events

input, scroll and resize can fire dozens of times a second. Running a search or a filter on each one is wasted work.

function debounce(fn, wait = 200) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

searchBox.addEventListener("input", debounce(runSearch));

Break up long tasks

When a loop has to process thousands of items, give the browser a chance to handle input in between.

async function processAll(items) {
  for (const [i, item] of items.entries()) {
    process(item);
    if (i % 200 === 0) await new Promise((resolve) => setTimeout(resolve));
  }
}

The total work is the same. The page stays responsive while it happens.

Render only what is on screen

A list of five thousand product rows does not need five thousand DOM nodes. Paginate, or use a virtual list that renders only the visible rows. For long static pages, the CSS property content-visibility: auto lets the browser skip rendering sections that are off screen.

Move heavy work off the main thread

Parsing a large file, resizing an image or crunching numbers does not have to block the interface at all. Web Workers run that code on a separate thread.

Find the real culprit

Do not guess. Record the slow interaction in the Performance panel and look for the long task behind it. Our guide to finding bottlenecks with Chrome DevTools walks through it.

Keep reading

  1. Guides2 min read

    How to reduce JavaScript bundle size

    Every kilobyte of JavaScript is downloaded, parsed and run before a page responds. Measure the bundle, split it by route, lazy load the rest and trim dependencies.

  2. Tech Tips2 min read

    Client-side caching for API responses

    Refetching the same data on every visit wastes time and bandwidth. HTTP cache headers, an in-memory cache and browser storage each solve a different part of the problem.

  3. Code Snippets2 min read

    Web Workers: move heavy work off the main thread

    Heavy JavaScript freezes the page because it shares a thread with the interface. A Web Worker runs that work in the background. A working example and the limits to know.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? [email protected]

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.