Critical Rendering Path
12 min read

Critical Rendering Path: Commit

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.

Active TreePending TreeCompositor Thread(ProxyImpl)Main Thread(ProxyMain)Active TreePending TreeCompositor Thread(ProxyImpl)Main Thread(ProxyMain)Paint CompleteBlocked via MutexFree for Frame N+1Schedule RasterizationTiles ReadyReady for DrawNotifyReadyToCommitCopy Layer Tree,Property Trees,Display ListsRelease MutexRasterize TilesActivate(Pending → Active)
The Commit synchronization: ProxyMain blocks via mutex while ProxyImpl copies data structures to the pending tree. After commit, the main thread can process the next frame while rasterization proceeds on the pending tree.

Commit is the atomic handoff from Main Thread to Compositor Thread—a blocking synchronization that enables Chromium’s dual-tree architecture.

The Core Model:

  • Why blocking? Atomicity. Multiple JavaScript modifications within a single call stack must appear as a unified frame update. Non-blocking IPC (Inter-Process Communication) would risk partial state transfer (a “half-moved” element).
  • What transfers? Four property trees (transform, clip, effect, scroll), layer metadata, and display lists. Each DOM element’s PropertyTreeState—a 4-tuple referencing nodes in each tree—gets synchronized to compositor-side structures.
  • Threading constraint: Main thread can block waiting on compositor thread, but the reverse is forbidden (prevents deadlocks). This asymmetry is enforced via DCHECKs in ProxyMain and ProxyImpl.

The Pipeline Flow:

BeginImplFrame → BeginMainFrame → [Main Thread Work] → ReadyToCommit → Commit → Activate → Draw

Dual-Tree Purpose:

  • Pending Tree: Receives commits; rasterizes new content
  • Active Tree: Currently displayed; handles animations and scroll input while pending tree rasterizes

Activation (pending → active) occurs only when sufficient tiles are rasterized, preventing checkerboard artifacts. A second commit cannot start until the pending tree activates—this back-pressure mechanism bounds pipeline depth.

Performance: Commits typically take 1-3ms. Durations exceeding 5ms consume significant frame budget (16.6ms at 60Hz) and indicate excessive layer count or complex display lists.


The commit is not a simple data copy—it’s a carefully orchestrated synchronization using Chromium’s proxy pattern.

Chromium’s compositor uses two proxy objects to mediate between threads:

ComponentThreadResponsibilities
ProxyMainMain ThreadOwns LayerTreeHost; sends NotifyReadyToCommit; enforces thread-safety via accessor DCHECKs
ProxyImplCompositor ThreadOwns pending/active trees; performs the actual data copy during commit; controls activation timing
SingleThreadProxyBoth (fallback)Used when compositor runs on main thread (non-threaded mode)

The proxy pattern enforces a critical invariant: ProxyImpl only accesses main-thread data structures when the main thread is blocked. This eliminates the need for fine-grained locking on individual layer properties.

  1. Main thread completes paint: Blink finishes style recalculation, layout, and paint recording. The LayerTreeHost holds the new frame’s property trees and display lists.

  2. ProxyMain sends NotifyReadyToCommit: This synchronous message passes a mutex to the compositor thread. The main thread then waits on this mutex—it cannot proceed until the compositor releases it.

  3. Compositor thread schedules commit: The SchedulerStateMachine (which manages pipeline state) determines when to execute ScheduledActionCommit. If rasterization from a previous frame is still in progress, commit may be delayed.

  4. ProxyImpl performs the copy: Layer IDs are used to match main-thread layers to their pending-tree counterparts:

    • Layer 5 on main thread pushes to layer 5 on pending tree
    • pushPropertiesTo() recursively synchronizes property tree nodes
    • Display lists (cc::PaintRecord) are shared (copy-on-write semantics)
  5. Mutex released: ProxyImpl signals completion. The main thread unblocks and can immediately begin processing the next frame’s JavaScript, style, and layout.

The blocking behavior is intentional and addresses several constraints:

Atomicity Guarantee: JavaScript often makes multiple DOM modifications in a single event handler:

element.style.transform = "translateX(100px)"
element.style.opacity = "0.5"
element.style.backgroundColor = "red"

Without atomic commit, the compositor might receive transform and opacity changes but miss the background change—producing a visually inconsistent frame. The blocking commit ensures all modifications within a call stack appear together.

