Skip to main content
After layout calculates the position and size of every element, the paint and composite phases transform the layout tree into pixels on the screen. This involves sophisticated GPU acceleration and multi-threaded rendering.
Modern browsers separate painting and compositing to enable smooth scrolling, animations, and visual effects without recalculating layout.

Layer tree construction

The browser constructs a layer tree to determine which elements should be painted to separate layers for efficient compositing.

Why layers matter

Layers enable performance optimizations:

Isolated repainting

Changes to one layer don’t require repainting other layers, reducing paint work.

GPU acceleration

Layers can be uploaded to the GPU as textures and composited with hardware acceleration.

Transform optimization

CSS transforms on a layer can be applied by the GPU without repainting.

Smooth animations

Animations of certain properties (transform, opacity) can run on the compositor thread.

What creates a layer?

Elements are promoted to their own layer based on several criteria:
CSS properties that explicitly create layers:
.layer {
  /* Any of these creates a new layer */
  will-change: transform;
  transform: translateZ(0);
  transform-style: preserve-3d;
  perspective: 1000px;
}
Creating too many layers increases memory usage and compositor overhead. Only promote elements that genuinely benefit from layering.

Layer tree structure

The layer tree is a hierarchy of compositing layers:
1

Identify compositing reasons

Scan the layout tree to find elements that require their own layer based on CSS properties and content type.
2

Build layer tree

Create a tree of layers where each layer corresponds to one or more elements that will be painted together.
3

Determine layer boundaries

Calculate the size and position of each layer in the layer tree.
4

Assign elements to layers

Map each layout object to the layer it should be painted into.

Paint recording and display lists

Paint recording captures drawing commands without immediately rasterizing them to pixels.

Display lists

A display list is a sequence of drawing commands that can be replayed later:
// Conceptual display list structure
DisplayList {
  DrawRect(x: 0, y: 0, width: 100, height: 50, color: blue)
  DrawText("Hello", x: 10, y: 20, font: Arial, size: 16)
  DrawImage(image_id: 123, x: 50, y: 30, width: 200, height: 150)
  DrawRoundedRect(x: 100, y: 100, width: 80, height: 40, radius: 5)
}
Display lists decouple paint recording from rasterization, enabling optimizations like partial invalidation and threaded rasterization.

Paint recording process

The browser walks the layer tree and records paint commands:
1

Create graphics context

Initialize a recording context for each layer’s display list.
2

Walk the paint tree

Traverse elements assigned to the layer in paint order (back to front).
3

Record drawing commands

For each element, emit paint commands:
  • Background (color, image, gradient)
  • Border
  • Decorations (outline, box-shadow, text-decoration)
  • Content (text, replaced elements, children)
4

Apply effects

Record commands for filters, masks, blend modes, and clipping.
5

Optimize display list

Cull invisible commands, merge adjacent operations, deduplicate shared resources.

Paint invalidation

When elements change, the browser determines which parts need repainting:
The entire layer is marked dirty and must be completely repainted.Triggered by:
  • Major layout changes
  • Layer boundary changes
  • Fundamental style changes
Paint invalidation is complex. Incorrectly invalidating too little causes visual bugs, while invalidating too much wastes performance.

Rasterization strategies

Rasterization converts display lists into pixel buffers (bitmaps).

Software rasterization

CPU-based rasterization using algorithms like Skia:
1

Allocate bitmap

Create a pixel buffer in RAM matching the layer’s size.
2

Rasterize commands

Execute each display list command, writing pixels to the bitmap.
3

Apply antialiasing

Use subpixel sampling to smooth edges and text.
4

Upload to GPU

Transfer the rasterized bitmap to GPU memory as a texture.
Advantages:
  • Highly accurate rendering
  • Supports all drawing operations
  • Works on systems without GPU support
Disadvantages:
  • Slower than GPU rasterization
  • Blocks the main thread if not done asynchronously

GPU rasterization

Direct rasterization using GPU shaders:
1

Translate to GPU commands

Convert display list commands to GPU draw calls and shader programs.
2

Execute on GPU

Run shaders to rasterize directly into GPU textures.
3

Handle unsupported operations

Fall back to software rasterization for complex operations the GPU can’t handle efficiently.
Advantages:
  • Much faster for simple shapes and common operations
  • Reduces CPU load
  • Eliminates bitmap upload step
