This episode covers CSS performance and optimal rendering: the browser render pipeline, avoiding reflow and layout thrashing, properties that are safe to animate, fast font loading, minification and bundling, and techniques like content-visibility for long pages.

Beautiful but slow CSS is still bad. Episode 22 closes the styling techniques series with CSS performance: how browsers render a page, how CSS slows down or speeds up that process, and what you can do to make a stylesheet production-ready. The browser turns HTML and CSS into pixels through several stages — parse, build CSSOM, calculate layout, paint, and composite. Every property you animate or change triggers different stages, and some are far more expensive than others. Why does it matter? Performance is user experience. Slow pages waste conversions and hurt search rankings. By understanding the render pipeline, you can write CSS that is smooth and fast at the same time.
The render pipeline runs in layers, and the properties you use determine which stage gets triggered:
/* Triggers layout: expensive */
.kotak { width: 50%; top: 20px; }
/* Triggers paint: moderate */ .bayangan { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); }
/* Composite only: cheap */ .gerak { transform: translateX(24px); opacity: 0.5; }Changing width, top, or margin forces the browser to recalculate the layout of the whole subtree — the most expensive. Changing color or shadows triggers paint. transform and opacity only run at the composite stage, so animations stay smooth at 60 frames per second.
.geser {
transition: transform 0.2s ease;
}
.geser:hover {
transform: translateY(-4px);
}For animations, always prefer transform and opacity. If you must change size, consider scale from transform instead of width and height so you don't trigger layout.
Reflow happens when layout is recalculated. Reading then writing dimensions alternately makes it worse, turning into layout thrashing:
// Buruk: baca, tulis, baca, tulis
const items = document.querySelectorAll(".item");
for (const item of items) {
const tinggi = item.offsetHeight;
item.style.height = tinggi + "px";
}The code above reads and writes offsets in each iteration, forcing the browser to recalculate layout repeatedly. Fix it by separating reads from writes:
const items = document.querySelectorAll(".item");
const tinggiSemua = [];
for (const item of items) {
tinggiSemua.push(item.offsetHeight);
}
for (let i = 0; i < items.length; i++) {
items[i].style.height = tinggiSemua[i] + "px";
}Read all the values first, then write all the changes. The browser batches the writes into a single layout pass, avoiding duplicate calculations. Also avoid reading dimensions inside events like scroll and resize.
Large fonts slow down the first render. The font-display property controls their behavior:
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
font-weight: 400;
}
body {
font-family: "Inter", system-ui, sans-serif;
}font-display: swap shows a fallback immediately and swaps it in when the font is ready — text stays readable with no blank blocks. Use the much smaller woff2 format, and request only the character subset you actually need.
p {
font-size: 1rem;
line-height: 1.6;
}
h1,
h2,
h3 {
font-wrap: balance;
}Stable font sizes and line spacing help the browser lay out text once. font-wrap: balance tidies a heading's last line so the layout doesn't jump when fonts swap in.
Production stylesheets should be as small and fast as possible:
/* Kode sumber yang ditulis rapi */
.card {
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
}Minifiers remove spaces and comments and shrink names:
bunx lightningcss --minify style.css -o style.min.css
bunx lightningcss --minify --targets '>= 0.5%' style.css -o style.min.cssLightningCSS, esbuild, and PostCSS minify and flag properties that need vendor prefixes. Use identical media queries to merge rules, and consider splitting CSS files so each page only loads what it needs.
gzip -k style.min.css
brotli -k style.min.cssServe CSS with gzip or brotli compression on the server. Combined with proper cache headers, files that have already been loaded aren't downloaded again on the next visit.
For long pages, content-visibility defers rendering areas outside the screen:
.artikel {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
.diagram {
content-visibility: hidden;
contain-intrinsic-size: 0 0;
}content-visibility: auto makes the browser skip render work for parts that aren't visible yet, then renders them when scrolled to. contain-intrinsic-size gives an estimated size so the scrollbar doesn't jump. This feature dramatically speeds up long content pages.
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 60vh; display: flex; align-items: center; justify-content: center; background-color: #0f172a; color: white; padding-inline: 24px; }
.hero h1 { font-size: clamp(2rem, 5vw, 3.5rem); font-wrap: balance; }
.daftar { display: grid; gap: 16px; padding: 24px; }
.item { content-visibility: auto; contain-intrinsic-size: auto 120px; padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px; }
@media (min-width: 700px) { .daftar { grid-template-columns: repeat(2, 1fr); } }<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Performance CSS</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<section class="hero">
<h1>Halaman yang Cepat</h1>
</section>
<main class="daftar">
<div class="item">Konten pertama</div>
<div class="item">Konten kedua</div>
<div class="item">Konten ketiga</div>
<div class="item">Konten keempat</div>
</main>
</body>
</html>Run npx serve ., then open the Performance panel in DevTools and record a scroll interaction. Compare the times before and after applying content-visibility. Also run bunx lightningcss --minify css/style.css -o css/style.min.css and compare both file sizes.
width, height, and top trigger layout every frame. Use transform and opacity for motion, and reserve layout values for rare changes.
Without font-display, the browser may show invisible text while waiting for the font. Set font-display: swap so text is always readable.
content-visibility without contain-intrinsic-size makes the scrollbar jump when content renders. Always include an estimated size.
Key takeaways:
transform and opacity only trigger composite and are the cheapest to animate.font-display: swap and the woff2 format speed up text loading.content-visibility defers rendering parts outside the screen.
The Learning CSS series is entering its final chapter. In the next, final episode we'll cover creating production-ready, maintenance-friendly stylesheets — assembling a design system, validating accessibility, and establishing a workflow so that everything you've learned in this series comes together in a real, launch-ready project.