Thread Safety Without Locking: Alternative designs could use per-property locks or lock-free data structures, but these add complexity and overhead. Blocking for a few milliseconds is acceptable because:

  • Commits are fast (typically <3ms)
  • The main thread was already idle (waiting for vsync)
  • Immediate release enables main thread to start frame N+1 while compositor rasterizes frame N

Historical Context: Early Chrome versions attempted non-blocking commits with complex synchronization. The blocking model proved simpler and more predictable, with minimal performance penalty for typical workloads.


The property tree architecture is central to efficient commits. Rather than transferring a monolithic “layer tree” with entangled spatial relationships, Chromium uses four independent trees.

TreeNode TypeWhat It RepresentsExample CSS
TransformTransformNode2D transform matrices, including scroll offsetstransform, translate, scroll containers
ClipClipNodeRectangular clip regionsoverflow: hidden, clip-path
EffectEffectNodeOpacity, filters, blend modes, masksopacity, filter, mix-blend-mode
ScrollScrollNodeScroll behavior and offset synchronizationoverflow: scroll, scroll containers

Each DOM element receives a PropertyTreeState—a 4-tuple of node IDs (transform_id, clip_id, effect_id, scroll_id). This design decouples spatial relationships from the DOM hierarchy.

Before property trees (legacy architecture): The compositor received a “Layer Tree” where each layer encoded its position relative to ancestors. Moving an element required:

  1. Updating the layer’s position
  2. Walking all descendant layers to recalculate their world-space positions
  3. Complexity: O(total layers)O(\text{total layers}) per change

With property trees: Transform changes update only the affected TransformNode. Descendant elements reference this node by ID; their world-space transforms are computed lazily by walking from node to root.

  • Complexity: O(tree depth)O(\text{tree depth}) per change
  • Typical tree depth: 5-10 levels, even on complex pages

Real-world impact: A page with 1,000 promoted layers and a transform animation on a container:

  • Legacy: ~1,000 layer updates per frame
  • Property trees: 1 transform node update, descendants computed on demand

During the Paint stage, display items are grouped into paint chunks based on their PropertyTreeState. Adjacent display items with identical (transform_id, clip_id, effect_id, scroll_id) share a chunk.

During commit:

  1. Blink’s property trees are converted to cc property trees
  2. Paint chunks reference property tree nodes by ID
  3. The compositor can apply transforms/effects without re-rasterizing—just update the node and recomposite

This separation is why transform and opacity animations don’t require re-paint or re-rasterization: only the property tree node value changes.


Commit doesn’t push directly to the “active” tree that’s currently being displayed. Instead, it targets the pending tree—a staging area that allows rasterization to proceed without visual artifacts.

TreePurposeUpdated By
Pending TreeReceives new commits; tiles are rasterized hereCommit
Active TreeCurrently displayed; handles animations, scroll, inputActivation
Recycle TreeCached for allocation reuse (optional)Tree swap

Why two trees? Without this separation, committing a new frame would immediately expose partially-rasterized content. Users would see checkerboard artifacts—empty rectangles where tiles haven’t finished rasterizing.

The dual-tree design ensures:

  1. Commit populates the pending tree
  2. TileManager schedules rasterization for pending tree tiles
  3. Activation proceeds only when viewport tiles are ready
  4. Active tree continues handling scroll/animation during rasterization

Activation (pending → active) is the second critical synchronization point. The SchedulerStateMachine tracks:

  • Which tiles are required for the current viewport (“NOW” priority)
  • Whether rasterization has completed for required tiles
  • Whether a new commit is waiting

Key constraint: A second commit cannot begin while a pending tree exists. If the main thread calls NotifyReadyToCommit before activation:

  1. ProxyMain blocks at the mutex
  2. Compositor completes current rasterization
  3. Activation occurs (pending → active)
  4. Pending tree becomes empty
  5. Commit proceeds to new pending tree
  6. ProxyMain unblocks

This mechanism bounds pipeline depth to two frames: active tree displaying frame N while pending tree rasterizes frame N+1.

The activation requirement creates back-pressure: if rasterization is slow, commits wait. This prevents unbounded queue growth but can cause jank if commit waits exceed frame budget.

Symptoms of back-pressure:

  • Main thread stalls in DevTools flame chart with “Commit” marker
  • Input events appear delayed (INP degradation)
  • PipelineReporter traces show “Waiting for activation”

