This Month in Ladybird - April 2026

Ladybird saw significant progress in April 2026, featuring major JavaScript engine optimizations, a new GTK4 frontend for Linux, and the implementation of speculative HTML parsing.
Hello friends! In April we merged 333 PRs from 35 contributors, 7 of whom made their first-ever commit to Ladybird! Here’s what we’ve been up to.
Welcoming new sponsors
Ladybird is entirely funded by the generous support of companies and individuals who believe in the open web. This month, we’re excited to welcome the following new sponsors:
- Human Rights Foundation (via the “AI for Individual Rights” program) with $50,000
- Jakub Stęplowski with $1,000
We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.
Inline PDF viewer
PDFs now render inline through the bundled pdf.js viewer (#9132). pdf.js is a full-featured PDF viewer written entirely in JavaScript, HTML, and CSS, with page navigation, text selection, zoom, and find-in-document. Profiling pdf.js loading the Intel ISA Manual also drove improvements to our typed-array view cache and :has() invalidation.
Browsing history and rich address bar autocomplete
Type in the address bar and you now get rich, history-aware suggestions: previously visited pages with favicons and titles, a search-engine shortcut, and plain URL completions (#8933). Behind the scenes, a SQLite-backed HistoryStore persists every navigation along with its title, favicon, visit count, and last-visit time, and “Clear browsing history” is wired up in the Privacy settings page. Both the Qt and AppKit UIs render the new rich rows.
Speculative and incremental HTML parsing
The HTML parser now consumes the response body incrementally (#9151). Bytes flow through a streaming text decoder into the tokenizer one chunk at a time, the tokenizer pauses when it runs out of input, and resumes when more arrives. This replaces a model where we waited for the full body before starting to parse.
We also implemented the speculative HTML parser (#9114). When the main parser blocks on a synchronous external script, a separate tokenizer scans ahead through the unparsed input and issues speculative fetches for the resources it finds: <script src>, <link rel=stylesheet|preload>, and <img src>. It tracks <base href> and skips into templates and foreign content correctly. A follow-up wired the speculative parser into the document’s preload map (#9164), so resources discovered speculatively get deduplicated against the regular parser’s later fetches instead of being requested twice.
Off-thread JavaScript compilation
Bytecode generation for fetched scripts’ top-level code now runs on a background thread pool (#9118). Worker threads produce the bytecode and the data needed to build an Executable, while everything that touches the VM or GC heap stays on the main thread. This covers classic scripts, modules, and top-level IIFEs, and shifts roughly 200ms of main thread time onto background threads while loading YouTube alone.
Per-Navigable rasterization
Each Navigable now rasterizes independently on its own thread (#8793). Previously, iframes were painted synchronously as nested display lists inside their parent’s display list, which meant only the top-level traversable’s rendering thread was ever active. The parent’s display list now references each iframe’s rasterized output through an ExternalContentSource, so iframe invalidations no longer require re-recording the parent. Beyond the parallelism, this is prep work for moving iframes into separate sandboxed processes.
JavaScript engine
With the C++/Rust transition behind us, we spent April cashing in.
Faster JS-to-JS calls. A multi-part series (#8891, #8909, #8912) made Call, Return, and End instructions stay entirely in the AsmInt assembly interpreter for the common case, with hand-tuned ARM64 paired load/store (ldp/stp) for register save/restore. Native function calls also dispatch directly from AsmInt now, via a new RawNativeFunction variant that holds a plain function pointer instead of an AK::Function (#8922).
O(1) bytecode register allocator. Generator::allocate_register used to scan the free pool to find the lowest-numbered register. We were spending ~800ms in this function alone while loading x.com. With the C++/Rust pipeline parity period over, the allocator is now a plain LIFO stack (#9007).
Cached for-in iteration. for (key in obj) sites now cache the flattened enumerable key snapshot and reuse it as long as the receiver’s shape, indexed storage, and prototype chain still match (#8856). Speedometer 2 went from 67.7 to 73.6, and Speedometer 3 from 4.11 to 4.22!
A grab-bag of other improvements:
- The parser uses zero-copy identifier name sharing across the lexer, parser, and scope collector. On a corpus of website JS, parsing is 1.14x faster and uses 282 MB less RSS. (#8801)
- Short string concatenations skip the rope representation when the result is going to be observed as a flat string anyway. 2.13x speedup on a tight
a + bloop. (#9184) - Lexical-this arrow functions no longer allocate a function environment per call. Another 2.13x on a microbenchmark. (#9192)
- Sparse arrays no longer pay an eager cost for their holes:
Array(20_000_000)stays mostly metadata instead of doing work proportional to twenty million imaginary elements. (#8847) - A new lazy
JS::Substringtype backs regexp captures and string builtins likeslice,split, and indexed access, gaining 1.066x on Octane’s regexp benchmark. (#8863) - Source positions are preserved end-to-end in bytecode source maps, saving ~250ms on x.com. (#9027)
- Zero-copy
TransferArrayBuffersaves ~130ms on YouTube load. (#9088) - Cached typed-array views switched from a
WeakHashSetto an intrusive list, saving ~250ms loading the Intel ISA PDF in pdf.js. (#9180) - Every
Promiseallocated twoPromiseResolvingFunctioncells withAK::Functionclosures that didn’t actually capture anything. They’re now static functions dispatched by aKindenum, dropping a per-resolver allocation across every promise the engine creates. (#9188) - Skipping property-table marking for non-dictionary shapes cut 1.3 seconds off GC time while loading maptiler.com. (#9044)
- A fast path for
Array.prototype.indexOfon packed arrays (#9123) Array.prototype.sortreuses cached UTF-16 instead of re-transcoding on every comparison (#9036)- Imports for WASM, JSON, and CSS modules (#6029)
- Removed
ShadowRealmsupport, since the proposal has stalled in the standards process (#8753)
GTK4 / libadwaita frontend
Ladybird has a new Linux frontend built on GTK4 and libadwaita, sitting alongside the existing Qt frontend (#8691). It’s inspired by GNOME Web (Epiphany) and follows GNOME’s design guidelines: no menubar, a hamburger menu, and AdwTabView for tabs. Out of the box you get autocomplete and security icons in the URL bar, find-in-page, fullscreen, context menus, alert/confirm/prompt/color/file dialogs, clipboard, multi-window, light/dark theme, and DPR scaling. It’s still early, so not yet at feature parity with the Qt and AppKit frontends.
Bookmarks
Last month we got bookmarks. This month they got a proper management UI:
- An
about:bookmarkspage for managing bookmarks and folders (#8825) - Bookmark import and export from the new page (#8938)
- Context menus for editing bookmarks and folders (#8715)
- A
date_addedtimestamp on every bookmark and folder (#8867) - Bookmarks bar QoL: open in new tab, copy URL, middle-click and Ctrl/Cmd+click to open in new tab (#8758)
- The HTML5 drag-and-drop API is now wired up (#8783).
about:bookmarksuses it for reordering, and it works on regular web pages too.
Cache and CacheStorage
We implemented Cache and CacheStorage end to end, with all nine methods (open, has, delete, keys, match, matchAll, add, addAll, put) backed by an ephemeral in-memory store (#8745).
CSS features
- Basic support for the standard
image-set()and-webkit-prefixed forms.
Source: Hacker News














