Critical Rendering Path
9 min read

Critical Rendering Path: Layerize

The Layerize stage converts paint chunks into composited layers (cc::Layer objects), determining how display items should be grouped for independent rasterization and animation. This process happens after Paint produces the paint artifact and before Rasterization converts those layers into GPU textures.

Compositor Thread

Main Thread

Paint

Paint Artifact

Layerize

(PaintArtifactCompositor)

Commit

Copy to Pending Tree

Rasterize

Tiles → Textures

Layerization in the RenderingNG pipeline (M94+): PaintArtifactCompositor converts paint chunks to cc::Layers on the main thread, which are then committed to the compositor thread for rasterization.

Layerization is the merging and grouping phase that converts paint output into compositor-ready structures.

The Core Model:

Paint Chunks → PendingLayers → cc::Layers + cc::PropertyTrees
↑ ↑ ↑
│ │ │
input intermediate output
(per PropertyTreeState) (merged groups) (compositor-ready)

Key decisions:

DecisionRationaleTrade-off
Merge by defaultFewer layers → less GPU memoryMore display items per layer → larger rasterization cost
Separate on compositor-changeable propertiesTransform/opacity animations need isolated layersMore layers for animatable content
Prevent merge on overlapMaintain correct z-orderingForces layer creation for overlapping content
Sparsity toleranceAvoid wasting GPU memory on large empty areasAlgorithm complexity in bounds calculation

Why separate from Paint? CompositeAfterPaint (M94) moved layerization after paint because:

  1. Paint produces immutable output—no circular dependencies
  2. Runtime factors (memory pressure, overlap analysis) inform layer decisions
  3. The main thread completes paint recording before layerization begins

Why not on compositor thread? Layerization examines the paint artifact, which lives on the main thread. The compositor thread receives the finalized layer list and property trees during Commit.


The PaintArtifactCompositor implements layerization via LayerizeGroup(), converting paint chunks into compositor layers through a two-phase process.

Each paint chunk initially becomes a PendingLayer—an intermediate representation that may later merge with others.

Paint Chunk A (transform_id=1, clip_id=1, effect_id=1) → PendingLayer A
Paint Chunk B (transform_id=1, clip_id=1, effect_id=1) → PendingLayer B
Paint Chunk C (transform_id=2, clip_id=1, effect_id=1) → PendingLayer C

What PendingLayer holds:

FieldPurpose
chunks_Paint chunk subset (display items + property tree state)
bounds_Bounding rectangle in property tree state space
property_tree_state_Transform, clip, effect node IDs
compositing_type_Classification (scroll hit test, foreign, scrollbar, overlap, other)
rect_known_to_be_opaque_Region guaranteed fully opaque (for optimization)
hit_test_opaqueness_Whether hit testing can bypass main thread

The algorithm attempts to combine PendingLayers to reduce layer count. Merging succeeds when:

  1. Compatible PropertyTreeState: Adjacent chunks share the same transform, clip, and effect ancestors
  2. No overlap conflicts: The chunk doesn’t interleave with content already assigned to incompatible layers
  3. Within sparsity tolerance: Combined bounds don’t waste excessive GPU memory (kMergeSparsityAreaTolerance)

When PropertyTreeStates differ but merging is still beneficial, the algorithm flattens the difference by emitting paired display items that adjust the chunk’s state to match the target layer.

Certain property nodes carry direct compositing reasons that prevent merging:

ReasonCSS TriggerWhy Separate Layer Required
3D/Perspective transformtransform: rotate3d(), perspectiveGPU-native operation; benefits from isolation
Compositor animationanimation on transform/opacityCompositor updates values without re-raster
Will-change hintwill-change: transformDeveloper signals animation intent
Hardware content<video>, <canvas>, <iframe>External GPU surface or separate process
Composited scrollLarge scrollable contentEnables compositor-driven scroll

Elements with direct compositing reasons become their own layers regardless of merging opportunities.

When a paint chunk overlaps content already assigned to an incompatible layer (different z-order, different PropertyTreeState), it cannot merge with that layer. This overlap testing maintains correct visual ordering.

Example of overlap forcing layer creation:

Layer A: Element with will-change: transform (z-index: 1)
Layer B: ???
Paint Chunk X (z-index: 2, overlaps A)
→ Cannot merge with A (different z-order)
→ Cannot merge with any earlier layer (would violate paint order)
→ Must become new Layer B

This cascade can cause layer explosion: one promoted element forces neighbors to promote, which forces their neighbors, etc. The compositor mitigates this through layer squashing—merging multiple overlapping elements into a single layer when they share the same compositing context.

