Hello friends! May was a busy month, with 464 pull requests merged. Ladybird now clears Cloudflare Turnstile, and scrolling has moved onto an out-of-process compositor. 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:
- CodeRabbit with $10,000
- wheelofnames.com with $1,000
We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.
Passing Cloudflare Turnstile
Cloudflare Turnstile is the bot-detection challenge that sits in front of a large number of sites, deciding whether you’re a real browser before it lets you reach the page. Until this month it refused to let Ladybird through, which kept us out of a lot of sites.
Two things got us here. Over the past several months we’ve been chipping away at the many little web compatibility gaps that Cloudflare’s checks expect a real browser to handle. Cloudflare also made a change on their end to help. We can’t say exactly which combination of the two did it, but in our testing Ladybird now clears the challenge and loads the page.
Async scrolling on an out-of-process compositor
Scrolling has historically run on the main thread, so a busy page could make scrolling stutter. Scrolling now runs asynchronously on the compositor, covering viewport scrolling (#9370), nested scrollable boxes (#9407), and viewport scrollbars (#9386). It’s enabled by default (#9413).
The compositor also runs in its own process now (#9564), with backing store management moved across the boundary and crash recovery so a compositor crash doesn’t take down the page (#9589). It’s the default compositor backend (#9604). Scrolling stays smooth even while the main thread is busy with layout or JavaScript.
Here’s IMDb being scrolled while it loads, without and with async scrolling:
| Without async scrolling | With async scrolling |
|---|---|
Media Source Extensions on by default
Media Source Extensions is now on by default after a month behind an experimental flag (#9673). MSE is how streaming sites feed media to the player in chunks, so turning it on lets a lot more video sites work, and unlocks adaptive streaming on YouTube: higher resolutions, higher frame rates, and quality switching. The same PR added SourceBuffer capacity limits, so Ladybird evicts buffered media data instead of holding onto it indefinitely.
Under the hood, LibMedia’s pipeline was rewritten to allow media data processing to be inserted easily, and seeks are now handled as part of that flow instead of being tracked separately (#9288). Forward video seeks are now aware of already-decoded data in the pipeline, making forward frame skipping on YouTube instant (#9495).
The built-in media controls now display buffered ranges, which are accurately reported for MP3, FLAC, Ogg, WAV, Matroska, and MP4 (#9705).
HTMLMediaElement.getStartDate() is implemented (#9218).
On-disk JavaScript bytecode cache
JavaScript bytecode is now cached on disk (#9259). RequestServer stores bytecode cache blobs as sidecar data alongside script HTTP disk cache entries, and warm cache hits can map bytecode from disk instead of keeping it in anonymous heap memory. On large modern websites, that saves hundreds of megabytes while also avoiding recompilation.
The mapped cache is lazily decoded, so warm hits only pay to decode functions, source text, and instruction streams as they’re actually used (#9433, #9453, #9466, #9472, #9486, #9540).
A WebAssembly JIT
LibWasm gained a Cranelift-based just-in-time compiler (#8866). Instead of interpreting WebAssembly bytecode, hot modules are compiled to native code. That shows up in benchmarks: roughly 8x on CoreMark, and 3-4x on function-call microbenchmarks. The JIT supports macOS (#9356) and is enabled by default (#9758).
Content blocking
We rebuilt Ladybird’s content blocker. The old one was a simple URL substring matcher; the new one is backed by brave/adblock-rust, Brave’s mature open-source content-filtering library (#9538, #9587). It reads standard Adblock Plus / EasyList-style filter lists, and beyond network request filtering it now does cosmetic filtering, hiding page elements with CSS, including generic class and id selectors gathered from shadow-including descendants. Block lists are configurable and can be loaded from settings (#9601).
The HTML parser is now written in Rust
Continuing our push to handle untrusted data from the web in memory-safe languages, the HTML parser fully migrated to Rust this month. The tokenizer (#9429), tree-construction parser (#9457), and speculative preload scanner (#9462) are now all Rust. The Rust parser is also about 10% faster than the C++ version it replaced, and was checked against a corpus of around 7,000 websites for DOM parity with the old parser.
The URL parser and URLPattern implementation moved to Rust as well (#9460).
Incremental GC sweeping
The GC now sweeps incrementally instead of doing all the work in one stop-the-world pass (#7663). Long sweeps are cut into small slices, one heap block at a time, interleaved with normal program execution and driven by both a background timer and allocation pressure. That means fewer long pauses you can actually feel while browsing.
Supporting changes make that pacing work better: heap blocks now come from 2 MiB chunks with decommit deferred to a worker thread (#9299), the collector runs when JavaScript execution goes idle (#9423), and thresholds account for memory held outside the GC heap (#9286). A new family of GC-aware container types also landed, fixing long-standing bugs from keeping GC objects in plain containers (#9494, #9650, #9694).
CSS features
-
@containerqueries : Container queries now work, with thecontainer/container-nameproperties parsed and propagated (#9298), name-only queries (#9319), and size queries (#9405). -
@scope: We implemented@scope, including implicit element scope (#9534, #9677), and made@supports at-rule(@foo)work so pages can feature-detect it (#9536). - Subgrid : Initial CSS grid subgrid support landed (#9620, #9628).
- Transitions and animations on all pseudo-elements : CSS transitions and animations now apply to every pseudo-element, not just a handful (#9519).
- Scroll APIs :
scrollIntoViewis implemented for boxes (#9225), and we addedscroll-marginandscroll-padding(#9367). -
::first-letter: First-letter pseudo-element styles are now applied (#8885). - Gradient fixes : Conic gradients are now painted with Skia’s sweep shader (#9665),
-webkit-linear-gradientnumeric angles are converted correctly (#9528), and gradient color stops interpolate using premultiplied alpha (#9765).
DevTools layout inspection
The DevTools inspector can now inspect grid layouts, showing line numbers, named areas, infinitely-extended lines, recoloring, and subgrid hierarchies (#9672):
Flexbox inspection landed too (#9707):
There’s now a “pick element” button that lets you click an element in the page to select it in the inspector (#9735), implemented for the Qt and AppKit frontends. DevTools can also drive page navigation now (#9801).
Browser UI
Tabs can now run down the side of the window instead of across the top, in the Qt frontend (#9795, #9797). Switch them on and the sidebar shows each tab’s favicon and title, the window controls fold into the navigation toolbar, and the separate titlebar row disappears. It works in both light and dark themes, and you can collapse the sidebar when you want the space.
The Qt chrome was refreshed as well (#9631, #9788). The stock Qt look is replaced with an integrated custom chrome surface for the tab strip, toolbar, omnibox, and window controls, using a quieter, more neutral palette in both themes and generated chrome icons in place of the old TinyVG assets. The omnibox shows a shortened URL while you’re not editing, with contextual leading indicators (a “Not secure” pill for HTTP, a globe for URL-like typed input, a search icon for search input). Tabs support browser-like dragging (reorder, detach, and reparent between windows), frameless windows show resize cursors, and theme changes refresh the whole chrome immediately.
This is still early UI iteration, not a final design, but the Qt frontend is starting to feel much more browser-shaped.
Other UI improvements this month:
- Page loading state is now shown in the browser chrome, so you can tell a page is loading (#9500)
- Reopen recently closed tabs and windows (#9284)
- Detach a dragged tab into a new window by dropping it outside any tab bar (#9636)
- Zoom level is persisted and restored per host (#9255), with a zoom indicator in the location bar (#9790)
- Middle-mouse primary paste, integrated with the X11 selection clipboard (#9421, #9686)
- Sharper icons on high-DPI displays (#9698)
Networking and privacy
- HSTS : HTTP Strict Transport Security is now implemented, so sites that send the header are upgraded to HTTPS on subsequent visits (#9475).
- Permissions API and geolocation : We added the Permissions API along with the geolocation permission (#8874).
- Encoding detection : A
chardetng-based detector now guesses the character encoding of pages that don’t declare one (#9375).
Sites that work better
TradingView charts render now. Implementing ServiceWorkerContainer.getRegistrations() lets the site get past its startup feature checks, and a canvas compositing fix stops translucent chart pixels from accumulating as you move the mouse (#9581).
| Before | After |
|---|---|
![]() | ![]() |
Discord got several rendering and interaction fixes (#9764). foreignObject descendants now participate in hit testing, CSS mask image layers and avatars paint correctly, and drag-selection in the chat layout is much more forgiving, so a selection can start from gutters, avatar-adjacent areas, author names, and the space below message text instead of silently doing nothing.
Shopify no longer crashes on same-document navigation (#9391). Preserving imported binding names in the module bytecode cache removes visible jank during Shopify’s hydration, and grid sizing fixes clear up spurious horizontal scrolling on both Shopify and Reddit .
Hacker News threads got lighter (#9753). Large pages full of nested tables and inline text were doing a lot of avoidable work in the style and layout hot paths. Simple tables now skip redundant row intrinsic sizing, inline layout reuses cached text chunk lists instead of copying them, and scoped rule matching reuses its scratch storage between elements.
A few more:
- wheelofnames.com : The new Skia-backed conic gradient painting cleans up the rendering of the wheel edge (#9665).
- blt.se : Fixed unwanted horizontal and vertical scrolling in the top navigation bar, caused by async wheel hit testing treating hidden overflow axes as scrollable (#9455).
Web Platform Tests (WPT)
Our WPT score went from 2,067,263 to 2,075,546 this month, a gain of 8,283 subtests.
Other notable changes
- Themed text selection colors : Selection highlights now use the system theme’s colors (#9704).
- No more white flash : Dark-mode pages no longer flash white while loading (#9690).
- Animated images pause in inactive tabs , saving CPU on background tabs (#9675).
- Largest favicon wins : We now select the largest decoded favicon as the active page favicon (#9713).
That’s it for May. Thanks for reading, and we’ll see you next month.

