This Month in Ladybird — March 2026

Hello friends! In March we merged 352 PRs from 49 contributors, 19 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:

We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.

Media Source Extensions

We’ve implemented a portion of the Media Source Extensions spec, supporting playback of VP9 video and Opus audio in WebM (#8655). This allows YouTube quality selection to work, up to 4K on most videos:

As you can see from the video, playback performance still needs work, and memory usage will be high because we don’t yet evict buffered data when requested by the page. The feature is only available with the --expose-experimental-interfaces flag until the implementation is more complete.

Hand-written assembly interpreter

We added AsmInt, a hand-written assembly bytecode interpreter for LibJS (#8299). Instead of interpreting bytecode through a C++ switch loop, AsmInt uses a custom DSL that compiles to native x86_64 and AArch64 assembly. Hot paths like arithmetic, comparisons, and property access stay entirely in hand-written assembly, only calling into C++ for complex operations.

Between AsmInt, the new regex engine, and other optimizations throughout the month, our benchmark scores improved significantly: Kraken 1.44x, Octane 1.37x, and SunSpider 1.69x.

After the initial landing, we spent much of the month squeezing more out of the generated assembly:

  • Using hardware overflow flags for int32 arithmetic instead of software checks (#8314)
  • Pinning frequently-used tag constants in callee-saved registers (#8324)
  • Caching the program base + program counter in a callee-saved register on AArch64
  • Eliding redundant floating-point comparisons in consecutive branch instructions
  • Moving NaN canonicalization to cold fixup blocks
  • Emitting proper unwind info for both x86_64 and AArch64 so profilers and debuggers can unwind through assembly frames (#8455)

Bookmarks

Ladybird now has persistent bookmarks (#8589, #8715). You can bookmark pages, manage them through the application and context menus, and they persist across sessions in a JSON-backed store. Both the Qt and AppKit UIs have bookmark menus and toolbar integration.

New regex engine

The ECMA-262 regex engine was rewritten from scratch in Rust (#8612). The old engine went directly from parser to bytecode, which made it very hard to optimize. The new engine builds an AST first, giving us room to analyze and transform patterns before generating bytecode. This has already paid off: the Octane regexp benchmark runs about 6x faster. We imported V8 and WebKit’s regex test suites to ensure correctness.

More JavaScript engine work

The parser now runs on worker threads (#8211). When the HTML parser encounters a <script> tag, it dispatches parsing to a thread pool and only performs bytecode generation on the main thread. Since the parser is written in safe Rust, parallelizing it was straightforward. Here’s a profile of x.com loading: the main thread carries on executing JavaScript while four background threads parse downloaded scripts in parallel.

CPU activity showing parallel JS parsing on x.com

Object property storage got an overhaul (#8479). Named properties now live behind a raw pointer with 2 inline slots directly on Object, so small objects avoid heap allocation entirely. Indexed properties replaced a virtual dispatch hierarchy with inline Packed/Holey/Dictionary storage kinds, and Array.prototype methods get fast paths for packed arrays.

JS-to-JS calls now stay in the interpreter dispatch loop instead of re-entering the interpreter from C++ (#8270). async/await also got faster: non-thenable await values skip promise machinery entirely (#8449), and awaiting when the microtask queue is empty resolves synchronously (#8454).

In the parser, we boxed large AST enum variants, bringing ExpressionKind from 56 to 16 bytes and StatementKind from 88 to 32 bytes, which cut 15% of RSS when parsing large bundles like Instagram’s (#8581).

CSS features

  • if() : The if() function can now be used in property values, with the supports() and media() conditions both supported. (#8244)
  • inherit() : We implemented inherit(), which allows property values to use the inherited value of any property. (#8409)
  • @container : Initial parsing support for CSS container queries and CSSContainerRule. (#8626)
  • @function : Initial parsing support for CSS custom functions. (#8624)
  • @font-feature-values : We now parse and apply @font-feature-values rules, including font-variant-alternates: historical-forms. (#8050)
  • text-decoration-skip-ink : Text decorations (underlines, etc.) now cut out around glyph descenders and ascenders, matching the behavior of other browsers. (#8606)
BeforeAfter
BeforeAfter
  • scroll-behavior : The scroll-behavior CSS property is now supported, enabling smooth scrolling. (#8434)
  • text-overflow ellipsis : Reworked as a line box post-processing step, so ellipsis now works correctly with inline elements and mixed content. (#8600)
BeforeAfter
BeforeAfter
  • @supports env() : The env() function now works in @supports, allowing pages to query if Ladybird supports a given environment variable. (#8645)
  • Pseudo-element custom properties : Custom properties defined for ::first-line, ::first-letter, and ::placeholder pseudo-elements now work correctly. (#8267)
  • Flexbox baseline alignment : Flex items with align-items: baseline now align correctly, fixing emoji reaction alignment on GitHub among other things. (#8587)
BeforeAfter
BeforeAfter
  • clip-path: inset() border-radius : The inset() function now supports border-radius values. (#8442)
  • Float fixes : clear now clears past all stacked floats, floats now lower past opposite-side blockers, line boxes now shift below floats when their content doesn’t fit, and more. (#8615, #8662)
BeforeAfter
BeforeAfter
BeforeAfter
  • calc() in more places : calc() now works in clip: rect(), repeat(), percentages in color-mix(), the sizes attribute in <img srcset>, grid track placements, and more. (#8676)

Smarter style invalidation

Sibling invalidation sets (#8332) make CSS style invalidation much more precise. Previously, changing an element’s class could trigger style recalculation across a wide swath of the tree. Now, invalidation follows the combinator structure of the selectors that actually matched. For example, .a + .b only invalidates the adjacent sibling rather than the entire subtree.

:has() invalidation now deduplicates ancestor walks (#8353), works correctly with nested :is() selectors (#8351), and font-load invalidation only marks elements that actually use the loaded font (#8357).

More sites working

chess.com was pegging the CPU at 100% due to linear scans in IndexedDB. Switching to binary search and fixing a CSS animation crash made the site usable (#8546).

microsoft.com got a series of rendering fixes: shadow DOM selector matching, whitespace handling, letter-spacing, and grid item sizing. The site looks noticeably better now (#8562).

BeforeAfter
BeforeAfter
BeforeAfter

Removing the legacy JS compiler

Last month, we landed a complete reimplementation of the LibJS compiler frontend and made it the default pipeline. In March, we took the final step: deleting the old compiler pipeline entirely (#8517). The old parser, AST, lexer, and bytecode code generator are all gone. This removed roughly 20,000 lines of C++ code.

With the old pipeline out of the way, we were able to clean up compatibility shims in the codegen and simplify the build, including switching to cbindgen for automatically generating FFI headers instead of maintaining them by hand (#8476).

macOS IPC overhaul

All IPC on macOS now goes through Mach ports instead of Unix sockets (#8514). Mach ports are the native IPC primitive on macOS and support atomic transfer of port rights between processes. This lets us send IOSurface backing stores (GPU-shared memory) directly over the main IPC channel (#8595) instead of through a separate side channel, simplifying the architecture and enabling IOSurfaces on Intel Macs as well.

A new IPC::Attachment abstraction (#8404) unifies file descriptor and Mach port passing across platforms. We also removed several allocation hot spots in the message enqueue and decode paths.

Web Platform Tests (WPT)

We crossed the 2 million mark this month! Our WPT score went from 1,998,398 to 2,003,690, a net gain of 5,292 subtests.

Other notable changes

Scoped custom element registries (#8613) allow more than one CustomElementRegistry per page, avoiding name clashes when multiple libraries define custom elements with the same tag name. Form-associated custom elements (#8534) let custom elements participate in form validation and submission through ElementInternals. This enables frameworks like Home Assistant that rely on the feature.

The Temporal API now supports international calendars (Chinese, Hebrew, Islamic, and others) using icu4x (#8325, #8356), bringing Temporal test262 compliance to 100% for both core and Intl402 tests.

IndexedDB transactions are now correctly ordered by dependency, and request reverts work properly (#8264, #8295). This fixed issues on sites that use IndexedDB heavily.

The document unload and destroy lifecycle was rewritten to follow the spec (#8397, #8660), replacing several blocking spin_until() calls with proper state machines and async flow.

Paint commands are now cached per paintable (#8271), so subsequent display list rebuilds replay cached commands instead of re-resolving all paint properties.

IntersectionObserver now supports scroll margins, correctly accounts for intermediate scroll containers, and uses binary search for threshold lookups. The number of allocations on the hot path was also reduced. (#8574)

The built-in media controls now have a button to toggle fullscreen. (#8219)

Selectors parsed by querySelector and querySelectorAll are now kept in a per-document cache, avoiding re-parsing selectors that are repeatedly used. (#8535)

WebCrypto now supports KMAC authentication. (#8445)

Credits

We want to give a special shout-out to the 19 people who made their first code contribution this month:

  • aguiarcode. Fixed headline text selection on Tweakers (#8466)
  • alwin. Fixed an outdated script to configure clangd, a C++ language server used by Ladybird developers (#8558)
  • Charlie Tonneslan. Fixed a typo in our CSS code comments (#8591)
  • Christian Frey. Enabled IOSurfaces on Intel Macs (#8279)
  • Davi Gomes
    • Fixed comparing GC weak pointers to references not compiling (#8422)
    • Consistently placed #pragma once declarations to be above includes in header files (#8561)
  • desmese. Fixed a rounding error when calculating the radius of an SVG circle (#7964)
  • dominick038. Clamped extreme CSS dimensional values in more places to prevent crashes during layout (#8367)
  • Dylan Hart
    • Fixed canvas font parsing crashing on detached documents (#8634)
    • Fixed a crash when selecting text on the Ladybird website (#8638)
    • Fixed input being dropped in the Google search box (#8642)
  • Guilherme Mendes. Made it so that <br> elements that are being displayed are always treated as display: inline; (#8646)
  • Ivan Krasilnikov. Fixed parsing of multi-line YAML flags in test262 test configurations, which caused us to fail those tests (#8664)
  • Jenn Barosa. Fixed a crash on Modrinth where a navigation being superseded left session history out of sync (#8275)
  • juhotuho10. Enabled clippy linting (#8200)
  • Lluc Simó. Fixed a crash when multiple fetches are started and cancelled in a short timespan (#8282)
  • Matt Van Horn. Fixed a crash when double clicking with an unrecognised mouse button (#8433)
  • MichielN19. Fixed input elements with placeholder text being positioned too low (#8062)
  • Ollie Hensman-Crook. Fixed JavaScript object methods always being treated as a constructor (#8306)
  • RubenKelevra. Prevented parsing deeply nested JSON objects from crashing and fixed several parser inconsistencies (#8242, #8415)
  • sasetz. Fixed the mouse cursor always being the default in iframes (#8556)
  • slim. Refactored identifier checking in the parser (#8168)

And of course we’d like to thank everyone who contributed code this month:

Adam Colvin, aguiarcode, Ali Mohammad Pur, Aliaksandr Kalenik, alwin, Andreas Kling, Andrew Kaster, Callum Law, Charlie Tonneslan, Christian Frey, Davi Gomes, desmese, dominick038, Dylan Hart, Glenn Skrzypczak, Guilherme Mendes, InvalidUsernameException, Ivan Krasilnikov, Jelle Raaijmakers, Jenn Barosa, Johan Dahlin, Jonathan Gamble, juhotuho10, Lluc Simó, Luke Wilde, Matt Van Horn, Michael Watt, MichielN19, mikiubo, Nicolas Danelon, Ollie Hensman-Crook, Praise-Garfield, Psychpsyo, pwespi, Rob Ryan, Rocco Corsi, RubenKelevra, Ryszard Goc, Sam Atkins, sasetz, Shannon Booth, sideshowbarker, slim, Tim Ledbetter, Timothy Flynn, Undefine, zac, Zaggy1024