# Ladybird Browser's Rendering Pipeline for Web Content: Multi-Process Architecture Explained

> Explore Ladybird Browser's rendering pipeline. Learn how its multi-process architecture parses HTML/CSS, builds paint trees, and displays content efficiently.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: deep-dive
- Published: 2026-03-05

---

**Ladybird Browser's rendering pipeline uses a multi-process architecture where the WebContent service parses HTML/CSS, builds a paint tree, and renders bitmaps via a background RenderingThread, which are then composited by the UI process for display.**

The Ladybird Browser engine (available at `LadybirdBrowser/ladybird`) implements a modern web rendering pipeline that separates content processing from UI presentation. This architecture processes HTML and CSS through distinct stages—from document parsing to final bitmap rasterization—using explicit process boundaries and a dedicated background rendering thread. Understanding this pipeline reveals how Ladybird achieves performance isolation between web content execution and interface responsiveness.

## Document Parsing and DOM Construction

The pipeline begins when the **WebContent** process initializes a new page. In [`Services/WebContent/PageHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebContent/PageHost.cpp), the `PageHost::create_page()` method constructs a `PageClient` instance that owns a `Web::Page` object. The page's top-level `TraversableNavigable` parses incoming HTML and builds the **DOM** (Document Object Model) and **CSSOM** (CSS Object Model) trees.

This stage establishes the structural foundation for all subsequent rendering operations. The `PageHost` tracks active page instances and manages the lifecycle of the rendering context, ensuring that navigation requests properly reset or update the document state.

## Style Resolution and Layout Computation

Once the DOM is constructed, [`Libraries/LibWeb/DOM/Document.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/DOM/Document.cpp) triggers style resolution through `StyleInvalidation` and computes the layout tree. The **layout tree** consists of `PaintableBox` hierarchies that represent the visual geometry of each element.

During this phase, the engine calculates positioning, dimensions, and stacking contexts according to CSS rules. The layout computation produces a complete box model representation that serves as the input for the painting stage.

## Paint Tree Construction and Stacking Contexts

Each layout box creates a corresponding `PaintableBox` in the **paint tree**. In [`Libraries/LibWeb/Painting/ViewportPaintable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/Painting/ViewportPaintable.cpp), the viewport paintable (`ViewportPaintable`) builds a stacking-context tree when needed via `build_stacking_context_tree_if_needed()`.

This stage handles complex visual layering, overflow clipping, and paint order determination. The paint tree abstracts the geometric layout into a display-oriented structure optimized for rasterization.

## Rendering Thread Coordination

The **RenderingThread** manages background rasterization to prevent UI blocking. In [`Libraries/LibWeb/HTML/Navigable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/Navigable.cpp), the `ready_to_paint()` method signals the per-page rendering thread that a frame is ready for processing.

When `Navigable::ready_to_paint()` fires, the rendering thread wakes and prepares to receive a **display list**—a serialized representation of drawing commands—and a snapshot of the current scroll state. This decouples the main thread from expensive paint operations.

## Display List Generation

