Skip to content

0%

Norbert Brettschneider

JavaScript

JavaScript Performance Optimization: A Practical Guide for Better Web Applications

2026-01-03
4 Min Read
JavaScript Performance Optimization: A Practical Guide for Better Web Applications

JavaScript performance tricks that actually move the needle

Storytime: I once inherited a codebase where a simple list render took over 800ms. The culprit was a loop touching the DOM 200 times instead of once. A quick refactor of only four lines dropped that time to 12ms. I have been a little obsessed with JS performance ever since.

The good news is that you don’t need to understand V8 internals to write fast JavaScript. A handful of patterns cover most of the performance gains you’ll ever need. Let’s look at the ones that matter.

Stop touching the DOM so much

Every time you modify the DOM, the browser recalculates styles, reruns layout, and repaints. Doing this in a loop triggers that entire pipeline on every iteration.

To fix this, batch your changes off-screen first, then commit them once.

// ❌ Triggers a reflow for every single item
const container = document.getElementById("user-list");
users.forEach((user) => {
  const li = document.createElement("li");
  li.textContent = user.name;
  container.appendChild(li); // reflow, reflow, reflow...
});

// ✅ One reflow, no matter how many items
const fragment = document.createDocumentFragment();
users.forEach((user) => {
  const li = document.createElement("li");
  li.textContent = user.name;
  fragment.appendChild(li);
});
container.appendChild(fragment); // single reflow here

The same idea applies to style changes. Instead of setting individual properties, which each trigger their own recalculation, swap a CSS class instead:

// ❌ Multiple style changes = multiple reflows
element.style.width = "200px";
element.style.height = "200px";
element.style.backgroundColor = "blue";

// ✅ One class change = one reflow
element.classList.add("highlighted-box");

Your CSS already lives in a stylesheet. Let the browser do its job.

Write loops that know when to stop

Loops are where I see the most avoidable waste. The classic example is iterating an entire array looking for one item, even after you’ve already found it.

// ❌ Checks all 10,000 users even after finding the one you want
let target;
users.forEach((user) => {
  if (user.id === 42) target = user;
});

// ✅ Stops the moment it finds a match
const target = users.find((user) => user.id === 42);

find(), some(), and findIndex() all short-circuit, meaning they stop iterating the moment the condition is met. Reach for them before writing a manual loop.

For hot loops over large datasets, cache the array length:

const len = items.length;
for (let i = 0; i < len; i++) {
  // avoids re-evaluating items.length on every iteration
}

This may seem like a minor detail, but in tight loops processing thousands of items, it adds up.

Parallel async operations with Promise.all

This optimization is often overlooked. If you have multiple independent async operations, awaiting them sequentially leaves performance on the table.

// ❌ Sequential: 6 seconds total
async function loadPage() {
  const users = await fetchUsers(); // 2s
  const posts = await fetchPosts(); // 2s
  const comments = await fetchComments(); // 2s
}

// ✅ Parallel: 2 seconds total
async function loadPage() {
  const [users, posts, comments] = await Promise.all([fetchUsers(), fetchPosts(), fetchComments()]);
}

The rule is simple: if the requests do not depend on each other’s results, run them together. I check for this pattern in every code review, as it is one of the easiest wins available.

Lazy load what you don’t need yet

Don’t pay the cost of loading something until the user actually needs it. The Intersection Observer API makes this trivial for images:

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll("img[data-src]").forEach((img) => {
  observer.observe(img);
});

Mark your images with data-src instead of src, and they will only load as they scroll into view. For image-heavy pages, this alone can dramatically improve initial load time.

The same principle applies to JavaScript modules. If a feature is only used sometimes, import it dynamically instead of bundling it upfront:

button.addEventListener("click", async () => {
  const { heavyFeature } = await import("./heavyFeature.js");
  heavyFeature.init();
});

Measure first, optimize second

Here is the advice I wish someone had given me earlier: do not guess at bottlenecks, but measure them.

const start = performance.now();
doExpensiveThing();
console.log(`Took ${performance.now() - start}ms`);

Or use console.time for quick checks:

console.time("render");
renderList();
console.timeEnd("render"); // "render: 23.4ms"

Open DevTools, run a Performance trace, and let the flame graph show you where time is actually going. Most of the time it is not where you expected.

The short list

When something feels slow, work through these steps:

  • Batch DOM changes with document fragments
  • Swap CSS classes instead of inline styles
  • Use find() or some() instead of forEach when you need an early exit
  • Run independent async calls with Promise.all
  • Lazy load images and heavy modules
  • Measure before and after, every time

Performance work is satisfying precisely because the feedback is immediate. Slow code becomes fast, measured before and after. Go find your slow renders and optimize them.

Indexed Under:
JavaScript
* NORBERT BRETT * DESIGN & CODE FOR THE WEB * AVAILABLE FOR WORK * DEVELOPING IN BUDAPEST, HU * PORTFOLIO 2026 *
* NORBERT BRETT * DESIGN & CODE FOR THE WEB * AVAILABLE FOR WORK * DEVELOPING IN BUDAPEST, HU * PORTFOLIO 2026 *

© 2026 Norbert Br3tt. All rights reserved.

Designed & Developed in Budapest, HU