Great performance starts with understanding how the browser paints the screen. This lesson covers the rendering pipeline and lazy-loading patterns. “Reflow and lazy loading” are the core topics; scheduling work with requestAnimationFrame is optional depth once you need it.
Key terms
- Reflow: The expensive stage where layout gets recalculated when element size or position changes.
- Repaint: The stage that recolors pixels without changing layout.
- DocumentFragment: A lightweight container used to batch DOM nodes before attaching them to the document, reducing reflows.
- Lazy loading: Strategy that defers network requests until an element actually enters the viewport.
- requestAnimationFrame: Runs code right before the next paint so you can group measurement or animation work per frame.
Core ideas
- Reflow (layout recalculation): Triggered when DOM size or position changes; it is costly, so minimize it.
- Repaint (color-only update): Layout stays the same, but color/shadow changes force repainting.
- Layout thrashing (layout abuse): Alternating reads and writes to layout, causing repeated recalculations.
- Lazy loading (load when needed): Fetch images or components only when they become visible using
loading="lazy"orIntersectionObserver.
Code examples
Start by batching DOM updates with DocumentFragment. Before that, quickly feel how naive DOM manipulation can get expensive.
const list = document.querySelector("ul");
for (let i = 0; i < 3; i += 1) {
list?.append(`Item ${i}`);
}
➡️ If repeating “append → render → append → render” felt clumsy, you already feel why DocumentFragment exists.
const list = document.querySelector(".card-list");
function batchUpdate(cards) {
const fragment = document.createDocumentFragment();
cards.forEach((card) => {
const li = document.createElement("li");
li.className = "card";
li.innerHTML = `<h3>${card.title}</h3><p>${card.summary}</p>`;
fragment.appendChild(li);
});
list?.appendChild(fragment);
}
By staging nodes in a fragment, you cut down reflow counts because the document only updates once.
Next, time your layout work with performance.now() so cost is visible.
function measureLayout(fn) {
const start = performance.now();
fn();
const end = performance.now();
console.log(`Layout work: ${(end - start).toFixed(2)}ms`);
}
measureLayout(() => batchUpdate(largeCardList));
performance.now() gives a precise duration for layout-heavy tasks.
Images get faster when you combine lazy-loading attributes with the observer API.
const heroImg = document.querySelector(".hero img");
heroImg?.setAttribute("loading", "lazy");
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const target = entry.target;
if (!(target instanceof HTMLImageElement)) return;
const src = target.dataset.src;
if (src) {
target.src = src;
target.addEventListener("load", () => target.classList.add("is-loaded"));
obs.unobserve(target);
}
});
});
document.querySelectorAll("img[data-src]").forEach((img) => observer.observe(img));
IntersectionObserver swaps data-src for src exactly when the element becomes visible, shrinking the initial payload.
When you must read layout values, schedule them inside requestAnimationFrame. Treat this as optional depth for projects that need it.
let scheduled = false;
function scheduleMeasure() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
const height = list?.getBoundingClientRect().height;
console.log("List height", height);
scheduled = false;
});
}
window.addEventListener("resize", scheduleMeasure);
Running layout reads inside requestAnimationFrame caps reflow to once per frame.
Why It Matters
- Avoiding layout thrashing keeps dashboards and long lists from dropping frames.
- Lazy loading is a must-have optimization for image-heavy landing pages.
- Understanding the render pipeline makes DevTools Performance traces readable.
Practice
- Follow along: Render a card list with
DocumentFragmentand measure runtime against a naive version. - Extend: Apply
IntersectionObserverlazy loading to both images and a stats card section. - Debug: Intentionally trigger layout thrashing and watch layout counts spike in Performance recordings.
- Done: Initial network requests drop after lazy loading, and layout logs appear at most once per frame.
Wrap-Up
Master these basics to keep dashboards and long feeds stable. Next we will look at DOM testing and debugging routines.
💬 댓글
이 글에 대한 의견을 남겨주세요