How Ladybird Browser Implements Web Worker Support: Process-Per-Worker Architecture
Ladybird Browser implements Web Worker support by launching each worker in a dedicated OS process, isolating JavaScript execution from the UI thread while maintaining standard DOM Worker APIs through a robust IPC layer.
Ladybird Browser provides comprehensive Web Worker support—including Dedicated, Shared, and Service Workers—by adhering to the HTML Living Standard's "run a worker" algorithm while enforcing strict process isolation. This architecture ensures that worker scripts execute in separate OS processes, preventing UI blocking and maintaining security boundaries between workers and the main browser context.
Process-Per-Worker Architecture in Ladybird
Ladybird’s Web Worker implementation follows a strict process-per-worker model. Unlike thread-based implementations that share memory space, Ladybird spawns a separate WebWorker executable for each worker instance. This design prevents memory corruption from affecting the main browser process and ensures that long-running scripts cannot freeze the UI.
Launching the WebWorker Process
When JavaScript code invokes new Worker('script.js'), the rendering engine triggers process creation through the helper process system. In [Libraries/LibWebView/HelperProcess.cpp](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWebView/HelperProcess.cpp#L85), the launch_web_worker_process function constructs the command-line arguments and spawns the executable:
// Libraries/LibWebView/HelperProcess.cpp (lines 85-105)
ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(Web::Bindings::AgentType type)
{
Vector<ByteString> arguments;
arguments.append("--type");
switch (type) {
case Web::Bindings::AgentType::DedicatedWorker: arguments.append("dedicated"); break;
case Web::Bindings::AgentType::SharedWorker: arguments.append("shared"); break;
case Web::Bindings::AgentType::ServiceWorker: arguments.append("service"); break;
}
// Socket plumbing establishes IPC connection to browser process
return launch_server_process<Web::HTML::WebWorkerClient>("WebWorker"sv, move(arguments));
}
The function distinguishes between Dedicated, Shared, and Service worker types, passing the appropriate flag to the child process before establishing the IPC socket connection.
IPC Communication Between UI and Worker Processes
Once spawned, the worker process creates a ConnectionFromClient instance to handle IPC commands from the browser. The UI process holds a matching WebWorkerClient object that serves as the communication endpoint.
The IPC layer handles three critical operations:
start_worker– Receives the script URL, credentials, and environment snapshot to initialize executionclose_worker– Signals termination and triggers cleanup- Message port transfer – Serializes
MessagePortobjects forpostMessagecommunication
The WorkerHost Lifecycle Implementation
At the heart of Ladybird’s Web Worker support is the WorkerHost class, which implements the HTML specification’s run a worker algorithm. This class manages the entire lifecycle from realm creation to script execution and termination.
Creating the JavaScript Realm and Global Scope
When ConnectionFromClient receives the start command, it instantiates WorkerHost and invokes WorkerHost::run. This method creates a fresh JavaScript realm and allocates the appropriate global scope object based on the worker type.
In [Services/WebWorker/WorkerHost.cpp](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/WorkerHost.cpp), lines 60-68 handle realm creation:
// Services/WebWorker/WorkerHost.cpp (excerpt)
void WorkerHost::run(GC::Ref<Web::Page> page, /* ... */)
{
// Create a new JavaScript realm with the correct global object
auto realm_execution_context = Web::Bindings::create_a_new_javascript_realm(
Web::Bindings::main_thread_vm(),
[&](JS::Realm& realm) -> JS::Object* {
if (is_shared)
return realm.heap().allocate<Web::HTML::SharedWorkerGlobalScope>(realm, page);
return realm.heap().allocate<Web::HTML::DedicatedWorkerGlobalScope>(realm, page);
},
nullptr);
// ... environment setup continues
}
The lambda function passed to create_a_new_javascript_realm determines whether to instantiate DedicatedWorkerGlobalScope or SharedWorkerGlobalScope, ensuring that the global self object exposes the correct API surface for each worker type.
Setting Up the EnvironmentSettingsObject
After realm creation, WorkerHost constructs a WorkerEnvironmentSettingsObject to maintain the worker's execution context. This object captures a snapshot of the creating document's origin, Content Security Policy (CSP), and other critical settings required by the Fetch algorithm.
The setup occurs in WorkerEnvironmentSettingsObject::setup, which initializes:
- The realm execution context
- The environment settings snapshot (origin, CSP, referrer policy)
- The module map for
importScriptssupport - The global object's bindings
Fetching the Initial Worker Script
With the environment established, WorkerHost initiates the script fetch using the HTML specification's perform the fetch hook. In [Services/WebWorker/WorkerHost.cpp](https://github.com/LadybirdBrowser/ladybird/blob/master/Services/WebWorker/WorkerHost.cpp), lines 34-98 implement this logic:
// Services/WebWorker/WorkerHost.cpp (perform_fetch implementation)
auto perform_fetch = Web::HTML::create_perform_the_fetch_hook(
inside_settings->heap(),
[&](auto request, auto top_level, auto process_response) {
// Use the standard Fetch algorithm
auto fetch_controller = Web::Fetch::Fetching::fetch(
inside_settings->realm(),
request,
Web::Fetch::Infrastructure::FetchAlgorithms::create(
inside_settings->heap(),
{
.process_response = move(process_response),
.process_response_end_of_body = [&](auto response) {
// Update worker URL and environment after fetch completes
inside_settings->set_url(response->url().value_or({}));
}
}));
// CSP checks and environment updates occur here
});
This implementation ensures that worker scripts undergo the same security checks (CSP, CORS, referrer policy) as main-thread fetches, while maintaining the worker's isolated environment.
Worker Global Scopes and APIs
Once the script fetch completes and the realm is initialized, Ladybird exposes the standard Web Worker APIs through specialized global scope classes.
DedicatedWorkerGlobalScope vs SharedWorkerGlobalScope
Ladybird distinguishes between worker types through separate C++ classes that inherit from WorkerGlobalScope:
DedicatedWorkerGlobalScope– Created fornew Worker()instances, exposingpostMessage,close, andimportScriptsmethodsSharedWorkerGlobalScope– Created fornew SharedWorker()instances, additionally exposing theonconnectevent and managing multiple connection ports
Both classes are instantiated in WorkerHost::run based on the is_shared boolean flag passed during process initialization.
Message Passing and Port Management
Communication between the main thread and workers relies on the HTML MessagePort API. When WorkerHost::run executes, it receives a serialized MessagePort from the UI process via the implicit_port argument.
The implementation in WorkerHost.cpp (lines 94-99) attaches this port to the global scope:
// Message port setup in WorkerHost::run
auto implicit_port = Web::HTML::TransferDataDecoder::decode_message_port(
inside_settings->realm(), message_port_data);
// Expose the port to the worker's global object
global_scope->set_implicit_port(implicit_port);
This enables the standard worker.postMessage() and self.onmessage patterns to function across process boundaries, with IPC handling the underlying serialization and transfer semantics.
Key Source Files and Implementation Details
The Web Worker implementation spans multiple libraries and services within the Ladybird codebase:
Summary
Ladybird Browser’s Web Worker implementation delivers spec-compliant worker support through a process-per-worker architecture that prioritizes isolation and stability. The key architectural decisions include:
- OS Process Isolation: Each worker runs in a separate
WebWorkerprocess spawned viaHelperProcess::launch_web_worker_process, ensuring that worker scripts cannot crash or block the main browser UI. - Spec-Compliant Lifecycle: The
WorkerHostclass implements the HTML Living Standard's run a worker algorithm, handling realm creation,EnvironmentSettingsObjectsetup, and initial script fetching through the standard Fetch infrastructure. - Type-Specific Global Scopes: Ladybird distinguishes between
DedicatedWorkerGlobalScopeandSharedWorkerGlobalScopeat allocation time, ensuring that each worker type exposes the correct JavaScript APIs (postMessage,importScripts,onconnect). - Robust IPC Layer: Communication between the UI process and worker processes flows through
WebWorkerClientandConnectionFromClient, withMessagePortobjects handling the standardpostMessageAPI across process boundaries.
Frequently Asked Questions
Does Ladybird use threads or processes for Web Workers?
Ladybird uses separate OS processes rather than threads for Web Workers. When JavaScript code creates a worker via new Worker(), the browser calls HelperProcess::launch_web_worker_process in Libraries/LibWebView/HelperProcess.cpp to spawn a new WebWorker executable. This process-per-worker model provides stronger isolation than thread-based approaches, ensuring that memory corruption or infinite loops in worker code cannot affect the main browser process or other workers.
How does Ladybird handle SharedWorker compared to DedicatedWorker?
Ladybird distinguishes between worker types during global scope allocation in WorkerHost::run. For Dedicated Workers, the code allocates DedicatedWorkerGlobalScope, while Shared Workers receive SharedWorkerGlobalScope. This occurs in Services/WebWorker/WorkerHost.cpp within the lambda passed to Bindings::create_a_new_javascript_realm. The is_shared boolean flag, passed during process initialization via the --type command-line argument, determines which global scope class is instantiated, ensuring that Shared Workers expose the onconnect event while Dedicated Workers use the standard postMessage API.
What security isolation does the process-per-worker model provide?
The process-per-worker architecture provides memory isolation and failure containment at the OS level. Since each worker runs in a separate address space via the WebWorker process, buffer overflows or use-after-free bugs in worker JavaScript cannot corrupt the main browser's heap. Additionally, if a worker enters an infinite loop or triggers an unrecoverable error, the browser process can terminate the specific worker process via ConnectionFromClient::close_worker without affecting other workers or the UI thread. This aligns with the web security model's requirement that workers execute in isolated contexts.
How does script fetching work in Ladybird's WebWorker implementation?
Script fetching follows the HTML Living Standard's perform the fetch hook implemented in WorkerHost::run. The code creates a fetch hook using Web::HTML::create_perform_the_fetch_hook, which invokes the standard Fetch algorithm (Web::Fetch::Fetching::fetch) to retrieve the worker script. This occurs in Services/WebWorker/WorkerHost.cpp around lines 34-98. The fetch operation respects the worker's EnvironmentSettingsObject, applying CSP policies, referrer policies, and CORS checks before executing the script. Once the fetch completes, the response body is evaluated within the worker's isolated JavaScript realm.
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 →