Learning Material

Frontend Performance: What Actually Slows Pages Down

Learning Material · Web Performance

Cover: Frontend Performance: What Actually Slows Pages Down

Measure before you optimize

Lighthouse (built into Chrome DevTools) and Core Web Vitals give concrete, measurable numbers — Largest Contentful Paint, Cumulative Layout Shift, Interaction to Next Paint — instead of a vague sense that a page "feels slow." Optimizing without measuring first often means fixing something that wasn't actually the bottleneck.

Render-blocking resources

<!-- Blocks rendering until this CSS loads -->
<link rel="stylesheet" href="styles.css">

<!-- Doesn't block rendering; runs after parsing -->
<script src="app.js" defer></script>

By default, the browser stops rendering the page while it fetches and parses CSS and non-deferred JavaScript in the <head>. The defer attribute (or moving scripts to the end of <body>) lets the page render while scripts load in the background instead of blocking on them.

Images: the single biggest opportunity, usually

An unoptimized, full-resolution photo displayed at thumbnail size is often the largest single asset on a page by far. Serving appropriately-sized images, using a modern format (WebP or AVIF instead of PNG/JPEG where supported), and lazy-loading images below the fold typically produces the biggest measurable improvement for the least engineering effort.

<img src="photo.webp" loading="lazy" width="800" height="600" alt="...">

Setting explicit width/height also prevents layout shift as the image loads — the browser reserves the correct space immediately instead of the page jumping once the image arrives.

Layout thrashing: reading and writing the DOM in the wrong order

// Slow: forces a layout recalculation on every iteration
elements.forEach(el => {
  const height = el.offsetHeight  // read
  el.style.height = height + 10 + 'px'  // write
})

// Fast: batch all reads, then all writes
const heights = elements.map(el => el.offsetHeight)
elements.forEach((el, i) => el.style.height = heights[i] + 10 + 'px')

Reading a layout property (like offsetHeight) right after writing one forces the browser to recalculate layout immediately instead of batching it — repeated in a loop, this can visibly stutter a page. Separating all reads from all writes lets the browser batch the layout work once.

JavaScript bundle size compounds on slow connections. A large JS bundle costs time to download, parse, AND execute — three separate costs, all scaling with size. Code-splitting (loading only what a given page actually needs) and removing unused dependencies often matters more than micro-optimizing the code that's already there.

The 80/20 of frontend performance

In practice, image optimization, reducing render-blocking resources, and trimming JavaScript bundle size cover the large majority of real-world performance wins. Exotic optimizations are worth pursuing only after these fundamentals are handled — they rarely move the needle as much on their own.

Comments