Disadvantages:
  • May be less accurate for complex paths
  • GPU memory constraints
  • Driver compatibility issues
Modern browsers use hybrid approaches: GPU rasterization for most content with software fallback for complex cases.

Tiling for large layers

Large layers are divided into tiles for efficient rasterization:
1

Divide into tiles

Split the layer into fixed-size tiles (e.g., 256×256 pixels).
2

Prioritize visible tiles

Rasterize tiles in viewport first, then nearby tiles that might scroll into view.
3

Rasterize in parallel

Use multiple threads to rasterize different tiles concurrently.
4

Manage tile cache

Keep rasterized tiles in a cache, evicting least-recently-used tiles when memory is tight.

GPU compositing and texture management

The compositor combines all layers into the final frame.

Compositor thread

Compositing runs on a dedicated thread, independent of the main thread:

Smooth scrolling

Scrolling can update layer positions on the compositor thread without blocking on JavaScript or layout.

Smooth animations

Transform and opacity animations run on the compositor, maintaining 60fps even when the main thread is busy.

Input responsiveness

The compositor can respond to input events faster by not waiting for the main thread.

Reduced jank

Separating compositing from JavaScript execution prevents scripting from causing frame drops.

Compositing algorithm

The compositor draws the final frame:
1

Receive layer updates

Get the layer tree, transforms, and opacity values from the main thread.
2

Build quad list

Create a list of textured quads (rectangles) representing each layer, with position, texture ID, transform, and effects.
3

Sort for rendering

Order quads from back to front based on z-index and stacking contexts.
4

Draw to framebuffer

Use the GPU to draw each quad:
  • Bind the layer’s texture
  • Apply transform matrix
  • Apply opacity and blend mode
  • Draw two triangles forming a rectangle
5

Present to screen

Swap buffers to display the completed frame.

Texture management

Managing GPU textures efficiently is critical for performance:
Minimize texture uploads by:
  • Reusing textures when content doesn’t change
  • Uploading asynchronously when possible
  • Compressing textures for faster transfer
Texture memory can be exhausted quickly with many layers or large images. Monitor GPU memory usage and implement aggressive eviction policies.

Advanced compositing features

Modern compositors support sophisticated effects:
1

Filters and effects

Apply blur, brightness, contrast, and other filters using GPU shaders during compositing.
2

Blend modes

Implement blend modes (multiply, screen, overlay) using custom shader programs.
3

Backdrop filters

Capture the backdrop behind an element, apply filters, and composite the result (e.g., frosted glass effect).
4

3D transforms

Use transformation matrices to rotate, scale, and position layers in 3D space with perspective.

Performance optimization strategies

Minimize layer count

Each layer has memory and processing overhead. Only create layers when necessary for performance.

Use compositor-only properties

Animate transform and opacity instead of properties that trigger layout or paint.

Avoid paint invalidation

Design animations to avoid changing painted content when possible.

Profile with DevTools

Use browser DevTools to identify layers, paint regions, and compositor activity.

Batch DOM changes

Group multiple DOM updates to trigger a single paint/composite cycle.

Debounce expensive operations

Throttle operations that cause excessive layer updates or repaints.
Measurement is key: Use requestAnimationFrame timestamps and Performance API to measure paint and composite timing in production.

Implementation checklist

When building paint and composite systems:
1

Layer tree construction

Implement logic to identify elements requiring layers and build the layer tree hierarchy.
2

Display list recording

Create a display list format and record paint operations for each layer.
3

Rasterization engine

Choose a rasterization strategy (software, GPU, or hybrid) and implement tile-based rasterization.
4

GPU compositing

Build a compositor that combines layer textures using GPU acceleration.
5

Invalidation tracking

Implement efficient invalidation to minimize unnecessary repainting and reuploading.
6

Texture management

Create a texture cache with eviction policies to manage GPU memory.
7

Performance measurement

Instrument each phase (layer tree, paint, raster, composite) to measure timing and identify bottlenecks.
Project checkpoint: Build a complete paint and composite pipeline in your browser engine (C++ or Rust). Implement layer tree construction, paint recording, rasterization (CPU or GPU), and GPU compositing. Render complex websites like Wikipedia with images, CSS animations, and fonts. Measure every phase and optimize for 60fps scrolling and animations.

Build docs developers (and LLMs) love