“In the worst case, this algorithm has an O(n²) running time, where n is the number of PaintChunks.” — Chromium Blink Compositing README

The quadratic worst case occurs when every chunk must be checked against every preceding layer for overlap. Typical pages have far fewer problematic interactions, making practical complexity closer to O(n × average_layer_count).


Once the PendingLayer list is finalized, PaintChunksToCcLayer::Convert() creates actual compositor layers.

cc::Layer TypeContent SourceNotes
PictureLayerPainted content (cc::PaintRecord)Most common; holds display lists for rasterization
SolidColorLayerSingle-color regionsOptimization avoiding raster work
TextureLayerExternal GPU texturesCanvas, WebGL, plugins
SurfaceLayerCross-process contentIframes, out-of-process video
ScrollbarLayerScrollbar renderingSolid-color (mobile) or painted (desktop)

Blink property tree nodes referenced by paint chunks are converted to equivalent cc::PropertyTree nodes:

Blink TransformNode → cc::TransformTree node
Blink ClipNode → cc::ClipTree node
Blink EffectNode → cc::EffectTree node
Blink ScrollNode → cc::ScrollTree node

Each cc::Layer stores node IDs pointing into these trees, not hierarchical transform chains. This enables O(1) property lookups during compositing.

Non-composited nodes (those merged into layers with different PropertyTreeStates) become meta display items within the layer’s paint record, effectively baking the transform/clip/effect into the recorded drawing commands.

During layerization, hit-test opaqueness propagates from paint chunks to layers:

Paint Chunk: hit_test_opaqueness_ = OPAQUE
↓ accumulate
cc::Layer: hit_test_opaqueness = OPAQUE (can be hit-tested on compositor thread)

This enables the compositor to handle hit testing for opaque regions without consulting the main thread—critical for responsive input during JavaScript execution.


Layer decisions directly impact GPU memory consumption and rasterization cost.

Each composited layer consumes GPU memory proportional to its pixel area:

Formula: Width × Height × 4 bytes (RGBA)

Layer SizeMemory
1920×1080 (Full HD)~8 MB
2560×1440 (QHD)~14 MB
3840×2160 (4K)~33 MB

Mobile devices with shared GPU memory (2-4 GB total RAM) exhaust resources quickly. Aggressive layerization can cause:

  • Checkerboarding during scroll
  • Fallback to software rasterization
  • Browser process crashes
Fewer Layers (More Merging)More Layers (Less Merging)
Lower GPU memory usageHigher GPU memory usage
Larger rasterization surface per layerSmaller, isolated rasterization surfaces
Property changes require re-rasterProperty changes = compositor-only update
Single point of failure for complex contentParallelizable rasterization

Example: A scrollable list with 100 items:

  • Merged approach: One PictureLayer containing all items. Scrolling uses compositor offset; content change re-rasters entire layer.
  • Separated approach: 100 PictureLayer objects. Each change re-rasters one layer, but uses 100× the memory.

The algorithm balances these by merging by default but separating content expected to change independently.


PaintArtifactCompositor::Update() is expensive. Chromium implements fast paths for common scenarios:

When display items change but layerization structure remains stable:

UpdateRepaintedLayers():
- Skip LayerizeGroup()
- Update existing cc::Layer display lists in-place
- No layer tree reconstruction

Triggers: Color changes, text updates, background modifications that don’t affect bounds or PropertyTreeState.

Transform or opacity changes that don’t affect layer structure:

DirectCompositorUpdate():
- Skip both layerization and display list update
- Modify cc::PropertyTree nodes directly
- Compositor applies new values during compositing

Triggers: transform or opacity animations on already-promoted layers.

Scroll position changes within a composited scroller:

kRasterInducingScroll:
- Minimal update path
- Adjust scroll offset in property trees
- Rasterize newly-visible tiles only

Layerization occurred before paint:

Style → Layout → Compositing (layerization) → Paint

Problems:

  1. Circular dependency: Paint invalidation needed current layer decisions; layer decisions needed paint output
  2. Heuristic complexity: 22,000+ lines of C++ to guess which elements needed layers
  3. Correctness bugs: Fundamental compositing errors from premature decisions

The codebase relied on DisableCompositingQueryAsserts objects to suppress safety checks—a code smell indicating architectural problems.

The modern pipeline inverts the order:

Style → Layout → Prepaint → Paint → Layerize → Commit → Raster

Benefits:

  • Paint produces immutable output before layerization examines it
  • Layer decisions based on actual content, not predictions
  • Simpler, more correct algorithm
  • Enabled Non-Blocking Commit and off-main-thread compositing improvements

