EduFlow
Modern CSS Mastery
0%

Section 4 · Motion and Architecture

Rendering Performance

18 min4 min readCode playground

Lesson at a glance

Know which CSS changes trigger layout, paint or only compositing, animate the cheap properties, and use content-visibility and aspect-ratio to keep long pages fast and stable.

  1. 1The rendering pipeline
  2. 2Animate the cheap properties
  3. 3Skip work you can’t see
  4. 4Don’t move things after they appear
  5. 5Where you are now

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Waypoint’s schedule looked finished, and then the team opened it on a three-year-old Android phone at the venue’s busy Wi-Fi. The save toast stuttered, the first scroll felt sticky, and speaker photos pushed the text down as they loaded. Nothing was broken; it was just doing more work than it needed to.

CSS performance comes down to one question: how much of the rendering pipeline does this change force the browser to run?

The rendering pipeline

After your CSS changes something, the browser runs up to four stages to update the screen:

  1. Style: work out which rules apply and compute each element’s values.
  2. Layout: calculate every box’s size and position.
  3. Paint: fill in the pixels: text, colors, borders, shadows, images.
  4. Composite: assemble the painted layers on the GPU, applying transforms and opacity.

The trick is that each change only needs to start at a certain stage. Change width and layout must run, then paint, then compositing. Change background-color and layout can be skipped. Change transform or opacity on an element that has its own layer, and the browser can skip layout and paint, moving existing pixels around on the GPU.

Four pipeline stages left to right: style, layout, paint, composite. A row for width, top and margin runs through all four. A row for color, background and box-shadow skips layout. A row for transform and opacity skips layout and paint. Style Layout Paint Composite width, top all four stages, every frame box-shadow skipped translate skipped
Which stages a property change triggers

For a single click, the difference doesn’t matter. For an animation running 60 times a second, or a style change that touches 500 elements, it decides whether the page feels smooth.

Animate the cheap properties

The save toast was animated like this:

CSS
@keyframes toast-in {
  from { left: -320px; }
  to   { left: 16px; }
}

left is a geometry property: every frame re-runs layout for the toast (and checks whether anything else moved), then repaints. The fix keeps the toast’s position fixed and moves its picture instead:

CSS
.toast {
  position: fixed;
  left: 16px;
  bottom: 16px;
  animation: toast-in 250ms ease-out;
}

@keyframes toast-in {
  from { translate: -336px 0; opacity: 0; }
  to   { translate: 0 0;      opacity: 1; }
}

Same visual result, a fraction of the work. This is why every animation in this course used translate, scale, opacity and friends. The same reasoning applies to transitions: a hover that changes box-shadow repaints on each frame, which is fine for one card and noticeable for a grid of fifty animating at once. Fading in a pre-painted shadow on a pseudo-element with opacity is the cheaper alternative when it matters.

Skip work you can’t see

The schedule renders two days of sessions, but the user sees a screenful at a time. content-visibility: auto lets the browser skip layout and paint for sections that are off-screen:

CSS
.day {
  content-visibility: auto;
  contain-intrinsic-size: auto 1200px;
}

Skipped content has no size, so contain-intrinsic-size gives the browser an estimate to use as a placeholder; with auto, it remembers the real size once the section has been rendered. Without it, sections collapse to zero and the scrollbar jumps as you scroll. Content stays in the DOM and in the accessibility tree, and find-in-page still works. content-visibility: auto has been Baseline since September 2025, when Safari 26 added the auto value; a browser without it simply renders every section, as before.

Don’t move things after they appear

Cumulative Layout Shift, one of the Core Web Vitals, measures content jumping as the page loads. In CSS, the usual culprit is media without reserved space. A speaker photo with width: 100% and no height is 0px tall until the image arrives, then shoves everything down. Reserve the space:

CSS
.speaker img {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
}

width and height attributes on the <img> do the same job, since browsers derive an aspect ratio from them. For web fonts, font-display: swap plus a fallback font with similar metrics keeps text from reflowing when the font loads.

Where you are now

Across twenty lessons you’ve taken Waypoint’s schedule from a stylesheet that fought you to one you can predict: layers and :where() settle conflicts, custom properties expose component APIs, grid, subgrid and container queries handle layout, oklch() and light-dark() handle color, logical properties make it work in Arabic, and motion is kind and cheap. The habit to keep is the one from the first lesson: before you change CSS, predict what the browser will do, then check.

Key takeaways

  • Every visual change goes through some of style, layout, paint and composite; geometry properties trigger layout, visual properties trigger paint, and transform and opacity can skip both.
  • Animate translate, scale, rotate and opacity; animating left, width or margin re-runs layout on every frame.
  • content-visibility: auto with contain-intrinsic-size lets the browser skip rendering off-screen sections of a long page.
  • Reserving space with aspect-ratio (or width and height attributes) prevents layout shifts when images and embeds load.
  • Measure before optimising, with the DevTools Performance panel and paint flashing, and remove will-change once an animation ends.

Further reading

Check your understanding

3 questions

Question 1 of 3

A toast slides in by animating left from -320px to 16px. On a mid-range phone it stutters. What’s the best fix?

Question 2 of 3

Which property change usually causes a repaint but no layout?

Question 3 of 3

The schedule has 60 session cards across two days, and the initial render is slow. You add content-visibility: auto to each day section. What else should you add?

Practice

Code playground

Make the schedule cheap to render

Three performance fixes, all in CSS:

  1. Rewrite the toast-in keyframes so the toast slides in from the left using only compositor-friendly properties (translate or transform, plus opacity if you like). Keep the name toast-in, and make sure the toast ends at left: 16px.
  2. Let the browser skip rendering off-screen .day sections, with an intrinsic size estimate that uses auto and a length.
  3. Make the .photo frame square before any image loads, so it reserves its space.
<section class="day">
  <h2>Day 1</h2>
  <article class="speaker">
    <div class="photo" role="img" aria-label="Ana Ortiz"></div>
    <p>Ana Ortiz</p>
  </article>
</section>
<section class="day">
  <h2>Day 2</h2>
  <p>Twenty-eight sessions…</p>
</section>
<div class="toast" role="status">Saved to your schedule</div>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • toast-in animates only translate/transform and opacity
  • The toast still runs the toast-in animation
  • The toast ends at its normal position
  • Day sections use content-visibility: auto
  • Day sections have an intrinsic size estimate with auto
  • The photo frame is square before the image loads