Before rasterization, the pipeline records drawing commands into a display list. The `Navigable::record_display_list_and_scroll_state()` method (found in [`Libraries/LibWeb/HTML/Navigable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/Navigable.cpp)) collects scroll state snapshots and passes the display list to `m_rendering_thread.update_display_list()`.

This intermediate representation allows the rendering thread to process paint operations independently while maintaining synchronization with the main thread's state changes.

## GPU and CPU Rasterization Options

Ladybird supports both CPU and GPU-backed rasterization through Skia integration. In [`Services/WebContent/PageClient.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebContent/PageClient.cpp), the `set_use_skia_painter()` method configures the backend:

- **GPU acceleration**: When `UseSkiaPainter::GPUBackendIfAvailable` is set, the rendering thread creates a `SkiaBackendContext` and paints into a GPU-accelerated `PaintingSurface`
- **CPU fallback**: Default CPU-only surfaces handle rasterization when GPU resources are unavailable

The `RenderingThread` executes the display list against the configured surface, producing the final bitmap representation of the web content.

## Frame Presentation and Surface Swapping

Once rasterization completes, `Navigable::paint_next_frame()` calls `RenderingThread::present_frame(viewport_rect)` to swap the front and back `PaintingSurface` buffers. This double-buffering mechanism prevents tearing and ensures atomic frame updates.

The `present_frame` operation notifies the UI process that a new bitmap is ready via the `did_paint` callback, completing the render-update-present cycle within the WebContent process.

## UI Process Integration and Bitmap Consumption

The **UI process** receives rendered content through [`Libraries/LibWebView/ViewImplementation.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWebView/ViewImplementation.cpp). When `WebContentClient::did_paint` triggers, the implementation stores the bitmap in a `ShareableBitmap` and updates the **front-bitmap** state.

The `on_ready_to_paint` callback signals platform-specific widgets (Qt, AppKit, or Android) to paint the bitmap onto the screen. For example, in [`UI/Qt/WebContentView.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/UI/Qt/WebContentView.cpp), the Qt view draws the shared bitmap using `QPainter::drawImage()`.

## Event Loop and Continuous Rendering

The pipeline maintains continuous frame updates through a paint timer. In [`Services/WebContent/PageClient.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/WebContent/PageClient.cpp), the `m_paint_refresh_timer` (a `Core::Timer`) fires at the configured FPS and queues `HTML::main_thread_event_loop().queue_task_to_update_the_rendering()`.

This creates a feedback loop: the timer triggers `ready_to_paint`, which initiates a new display list recording, rasterization, and presentation cycle.

## Code Examples

### Triggering Paint from the UI Process

When the WebContent process signals that a new bitmap is available, the Qt-based UI view renders it:

```cpp
// UI/Qt/WebContentView.cpp
void WebContentView::paintEvent(QPaintEvent*)
{
    // Obtain the shared bitmap from ViewImplementation
    QImage q_image = shared_bitmap_to_q_image();
    QPainter painter(this);
    painter.drawImage(QPoint(0, 0), q_image);
}

```

### Signaling Ready-to-Paint Across Processes

The UI process notifies WebContent when it is prepared to display the next frame:

```cpp
// Libraries/LibWebView/ViewImplementation.cpp
void ViewImplementation::server_did_paint(Badge<WebContentClient>, i32 bitmap_id)
{
    if (on_ready_to_paint)
        on_ready_to_paint();  // UI signals "ready_to_paint"
    
    client().async_ready_to_paint(page_id());  // IPC to WebContent
}

```

### Rendering Thread Frame Processing

The background thread receives paint requests and manages task queuing:

```cpp
// Libraries/LibWeb/HTML/RenderingThread.cpp
void RenderingThread::ready_to_paint()
{
    m_thread_data->decrement_queued_tasks();  // Tells thread a frame can render
    // Thread proceeds to execute display list
}

```

### Display List Recording

The main thread records drawing commands before handing off to the rendering thread:

```cpp
// Libraries/LibWeb/HTML/Navigable.cpp
void Navigable::record_display_list_and_scroll_state(PaintConfig paint_config)
{
    auto display_list = create_display_list();
    // Collect scroll state...
    m_rendering_thread.update_display_list(*display_list, move(scroll_state));
}

```

## Summary

- **Ladybird's rendering pipeline** separates web content processing (WebContent process) from UI presentation (UI process) using explicit IPC boundaries.
- The pipeline flows through **eight distinct stages**: document parsing, style/layout computation, paint tree construction, rendering thread queueing, display list generation, Skia-backed rasterization, frame presentation, and UI bitmap consumption.
- **Background threading** via `RenderingThread` prevents main thread blocking during expensive rasterization operations.
- **Display lists** serve as the intermediate representation between layout and painting, enabling efficient GPU or CPU backend selection.
- **Double-buffered `PaintingSurface` objects** ensure tear-free frame presentation via atomic front/back buffer swaps.
- Key implementation files include [`PageHost.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/PageHost.cpp) for process orchestration, [`Navigable.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Navigable.cpp) for rendering coordination, and [`ViewImplementation.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/ViewImplementation.cpp) for cross-process bitmap sharing.

## Frequently Asked Questions

### How does Ladybird's rendering pipeline differ from Chromium or WebKit?

Ladybird implements a similar **render-update-present** model to Blink/WebKit but uses a lightweight, Rust-inspired C++ codebase with explicit multi-process boundaries. Unlike Chromium's complex compositor thread architecture, Ladybird uses a simpler `RenderingThread` per page for background rasterization, with display lists serving as the primary abstraction between layout and paint operations.

### What is the role of the `RenderingThread` in Ladybird's architecture?

The `RenderingThread` (defined in [`Libraries/LibWeb/HTML/RenderingThread.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibWeb/HTML/RenderingThread.cpp)) executes rasterization on a background thread to keep the main thread responsive. It receives display lists and scroll state snapshots via `update_display_list()`, executes the painting commands against a `PaintingSurface` (CPU or Skia GPU), and signals completion through `present_frame()` to swap buffers and notify the UI process.

### Does Ladybird support GPU acceleration for web rendering?

Yes. When configured via `PageClient::set_use_skia_painter(UseSkiaPainter::GPUBackendIfAvailable)`, Ladybird creates a `SkiaBackendContext` and renders into GPU-accelerated surfaces. If GPU resources are unavailable or disabled, the engine falls back to CPU-only rasterization using the same display list infrastructure.

### How does the UI process receive rendered content from WebContent?

The UI process receives bitmaps through shared memory mechanisms. When the `RenderingThread` completes `present_frame()`, it triggers `WebContentClient::did_paint()` in the UI process. `ViewImplementation` stores this in a `ShareableBitmap`, updates the front-bitmap state, and invokes `on_ready_to_paint()` to trigger platform-specific painting (Qt, AppKit, etc.) onto the screen.