The broader architectural shift from “tree of cc::Layers” (GraphicsLayer in Blink terminology) to a “global display list” architecture:

  • BlinkGenPropertyTrees (M75): Blink generates property trees instead of cc re-generating them
  • Paint Chunks: Introduced as intermediate representation for raster invalidation and layerization
  • CompositeAfterPaint (M94): Final phase completing the architectural transformation

Layers Panel (More Tools → Layers):

  • View compositor layer tree
  • See compositing reasons for each layer
  • Memory consumption per layer
  • Paint counts and slow scroll rects

Performance Panel:

  • “Layerize” events in flame chart (rare; usually batched with “Commit”)
  • Layer count changes over time
  • Correlation with memory pressure
Terminal window
# Enable compositing borders (colored borders around layers)
--show-composited-layer-borders
# Log compositing reasons
--log-compositing-reasons
SymptomLikely CauseInvestigation
High GPU memoryToo many layers or oversized layersLayers panel → sort by memory
Unexpected layersOverlap forcing promotionCheck z-index stacking; look for will-change on ancestors
Slow layerizationMany paint chunks with complex overlapsReduce DOM complexity; use contain: strict
Missing compositor animationsElement not promotedVerify will-change or check for direct compositing reason

Layerization is the decision-making phase that transforms paint output into compositor-ready structures. The algorithm balances GPU memory conservation (merge layers) against animation flexibility (separate layers).

Key takeaways:

  1. PendingLayers are intermediate: Paint chunks first become PendingLayers, then merge into final cc::Layers
  2. Merging is the default: Fewer layers means less GPU memory; separation requires explicit reasons
  3. Direct compositing reasons prevent merging: will-change, 3D transforms, compositor animations need isolated layers
  4. Overlap forces layer creation: Maintaining z-order correctness can cascade into layer explosion
  5. CompositeAfterPaint (M94) moved layerization after paint, eliminating circular dependencies and enabling a cleaner algorithm
  6. Fast paths exist: Repaint-only and direct property updates skip full layerization

For production optimization: avoid unnecessary will-change (causes layer promotion), use contain: strict on independent components (limits overlap cascade), and monitor layer count in DevTools to prevent GPU memory exhaustion.


TermDefinition
Paint ChunkContiguous display items sharing identical PropertyTreeState
PendingLayerIntermediate representation during layerization; may merge with others
cc::LayerFinal compositor layer; input to rasterization
PaintArtifactCompositorClass implementing layerization algorithm
Direct Compositing ReasonProperty requiring dedicated compositor layer (3D transform, will-change, etc.)
Overlap TestingChecking whether paint chunks interleave with existing layers
Layer SquashingMerging multiple overlapping elements into single layer to prevent explosion
PropertyTreeState4-tuple (transform_id, clip_id, effect_id, scroll_id) identifying visual context
CompositeAfterPaint (CAP)Architecture (M94+) where layerization occurs after paint completes
Hit-Test OpaquenessWhether compositor can handle hit testing without main thread
  • Layerization converts paint chunks → PendingLayers → cc::Layers
  • Algorithm merges by default to conserve GPU memory
  • Direct compositing reasons (will-change, 3D transforms, animations) force separate layers
  • Overlap testing maintains correct z-ordering but can cause layer explosion
  • CompositeAfterPaint (M94) eliminated circular dependencies by moving layerization after paint
  • O(n²) worst case complexity; practical performance much better for typical pages
  • Fast paths (repaint-only, direct property updates) skip full layerization when possible
  • GPU memory is the primary constraint: each layer costs width × height × 4 bytes

Read more

  • Previous

    Critical Rendering Path: Commit

    Browser & Runtime Internals / Critical Rendering Path 12 min read

    Commit is the synchronization point where the Main Thread hands over processed frame data to the Compositor Thread. This blocking operation ensures the compositor receives an immutable, consistent snapshot of property trees and display lists—enabling the dual-tree architecture that allows rasterization to proceed independently while the main thread prepares the next frame.

  • Next

    Critical Rendering Path: Rasterization

    Browser & Runtime Internals / Critical Rendering Path 10 min read

    Rasterization is the process where the browser converts recorded display lists into actual pixels—bitmaps for software raster or GPU textures for hardware-accelerated paths. This stage marks the transition from abstract paint commands to concrete visual data. In Chromium, rasterization is managed by the compositor thread and executed by worker threads, ensuring smooth interactions even when the main thread is saturated with JavaScript or layout work.