Performance

CSS performance optimization is the practice of writing styles that load and render faster — sizing media to prevent layout shift, animating GPU-friendly properties like transform and opacity , and avoiding render-blocking resources — so pages paint quickly and feel smooth.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end of this lesson you'll be able to make a page load faster and feel smoother using nothing but HTML and CSS — sizing images so the layout never jumps, animating on the GPU instead of the layout engine, skipping work on off-screen content, and loading fonts and styles without blocking the first paint.

The browser draws your page like a theatre crew sets a stage. First the crew measures the floor and marks where every prop goes — that is layout . Then they paint the scenery — that is paint . Finally they slide finished flats around under the lights — that is composite .

Sliding a finished flat (a transform ) is cheap — the crew just moves something already built. But changing a prop's actual size mid-show (animating width ) forces them to re-measure the whole floor and repaint on every single frame, and the show stutters. Performance work is mostly about staying in the cheap "slide the flats" stage — and not making the crew build things the audience cannot even see yet.

Before you optimise anything, you need to know what "fast" is measured by. Google's Core Web Vitals are three real-user metrics, and a surprising amount of each is controlled by plain HTML and CSS:

The demo below is a live "scorecard" so you can see what the targets feel like. The numbers are illustrative — the point is to learn the three names and their thresholds, because every fix later in this lesson maps back to one of them.

Images are usually the heaviest thing on a page, so they drive both LCP (the hero image is often the largest element) and CLS (an unsized image shoves the layout when it arrives). Four HTML/CSS habits fix most of it:

Read the worked example. The first image is done right; the comments explain why each attribute is there and what would break without it.

To draw a frame, the browser may run up to four steps: Style (which rules apply) → Layout (sizes and positions) → Paint (fill in pixels) → Composite (slide finished layers around). At 60fps you have about 16ms per frame . The trick is to trigger as few of those steps as possible.

transform and opacity are special: they only touch Composite , the cheapest step, and run on the GPU. Animating width , height , top , or left forces a full Layout recalculation every frame — that is what makes animations stutter. will-change: transform is a hint that lets the browser promote an element to its own GPU layer in advance — use it sparingly, only on things that actually animate.

On a long page, the browser still does layout and paint work for sections far below the fold that nobody has scrolled to yet. content-visibility: auto tells it to skip rendering an element until it is near the viewport — a huge win for long feeds, articles, and lists.

The catch: a skipped element has no size, so it can cause its own layout shift when it pops in. Pair it with contain-intrinsic-size to give the browser an estimated height to reserve, so the scrollbar stays stable.

A link rel="stylesheet" in the head is render-blocking : the browser refuses to paint anything until that file is downloaded and parsed, which delays LCP. Two habits cut that delay: inline the critical CSS (the few rules the above-the-fold view needs) directly in a style tag, and defer the rest with rel="preload" . Avoid @import , which loads stylesheets one-by-one instead of in parallel.

Fonts have the same problem. By default a downloading web font hides its text (a flash of invisible text). font-display: swap shows a system fallback immediately and swaps in your font when it arrives, so the reader is never staring at blank space.

This image has no reserved space, so when it loads it shoves the paragraph below it (bad CLS). Give the image explicit dimensions and lazy-load it. Fill in the blanks marked ___ , then check the expected result in the comments.

This card slides up on hover by animating top — which forces a layout recalculation every frame and stutters. Rewrite it to move with transform instead, so it runs on the compositor. Fill in the blanks and verify the expected behaviour in the comments.

Support is faded now — only an outline is given. Build a small hero section that applies the lesson's habits all at once. Use the worked examples in sections 2, 3, and 5 as your reference if you get stuck.

You can now make a page measurably faster with HTML and CSS alone. The essentials:

Next up: Progressive Enhancement , where you'll build pages that work everywhere and get better where the browser supports more.

Practice quiz

What does the Core Web Vital LCP measure?

  • How fast the page reacts to a click
  • How much the page jumps as it loads
  • Time until the largest element appears
  • Total page weight

Answer: Time until the largest element appears. LCP (Largest Contentful Paint) is the time until the biggest element appears; aim for under 2.5s.

Which Core Web Vital measures how much the page jumps around while loading?

  • CLS
  • LCP
  • INP
  • TTFB

Answer: CLS. CLS (Cumulative Layout Shift) measures unexpected movement; a good score is under 0.1.

Which single habit most reliably prevents layout shift (CLS) from images?

  • Using loading="lazy"
  • Using a WebP format
  • Adding alt text
  • Setting explicit width and height attributes

Answer: Setting explicit width and height attributes. Explicit width and height (or an aspect-ratio) let the browser reserve the right-sized box before the image loads.

Which two properties animate on the compositor (GPU) and skip layout and paint?

  • width and height
  • transform and opacity
  • top and left
  • margin and padding

Answer: transform and opacity. transform and opacity run on the compositor, so they stay smooth; animating width/top forces layout every frame.

Where should you NOT put loading="lazy"?

  • On the above-the-fold hero / LCP image
  • On footer images
  • On below-the-fold gallery images
  • On off-screen avatars

Answer: On the above-the-fold hero / LCP image. Lazy-loading the hero delays your LCP element; keep above-the-fold images eager and lazy-load only below the fold.

What does do on a long page?

  • Hides the element permanently
  • Compresses images
  • Skips rendering an element until it is near the viewport
  • Disables animations

Answer: Skips rendering an element until it is near the viewport. content-visibility: auto defers layout and paint for off-screen sections until they near the viewport.

Which property pairs with content-visibility to reserve a skipped block's height?

  • min-height
  • contain-intrinsic-size
  • aspect-ratio
  • will-change

Answer: contain-intrinsic-size. contain-intrinsic-size gives the browser an estimated size so the scrollbar stays stable when blocks render in.

What is the main problem with a render-blocking <link rel="stylesheet"> in the <head>?

  • It breaks the layout
  • It disables JavaScript
  • It prevents caching
  • The browser will not paint until that CSS downloads and parses

Answer: The browser will not paint until that CSS downloads and parses. Render-blocking CSS stops the first paint until it downloads and parses, delaying LCP; inline critical CSS and defer the rest.

What does do?

  • Hides text until the web font loads
  • Shows fallback text immediately, then swaps to the web font
  • Prevents the font from loading
  • Compresses the font file

Answer: Shows fallback text immediately, then swaps to the web font. font-display: swap shows a system fallback right away and swaps in the web font on arrival, avoiding invisible text (FOIT).

Why should you avoid CSS for loading stylesheets?

  • It is invalid CSS
  • It only works in old browsers
  • It loads stylesheets sequentially, blocking the first paint
  • It disables media queries

Answer: It loads stylesheets sequentially, blocking the first paint. @import loads stylesheets one after another instead of in parallel, blocking rendering; use parallel <link> tags instead.