December has come and gone, and we ended up merging 197 PRs from 37 contributors. Let’s check out some of the work that got done!
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:
- FUTO with $250,000 (renewal!) (announcement + interview)
- “Open Source Santa” with $5,000 (funds provided by PlanetScale, selected by polling the YouTube audience of ThePrimeagen & teej_dv)
- styfle with $1,000
We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.
Web Platform Tests (WPT)
Progress on WPT continues. This month we’ve added 4,833 new test passes, bringing our total to 1,977,371.
For context, here are the current top 6 browser engines and their WPT scores today vs. one month ago.

CSS features
We added basic support for the ::part() pseudo-element, which lets custom elements expose parts of their shadow DOM for external styling. For example, widget::part(quantity) targets elements inside widget’s shadow DOM that have part="quantity". There’s still work to be done (particularly the exportparts attribute), but this already improves sites like MDN: (PRs #7034 and #7077)
Before on the left, after on the right:

We also implemented the CSS random() function, which lets authors use random values in CSS properties without resorting to JavaScript. (PR #6707)
For @import rules, we implemented the layer() clause for placing imported style sheets inside CSS layers, and fixed bugs that were causing supports() clauses to parse incorrectly. (PRs #6986 and #7021)
On a more minor note, we implemented the :autofill pseudo-class. Ladybird doesn’t support automatically filling form data yet, so this selector never matches, but it does now parse correctly. (PR #7177)
Media playback improvements
Building on our move to unbuffered fetch, we can now play incompletely-buffered media files, waiting for data over the network as needed. (PRs #7049, #7081, #7084 and #7132)
We can now play files with unspecified durations, or where the actual media data exceeds the specified duration. As data is decoded, we extend the timeline based on each frame’s end timestamp. (PR #6945)
We also added proper audio conversion for tracks with differing sample rates or channel counts. Previously, enabling a stereo track would make mono tracks inaudible since we had no conversion. Now we convert everything to the output device’s native format as we decode, allowing all tracks to mix correctly. This also made it easy to re-add WAV playback support. (PRs #7095 and #7149)
JavaScript performance
We made several performance improvements this month:
- Shrunk the generated interpreter code size, which particularly benefits Apple Silicon CPUs. (PRs #7080, #7087)
- Fixed
argumentsobjects deoptimizing property access in functions with no named parameters. (PR #7094) - Added inline caches for setting object properties from C++ code (we only had them for getting properties). (PR #7111)
- Various regular expression performance improvements. (PRs #7111, #7125)
- Added a dedicated bytecode instruction for the
x|0pattern used to coerce values to 32-bit integers. (PR #7140) - Made the
prototypefield on function objects lazily instantiated, since most code never uses it. (PR #7167) - Eliminated a redundant copy of all JavaScript source code. (PR #7209)
HTTP caching
We enabled HTTP disk cache by default (#6991), reducing redundant network traffic on repeat visits. We also implemented the stale-while-revalidate Cache-Control directive (#7121), which serves stale responses immediately while revalidating with the origin server in the background.
Garbage collector leak fixes
We spent time hunting down GC leaks this month. At the start, a full test-web run ended with ~240 leaked Window objects. Through a series of fixes, we brought that down to ~25.
The main culprit was improper use of GC::Root, storing strong references in places where they kept objects alive forever. This happened in 2D canvas fill/stroke styles, the rejected promises list, custom elements reaction stack, BroadcastChannel, and IndexedDB. Most fixes involved switching to GC::Weak references. We also introduced GC::HeapVector, a vector container that properly participates in GC. (PRs #7223, #7230, #7235, #7246)
Post-quantum cryptography
We’ve begun implementing post-quantum cryptography in the Web Cryptography API — algorithms designed to resist attacks from future quantum computers. This includes the recently NIST-standardized ML-DSA (PR #6935) and ML-KEM (PR #6970), along with key encapsulation operations (PRs #6961 and #7042).
We also added other modern cryptography algorithms: SHA-3 (PR #6942) and Argon2 (PR #6962).
Credits
We’d like to thank everyone who contributed code this month:
Ali Mohammad Pur, Aliaksandr Kalenik, Andreas Kling, aplefull, Arran Ireland, ayeteadoe, bingyuan.ng, breakgimme, Callum Law, Carlos Sousa, Feng Yu, Gingeh, Glenn Skrzypczak, InvalidUsernameException, Jacob M Hunter, Jan Koudijs, Jani Hautakangas, Jelle Raaijmakers, Jonathan Gamble, Lorenz A, Luke Wilde, Marcos Del Sol Vives, mikiubo, pepperoni21, Psychpsyo, R-Goc, Rocco Corsi, Sam Atkins, Sam Kravitz, Shannon Booth, Tete17, Tim Ledbetter, Timothy Flynn, Undefine, usebeforefree, Zaggy1024