Common causes:

  • Too many compositor layers (memory pressure)
  • Complex display lists (slow rasterization)
  • GPU driver issues (slow tile upload)

Commit duration directly impacts frame timing. Understanding what contributes to commit cost helps diagnose performance issues.

PhaseTypical CostWhat Increases It
Property tree sync<1msDeep nesting, many effect/clip nodes
Layer metadata copy0.5-2msHigh layer count (>100 layers)
Display list serialization0.5-3msLarge display lists, complex paint chunks
Mutex acquisition<0.1msCompositor busy with previous frame

Typical total: 1-3ms for most pages. Exceeding 5ms indicates a problem.

Chrome DevTools and tracing tools expose commit metrics:

  1. Performance panel: Look for “Commit” blocks in the main thread flame chart
  2. chrome://tracing: Record with “cc” category; PipelineReporter events show commit duration and breakdown
  3. Lighthouse: Excessive commit times manifest as “Long Tasks” and INP (Interaction to Next Paint) issues

What to look for in PipelineReporter:

PipelineReporter::StageType::kBeginImplFrame → ... → kCommit → kActivation → kDraw

The kCommit stage duration shows how long the main thread was blocked.

Commit Jank: Commit takes >5ms, consuming frame budget:

  • Cause: Excessive layer count, complex property trees
  • Symptom: Dropped frames during animations even when JavaScript is fast
  • Fix: Reduce promoted layers, simplify DOM structure

Commit Starvation: Main thread is busy with JavaScript, delaying commit:

  • Cause: Long-running tasks block requestAnimationFrame callback
  • Symptom: Animations skip frames; INP degradation
  • Fix: Break up long tasks; use scheduler.yield()

Activation Blocking: Commit waits for previous frame’s activation:

  • Cause: Slow rasterization (memory pressure, GPU issues)
  • Symptom: Main thread “Commit” blocks extend unexpectedly
  • Fix: Reduce layer count; simplify rasterization workload

PropertyTreeState Explosion: Many unique property tree states create many paint chunks:

  • Cause: Deeply nested transforms, clips, effects
  • Symptom: High “Layer metadata copy” time
  • Fix: Flatten DOM structure; avoid unnecessary isolation: isolate

Understanding how commit evolved clarifies current design decisions.

Before 2017, Blink generated “Layer Trees” that encoded hierarchical relationships directly. The compositor received layers with:

  • Absolute positions (computed from ancestor chain)
  • Explicit parent-child relationships
  • Transform/clip inheritance baked into layer data

Problems:

  1. Moving an element required updating all descendants
  2. Scroll offsets were mixed with transforms, complicating fast-path scroll
  3. Layer tree modifications triggered expensive recursive walks
  4. The boundary between Blink and cc was poorly defined

BlinkGenPropertyTrees restructured the commit boundary:

  • Blink generates property trees with explicit nodes for each transform/clip/effect
  • Elements reference nodes by ID, not by hierarchy
  • cc receives property trees and reconstructs spatial relationships

Benefits:

  • Transform updates: O(tree depth)O(\text{tree depth}) instead of O(total layers)O(\text{total layers})
  • Cleaner Blink/cc interface
  • Enabled CompositeAfterPaint

CompositeAfterPaint moved layer decisions after paint:

Legacy: Style → Layout → Compositing → Paint
Modern: Style → Layout → Prepaint → Paint → Layerize → Commit → Raster

Impact on commit:

  • Commit receives paint artifacts (display lists + paint chunks) rather than pre-layerized content
  • Layer decisions happen in cc based on paint output
  • Eliminated 22,000 lines of heuristic code
  • 3.5% improvement in 99th percentile scroll latency

The modern commit transfers:

  1. Property trees: Four trees converted from Blink structures to cc nodes
  2. Paint artifacts: Display items grouped into paint chunks by PropertyTreeState
  3. Layer metadata: Bounds, compositing reasons, scrollability flags

The cc layer tree is derived from paint chunks during layerization, not committed directly from Blink.


Reducing commit duration improves frame timing and INP.

Each compositor layer adds metadata copy overhead:

/* ❌ Forces layer promotion for every item */
.list-item {
will-change: transform;
}
/* ✅ Promote only during animation */
.list-item.animating {
will-change: transform;
}

Target: Keep promoted layer count under 100 for typical pages.

Deep nesting creates tall property trees:

<!-- ❌ Deep nesting = tall property trees -->
<div style="transform: translate(10px)">
<div style="opacity: 0.9">
<div style="filter: blur(1px)">
<div style="clip-path: circle()">
<!-- content -->
</div>
</div>
</div>
</div>
<!-- ✅ Flattened = shorter property tree paths -->
<div style="transform: translate(10px); opacity: 0.9; filter: blur(1px)">
<!-- content with clip-path applied directly -->
</div>

Properties that create stacking contexts also create effect nodes:

/* ❌ Unnecessary stacking context */
.container {
position: relative;
z-index: 1; /* Creates stacking context */
}
/* ✅ Only when actually needed */
.container {
position: relative;
/* z-index: auto = no stacking context */
}

Content containment hints to the browser that subtree changes don’t affect layout outside:

.widget {
contain: strict; /* layout + paint + size containment */
}

This can reduce property tree propagation during commit.


Commit is the atomic handoff that enables Chromium’s pipelined rendering architecture. The blocking synchronization, while counterintuitive, is the simplest correct solution for ensuring frame consistency without per-property locking.

Key takeaways:

  1. Blocking is intentional: Ensures atomicity; complexity of lock-free alternatives outweighs the few milliseconds of blocking
  2. Property trees enable efficiency: O(tree depth)O(\text{tree depth}) updates instead of O(total layers)O(\text{total layers})
  3. Dual-tree architecture prevents artifacts: Pending tree rasterizes while active tree displays
  4. Back-pressure bounds pipeline depth: Second commit waits for activation, preventing unbounded queue growth
  5. Commit duration impacts INP: Exceeding 5ms consumes significant frame budget

For production optimization: minimize layer count, flatten property tree structure, and use DevTools tracing to identify commit bottlenecks when animations or interactions feel sluggish.


  • Paint Stage: Understanding display lists and paint chunks
  • Rasterization: How tiles are prioritized and rasterized
  • Compositing: How compositor layers are assembled
  • Thread synchronization concepts (mutex, blocking calls)
TermDefinition
cc (Chrome Compositor)Multi-threaded system in Chromium handling animation, input, scrolling, and frame production
ProxyMainMain-thread component that owns LayerTreeHost and initiates commits
ProxyImplCompositor-thread component that owns pending/active trees and performs commit data copy
Property TreesFour specialized trees (transform, clip, effect, scroll) isolating spatial/visual relationships
PropertyTreeState4-tuple (transform_id, clip_id, effect_id, scroll_id) identifying an element’s visual context
Pending TreeStaging tree receiving commits; tiles rasterized here before activation
Active TreeCurrently-displayed tree; handles animations and input while pending tree rasterizes
ActivationProcess of swapping pending tree to active after sufficient rasterization
SchedulerStateMachineComponent managing pipeline state transitions (commit, activate, draw)
INP (Interaction to Next Paint)Core Web Vital measuring responsiveness from user input to visual update
BlinkGenPropertyTreesArchitecture where Blink generates property trees rather than compositor layer hierarchies
CompositeAfterPaintArchitecture (M94+) where layer decisions occur after paint, not before
  • Commit is a blocking synchronization where ProxyMain passes a mutex to ProxyImpl
  • Atomicity ensures multiple JavaScript modifications appear as a unified frame update
  • Property trees (transform, clip, effect, scroll) enable O(tree depth)O(\text{tree depth}) updates
  • Dual-tree architecture (pending/active) prevents checkerboard artifacts during rasterization
  • Activation gates second commit: pipeline depth bounded to two frames
  • Typical commit duration: 1-3ms; exceeding 5ms indicates performance issues
  • BlinkGenPropertyTrees and CompositeAfterPaint dramatically improved commit efficiency
  • Optimize by reducing layer count, flattening property trees, and avoiding unnecessary stacking contexts

Read more

  • Previous

    Critical Rendering Path: Paint Stage

    Browser & Runtime Internals / Critical Rendering Path 11 min read

    The Paint stage records drawing instructions into display lists—it does not produce pixels. Following Prepaint (property tree construction and invalidation), Paint walks the layout tree and generates a sequence of low-level graphics commands stored in Paint Artifacts. These artifacts are later consumed by the Rasterization stage, which executes them to produce actual pixels on the GPU.

  • Next

    Critical Rendering Path: Layerize

    Browser & Runtime Internals / Critical Rendering Path 9 min read

    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.