Ladybird Browser's Rendering Pipeline for Web Content: Multi-Process Architecture Explained
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, 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 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, 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, 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) 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, the set_use_skia_painter() method configures the backend:
- GPU acceleration: When
UseSkiaPainter::GPUBackendIfAvailableis set, the rendering thread creates aSkiaBackendContextand paints into a GPU-acceleratedPaintingSurface - 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. 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, 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, 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:
// 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:
// 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:
// 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:
// 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
RenderingThreadprevents 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
PaintingSurfaceobjects ensure tear-free frame presentation via atomic front/back buffer swaps. - Key implementation files include
PageHost.cppfor process orchestration,Navigable.cppfor rendering coordination, andViewImplementation.cppfor 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) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →