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:
Explicit promotion
Visual effects
Overflow and clipping
Special elements
CSS properties that explicitly create layers: Properties requiring special rendering: Elements with scrolling or clipping: Certain element types:
<video> and <canvas> elements
Elements with CSS 3D transforms
Elements with accelerated CSS animations
Root element
Fixed and sticky positioned elements (sometimes)
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:
Identify compositing reasons
Scan the layout tree to find elements that require their own layer based on CSS properties and content type.
Build layer tree
Create a tree of layers where each layer corresponds to one or more elements that will be painted together.
Determine layer boundaries
Calculate the size and position of each layer in the layer tree.
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:
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:
Create graphics context
Initialize a recording context for each layer’s display list.
Walk the paint tree
Traverse elements assigned to the layer in paint order (back to front).
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)
Apply effects
Record commands for filters, masks, blend modes, and clipping.
Optimize display list
Cull invisible commands, merge adjacent operations, deduplicate shared resources.
Paint invalidation
When elements change, the browser determines which parts need repainting:
Full repaint
Partial repaint
No repaint
The entire layer is marked dirty and must be completely repainted. Triggered by:
Major layout changes
Layer boundary changes
Fundamental style changes
Only a region of the layer is marked dirty. Triggered by:
Local style changes (color, background)
Cursor blinking
Small content updates
Optimization: Only replay display list commands intersecting the dirty region. Some changes don’t require repainting:
Transform changes on composited layers
Opacity changes on composited layers
Scrolling within a scrollable layer
These can be handled purely by the compositor.
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:
Allocate bitmap
Create a pixel buffer in RAM matching the layer’s size.
Rasterize commands
Execute each display list command, writing pixels to the bitmap.
Apply antialiasing
Use subpixel sampling to smooth edges and text.
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:
Translate to GPU commands
Convert display list commands to GPU draw calls and shader programs.
Execute on GPU
Run shaders to rasterize directly into GPU textures.
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:
Divide into tiles
Split the layer into fixed-size tiles (e.g., 256×256 pixels).
Prioritize visible tiles
Rasterize tiles in viewport first, then nearby tiles that might scroll into view.
Rasterize in parallel
Use multiple threads to rasterize different tiles concurrently.
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:
Receive layer updates
Get the layer tree, transforms, and opacity values from the main thread.
Build quad list
Create a list of textured quads (rectangles) representing each layer, with position, texture ID, transform, and effects.
Sort for rendering
Order quads from back to front based on z-index and stacking contexts.
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
Present to screen
Swap buffers to display the completed frame.
Texture management
Managing GPU textures efficiently is critical for performance:
Upload strategy
Memory management
Atlas packing
Minimize texture uploads by:
Reusing textures when content doesn’t change
Uploading asynchronously when possible
Compressing textures for faster transfer
GPU memory is limited:
Evict textures for offscreen or distant layers
Use lower resolution textures for scaled-down layers
Share textures between similar layers when possible
Combine small textures into texture atlases:
Reduces GPU state changes
Improves memory locality
Minimizes texture binds during rendering
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:
Filters and effects
Apply blur, brightness, contrast, and other filters using GPU shaders during compositing.
Blend modes
Implement blend modes (multiply, screen, overlay) using custom shader programs.
Backdrop filters
Capture the backdrop behind an element, apply filters, and composite the result (e.g., frosted glass effect).
3D transforms
Use transformation matrices to rotate, scale, and position layers in 3D space with perspective.
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:
Layer tree construction
Implement logic to identify elements requiring layers and build the layer tree hierarchy.
Display list recording
Create a display list format and record paint operations for each layer.
Rasterization engine
Choose a rasterization strategy (software, GPU, or hybrid) and implement tile-based rasterization.
GPU compositing
Build a compositor that combines layer textures using GPU acceleration.
Invalidation tracking
Implement efficient invalidation to minimize unnecessary repainting and reuploading.
Texture management
Create a texture cache with eviction policies to manage GPU memory.
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.