Ladybird Newsletter

Archives
Subscribe
December 3, 2025

This Month in Ladybird: November 2025

November is already over, and we got a lot done! We merged 215 PRs from 34 contributors. Let's look at some highlights!

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:

  • 37signals with $10,000 (renewed sponsorship!)
  • Coursiv with $1,000
  • Andrew Mead 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 7,497 new test passes, bringing our total to 1,972,556.

For context, here are the current top 6 browser engines and their WPT scores today vs. one month ago.

Variable fonts

In the past, a font file could contain multiple versions of a typeface such as bold and italic, but those would all be baked in ahead of time. However, newer "variable fonts" are becoming increasingly popular on the web. These are configurable based on a few axes such as the weight, width, or slant, and generate the desired font on the fly. This saves on downloads, but also makes it possible to animate font properties.

We've had small parts of this implemented in various places for a while, but this month the pieces finally started coming together, and we can render fonts with variable widths and weights, instead of them always looking thin. (PR #6439)

Before:

After:

Perspective and perspective-origin

We've implemented these two CSS properties. When an element has some kind of 3D transform on it, perspective determines how far the camera is from the subject. perspective-origin determines where the vanishing point is positioned. Together they give authors a lot more control over how 3D designs look. (PRs #6776 and #6805)

Unbuffered Fetch

There were two modes in our Fetch implementation: unbuffered and buffered. Unbuffered responses exposed data immediately as it arrived from the network. Buffered responses accumulated everything in memory and only produced the full body at the end.

We've now removed the buffered mode, so all Fetch users receive partial data as it comes in. Individual callers can still choose to buffer data on their own if needed.

There's ongoing work to make better use of streaming data in areas like HTML parsing and media decoding.

One visible improvement is that ChatGPT responses now stream in incrementally instead of appearing all at once.

(PR #6839)

Respecting UNPACK_PREMULTIPLY_ALPHA_WEBGL

This WebGL pixel storage parameter determines if a texture should be uploaded with (un)premultiplied alpha. Previously we always uploaded textures with premultiplied alpha, which made content expecting unpremultiplied alpha to be visually broken.

Before:

After:

SVG improvements

We've introduced support for <feComponentTransfer>, which allows you to define how color channels are transferred, and <feMorphology>, which makes it possible to erode or dilate the shapes in the input image. On top of that, we improved our interpretation of <text> and <tspan> attributes so text shows up where it should.

Before:

After:

(PRs #6709, #6831 and #6878)

Faster Base64 conversion

The author of simdutf published a blog post about Base64 performance in browsers, and we took this as an opportunity to go from terrible performance to decent. (PR #6975)

With a handful of optimizations in our runtime, we were able to improve the benchmark score in the blog post by:

  • 3.8x faster fromBase64()
  • 38.4x faster toBase64()

At this point, the main bottleneck is malloc() and that's something we'll eventually have to address as well.

CSS @import rules now block first render

We now wait until CSS @import rules have downloaded their referenced style sheets before rendering the page. This avoids an unpleasant FOUC (flash of unstyled content).

It's particularly noticeable on pages that use a lot of @import rules. (PR #6834)

SVG images on the GPU

Most of our rendering has been GPU accelerated for over a year already, but one thing remained: SVG content embedded in <img> tags!

We've finally refactored the rendering architecture for SVG embedded images, and they too now render with GPU acceleration. This is significantly faster. (PR #6699)

More consistent setInterval() callback timing

We had a sneaky bug where timers created by setInterval() would reschedule themselves after running their callback. This is fine if the callback returns instantly, but when the callback takes more time, the timer starts to drift.

To improve the situation, we now reschedule these timers before the callback runs. Together with some other optimizations, this made it possible to play Diablo on the web at full framerate (20 fps)! (PRs #6985 and #6982)

Native functions can now be written in JavaScript

All of our native functions are written in C++, which does not allow us to use key JavaScript features such as try/catch/finally and function suspension, a core component of async and generator functions, since these are handled by the bytecode interpreter.

We can now write these functions in JavaScript itself, allowing the use of such features. In particular, our implementation of Array.fromAsync is now written in JavaScript.

This gained us four Array.fromAsync test262 passes related to using the correct suspension mechanism implemented by the interpreter. (PR #6728)

DNS-over-HTTPS

There is ongoing work to be able to perform DNS queries over HTTPS, in addition to the UDP and TLS modes.

This is pending some final pieces (such as completing the UI) before code review. However, we are able to perform DNS-over-HTTPS, shown in this preliminary screenshot:

Checking out with Shopify

Luke, one of our full-time engineers, was able to buy a Christmas present from a Shopify store using Shop Pay!

Media bug fixes and improvements

This month, our WebM/Matroska demuxer has had several improvements. It now supports files that use Xiph lacing to store multiple frames in a block, and blocks that store metadata inside a BlockGroup element. In files that specify the complex type for a track, the type will now be selected based on the track's codec ID. (PRs #6785, #6686 and #6881)

We now signal EOF to FFmpeg's decoders, which enables us to play the last few frames of videos encoded using H.264, where frames in the container are reordered based on inter-frame dependency. (PR #6688)

We fixed a couple of issues that could cause stale audio to remain in the pipeline and break output after a backward seek. (PRs #6786 and #6804)

Windows support

While not one of our release targets, the community continues improving support for Windows.

Loading the project's homepage would result in a few missing images caused by some unhandled errors in our non-blocking sockets. Handling those errors in our socket abstractions allows all the images to load successfully. Additionally, implementing file transfers over a socket has enabled us to use the HTTP disk cache. (PRs #6863 and #6983)

Switching Skia's font manager from fontconfig to DirectWrite means we now display the correct fonts on our homepage! (PR #6904)

Credits

We'd like to thank everyone who contributed code this month:

Ali Mohammad Pur, Aliaksandr Kalenik, Andreas Kling, aplefull, ayeteadoe, Callum Law, Daniel Price, Estefania, Gabe, Glenn Skrzypczak, Hendiadyoin1, InvalidUsernameException, Jelle Raaijmakers, Lorenz A, Luke Wilde, Marcos Del Sol Vives, mikiubo, norbiros, Pavel Shliak, Prajjwal, Psychpsyo, R-Goc, Rocco Corsi, Sam Atkins, stasoid, stelar7, SvDp, Tete17, Tim Ledbetter, Timothy Flynn, Undefine, Zaggy1024

Don't miss what's next. Subscribe to Ladybird Newsletter:
Powered by Buttondown, the easiest way to start and grow your newsletter.