Skip to content
HN On Hacker News ↗

This Month in Ladybird - April 2026 - Ladybird

▲ 507 points 152 comments by richardboegli 2mo ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

20 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 5 of 6
SEGMENTS · AI 0 of 6
WORD COUNT 1,684
PEAK AI % 38% · §5
Analyzed
May 2
backend: pangram/v3.3
Segments scanned
6 windows
avg 281 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,684 words · 6 segments analyzed

Human AI-generated
§1 Human · 14%

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.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.

§2 Human · 20%

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).

§3 Human · 26%

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 + b loop. (#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::Substring type backs regexp captures and string builtins like slice, 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 TransferArrayBuffer saves ~130ms on YouTube load. (#9088) Cached typed-array views switched from a WeakHashSet to an intrusive list, saving ~250ms loading the Intel ISA PDF in pdf.js. (#9180) Every Promise allocated two PromiseResolvingFunction cells with AK::Function closures that didn’t actually capture anything. They’re now static functions dispatched by a Kind enum, 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. (#

§4 Human · 27%

9044) A fast path for Array.prototype.indexOf on packed arrays (#9123) Array.prototype.sort reuses cached UTF-16 instead of re-transcoding on every comparison (#9036) Imports for WASM, JSON, and CSS modules (#6029) Removed ShadowRealm support, 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:bookmarks page 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_added timestamp 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:bookmarks uses 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 image-set() : Basic support for the standard and -webkit- prefixed forms.

§5 Mixed · 38%

At paint time we pick the candidate whose resolution best matches the device pixel ratio, skipping unsupported MIME types. This makes header images show up on gocomics.com. (#9090) BeforeAfter position-anchor and CSS anchor positioning : Initial support for anchor-positioned elements, fixing the hand and gun positioning on cssdoom.wtf. (#8686) BeforeAfter Color interpolation rewrite : Aligned with css-color-4. We now interpolate in float instead of u8, handle missing and powerless components correctly, deal with out-of-gamut sRGB, and apply alpha multipliers consistently. (#8934) Presentational hints through the cascade : Legacy presentational HTML attributes (align, bgcolor, etc.) used to bypass the regular CSS cascade and write directly into the element’s cascaded properties. They now go through the cascade as normal author declarations, so var() substitution and the invalid-at-computed-value-time fallback work correctly. Fixes a crash on html.spec.whatwg.org. (#9176) align on table sections and rows : <thead>, <tbody>, <tfoot>, and <tr> honor the align presentational attribute, fixing button placement on bricklink.com. (#9177)

BeforeAfter stroke-dasharray interpolation : SVG dashes finally animate smoothly. (#9133) autofocus : Elements with the autofocus attribute actually receive focus on page load now. (#9016) List markers in RTL text : Bullets now sit on the right side of right-to-left text, fixing list rendering on Arabic Wikipedia. (#9099)

BeforeAfter Inline flex/grid baselines : An inline flex or grid container now derives its baseline from its child’s first line box, not its last wrapped line. Fixes link text and icon alignment on nos.nl. (#9183)

BeforeAfter Networking getaddrinfo no longer blocks the event loop. LibDNS now runs lookups on a thread pool, fires A and AAAA queries in parallel (RFC 8305-ish), and coalesces concurrent lookups for the same name.

§6 Human · 15%

RequestServer’s preconnect path was sneaking past our resolver and letting libcurl spawn its own threaded resolver that would pthread_join us on the main thread; that’s now routed through the same DNS pool. (#9109)Profile of loading x.com when DNS is slow, before and after: Over in RequestServer, draining queued response data was O(n²) when WebContent was slower than the network. RequestServer was spending ~30 seconds in memcpy and 3 seconds in Vector::remove while opening a YouTube video! Switching AllocatingMemoryStream to a singly-linked chunk list made consumption O(1). (#9028)We now advertise AVIF and WebP in our Accept header for image requests, matching other engines. Some CDNs use the Accept header to decide whether to serve modern formats or fall back to JPEG. (#9046) Style invalidation Selector invalidation used to be straightforward: selectors always looked downward. :host ruined that. :has() made it way worse. Any descendant change can now force you to walk up the tree finding ancestors whose :has() arguments just flipped, and a lot of this month’s invalidation work is about making that walk less wasteful.Four big wins this month: Reddit rule cache rebuilds: 13.2s → 3.2s. Stylesheet mutations no longer rebuild every style scope’s cache when only one scope changed. (#9138) Reddit infinite scroll: 11% fewer pointless recomputes. Sibling structural invalidation stopped fanning out to descendants that don’t observe the position. (#9155) :has() mutation invalidation skips unaffected anchors , with substantial reductions measured on azure.com. (#9168) :has() child-list visits on the Intel ISA PDF: 71k → 1.6k. Coalesced when pending data already covers every concrete feature bucket the scope cares about, saving ~650ms on the pdf.js load. (#