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.
Abstract
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 inProxyMainandProxyImpl.
The Pipeline Flow:
BeginImplFrame → BeginMainFrame → [Main Thread Work] → ReadyToCommit → Commit → Activate → DrawDual-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 Synchronization Mechanism
The commit is not a simple data copy—it’s a carefully orchestrated synchronization using Chromium’s proxy pattern.
The Proxy Architecture
Chromium’s compositor uses two proxy objects to mediate between threads:
| Component | Thread | Responsibilities |
|---|---|---|
| ProxyMain | Main Thread | Owns LayerTreeHost; sends NotifyReadyToCommit; enforces thread-safety via accessor DCHECKs |
| ProxyImpl | Compositor Thread | Owns pending/active trees; performs the actual data copy during commit; controls activation timing |
| SingleThreadProxy | Both (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.
The Commit Flow in Detail
-
Main thread completes paint: Blink finishes style recalculation, layout, and paint recording. The
LayerTreeHostholds the new frame’s property trees and display lists. -
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. -
Compositor thread schedules commit: The
SchedulerStateMachine(which manages pipeline state) determines when to executeScheduledActionCommit. If rasterization from a previous frame is still in progress, commit may be delayed. -
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)
-
Mutex released: ProxyImpl signals completion. The main thread unblocks and can immediately begin processing the next frame’s JavaScript, style, and layout.
Why Block the Main Thread?
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.
Data Transfer: Property Trees
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.
The Four Property Trees
| Tree | Node Type | What It Represents | Example CSS |
|---|---|---|---|
| Transform | TransformNode | 2D transform matrices, including scroll offsets | transform, translate, scroll containers |
| Clip | ClipNode | Rectangular clip regions | overflow: hidden, clip-path |
| Effect | EffectNode | Opacity, filters, blend modes, masks | opacity, filter, mix-blend-mode |
| Scroll | ScrollNode | Scroll behavior and offset synchronization | overflow: 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.
Why Property Trees Improve Commit
Before property trees (legacy architecture): The compositor received a “Layer Tree” where each layer encoded its position relative to ancestors. Moving an element required:
- Updating the layer’s position
- Walking all descendant layers to recalculate their world-space positions
- Complexity: 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: 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
PropertyTreeState and Paint Chunks
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:
- Blink’s property trees are converted to
ccproperty trees - Paint chunks reference property tree nodes by ID
- 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.
The Dual-Tree Architecture
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.
Pending vs. Active Trees
| Tree | Purpose | Updated By |
|---|---|---|
| Pending Tree | Receives new commits; tiles are rasterized here | Commit |
| Active Tree | Currently displayed; handles animations, scroll, input | Activation |
| Recycle Tree | Cached 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:
- Commit populates the pending tree
TileManagerschedules rasterization for pending tree tiles- Activation proceeds only when viewport tiles are ready
- Active tree continues handling scroll/animation during rasterization
Activation and Blocking
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:
- ProxyMain blocks at the mutex
- Compositor completes current rasterization
- Activation occurs (pending → active)
- Pending tree becomes empty
- Commit proceeds to new pending tree
- ProxyMain unblocks
This mechanism bounds pipeline depth to two frames: active tree displaying frame N while pending tree rasterizes frame N+1.
Back-Pressure and Frame Pacing
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)
PipelineReportertraces show “Waiting for activation”
Common causes:
- Too many compositor layers (memory pressure)
- Complex display lists (slow rasterization)
- GPU driver issues (slow tile upload)
Performance Characteristics
Commit duration directly impacts frame timing. Understanding what contributes to commit cost helps diagnose performance issues.
What Takes Time During Commit
| Phase | Typical Cost | What Increases It |
|---|---|---|
| Property tree sync | <1ms | Deep nesting, many effect/clip nodes |
| Layer metadata copy | 0.5-2ms | High layer count (>100 layers) |
| Display list serialization | 0.5-3ms | Large display lists, complex paint chunks |
| Mutex acquisition | <0.1ms | Compositor busy with previous frame |
Typical total: 1-3ms for most pages. Exceeding 5ms indicates a problem.
Measurement with DevTools
Chrome DevTools and tracing tools expose commit metrics:
- Performance panel: Look for “Commit” blocks in the main thread flame chart
chrome://tracing: Record with “cc” category;PipelineReporterevents show commit duration and breakdown- 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 → kDrawThe kCommit stage duration shows how long the main thread was blocked.
Failure Modes
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
requestAnimationFramecallback - 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
Historical Evolution
Understanding how commit evolved clarifies current design decisions.
Pre-BlinkGenPropertyTrees Era
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:
- Moving an element required updating all descendants
- Scroll offsets were mixed with transforms, complicating fast-path scroll
- Layer tree modifications triggered expensive recursive walks
- The boundary between Blink and
ccwas poorly defined
BlinkGenPropertyTrees (2017-2019)
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
ccreceives property trees and reconstructs spatial relationships
Benefits:
- Transform updates: instead of
- Cleaner Blink/cc interface
- Enabled CompositeAfterPaint
CompositeAfterPaint (2019-2021, shipped M94)
CompositeAfterPaint moved layer decisions after paint:
Legacy: Style → Layout → Compositing → PaintModern: Style → Layout → Prepaint → Paint → Layerize → Commit → RasterImpact on commit:
- Commit receives paint artifacts (display lists + paint chunks) rather than pre-layerized content
- Layer decisions happen in
ccbased on paint output - Eliminated 22,000 lines of heuristic code
- 3.5% improvement in 99th percentile scroll latency
Current Architecture (2024+)
The modern commit transfers:
- Property trees: Four trees converted from Blink structures to
ccnodes - Paint artifacts: Display items grouped into paint chunks by PropertyTreeState
- Layer metadata: Bounds, compositing reasons, scrollability flags
The cc layer tree is derived from paint chunks during layerization, not committed directly from Blink.
Optimization Strategies
Reducing commit duration improves frame timing and INP.
Reduce Layer Count
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.
Simplify Property Tree Structure
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>Avoid Unnecessary Stacking Contexts
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 */}Use contain: strict for Isolated Widgets
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.
Conclusion
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:
- Blocking is intentional: Ensures atomicity; complexity of lock-free alternatives outweighs the few milliseconds of blocking
- Property trees enable efficiency: updates instead of
- Dual-tree architecture prevents artifacts: Pending tree rasterizes while active tree displays
- Back-pressure bounds pipeline depth: Second commit waits for activation, preventing unbounded queue growth
- 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.
Appendix
Prerequisites
- 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)
Terminology
| Term | Definition |
|---|---|
| cc (Chrome Compositor) | Multi-threaded system in Chromium handling animation, input, scrolling, and frame production |
| ProxyMain | Main-thread component that owns LayerTreeHost and initiates commits |
| ProxyImpl | Compositor-thread component that owns pending/active trees and performs commit data copy |
| Property Trees | Four specialized trees (transform, clip, effect, scroll) isolating spatial/visual relationships |
| PropertyTreeState | 4-tuple (transform_id, clip_id, effect_id, scroll_id) identifying an element’s visual context |
| Pending Tree | Staging tree receiving commits; tiles rasterized here before activation |
| Active Tree | Currently-displayed tree; handles animations and input while pending tree rasterizes |
| Activation | Process of swapping pending tree to active after sufficient rasterization |
| SchedulerStateMachine | Component managing pipeline state transitions (commit, activate, draw) |
| INP (Interaction to Next Paint) | Core Web Vital measuring responsiveness from user input to visual update |
| BlinkGenPropertyTrees | Architecture where Blink generates property trees rather than compositor layer hierarchies |
| CompositeAfterPaint | Architecture (M94+) where layer decisions occur after paint, not before |
Summary
- 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 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
References
- Chromium: How cc Works — Definitive guide to Chrome’s compositor architecture, including commit and activation
- Chromium: Life of a Frame — End-to-end frame pipeline including commit timing
- Chromium: RenderingNG Architecture — Property trees, threading model, and process architecture
- Chromium: RenderingNG Data Structures — Property tree structure and PropertyTreeState semantics
- Chromium: BlinkNG — BlinkGenPropertyTrees and CompositeAfterPaint evolution
- Chromium: Compositor Thread Architecture — Threading model and synchronization design
- Chromium: cc README — Compositor implementation overview
- W3C: CSS Will-Change Level 1 — Specification for layer promotion hints
- W3C: CSS Containment Module Level 2 — Containment specification for optimization
- web.dev: Interaction to Next Paint (INP) — Core Web Vital impacted by commit timing
Read more
-
Previous
Critical Rendering Path: Paint Stage
Browser & Runtime Internals / Critical Rendering Path 11 min readThe 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 readThe 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.