May is done! It’s been a pleasantly productive month for the Ladybird project. We’ve merged 261 PRs from 53 contributors and made strong progress on multiple fronts.
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:
- The Primeagen with $5,000
- SerpApi with $5,000
- b1ack0wl with $1,000
- Clément Sibille with $1,000
- Gian Giovani with $1,000
- Philip Lonsing with $1,000
- Sjors Witteveen with $1,000
We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.
Our nonprofit is now officially tax-exempt
The IRS has officially recognized the Ladybird Browser Initiative as a public charity, granting us tax-exempt status.
This is retroactive to March 2024, so donations made since then may be eligible for tax exemption (depending on country-specific rules).
You can find all the relevant information on our new Organization page.
Web Platform Tests (WPT)
We’ve been making solid progress on WPT as usual. This month, we added 15,961 new passing tests for a total of 1,815,223.
For context, here are the top 6 engines at the start vs end of May:
We’ve also significantly improved the speed of our WPT runs by splitting the test list into multiple chunks and distributing them across multiple runner processes. This has cut our WPT runtime from over 5 hours to under 1 hour!
New JavaScript date parser
The JavaScript specification only requires that you support one date format (ISO8601). Anything else is implementation defined, and in practice all major engines support a wide range of weird date formats.
This has long been a source of web compatibility issues for us. We’ve had many cases where weird bugs were painstakingly traced back to not being able to parse a date format we’d never seen before.
In the past, we implemented date parsing by trying a list of specific known formats that we’d seen on the web. This list was frequently appended to.
We now have a new, more tolerant parser that allows all kinds of arbitrary sequences without relying on a strict list. This is more similar to what other engines do, and makes us far more likely to handle a date format we’ve never seen before.
Clipboard APIs
There are a handful of clipboard APIs available for web developers to interact with the user’s clipboard. For example, code blocks on GitHub repositories have a handy button to copy the code to the clipboard. We had previously implemented just one of these APIs, and this month we’ve implemented the rest: clipboard.read()
, clipboard.readText()
, clipboard.write()
, and clipboard.writeText()
.
These APIs include security checks to protect users from malicious scripts.
Transferable Streams
Last month, we refined our streams implementation to be much better aligned with the spec. The last remaining feature was stream transfer. This allows web developers to transfer streams via postMessage()
to, for example, a web worker. We’ve now implemented this feature, and as a result, we now have a complete streams implementation!
Initial support for shared workers
Shared workers are a type of web worker that can be shared between multiple browsing contexts (like tabs or iframes) from the same origin. Each SharedWorker
receives a MessagePort
from every browsing context attaching to it by listening to the onconnect
event.
This month, we’ve implemented initial support for SharedWorker
. It doesn’t yet allow sharing between multiple WebContent processes, but it does allow them to be created and used in a single WebContent process.
Future work will focus on enabling true sharing between processes, which will also lay the groundwork for true ServiceWorker support.
Replace in-house BigInt implementation with LibTomMath
We’ve replaced our UnsignedBigInteger and SignedBigInteger implementations in LibCrypto with LibTomMath. This is both a correctness and performance improvement, as BigInt operations were prominent in performance profiles of Google Maps before this change.
var()
and attr()
in CSS shorthands
When a CSS shorthand property (for example, margin
) is encountered, we expand it and assign values to its longhands (margin-top
, margin-right
, etc). However, when it includes something like var()
or attr()
, the engine can’t resolve those longhand values immediately, but we still need to assign a provisional value to proceed with cascade resolution. For example, margin-bottom
might be set as well, and we need to know which one will apply.
This month we implemented the CSS concepts of the “pending-substitution value” for this, as well as the “guaranteed-invalid value” which is used when the var()
does not resolve to anything and the declaration is invalid at computed-value time.
Early work on @page
for printed media styling
Continuing from the work on descriptors last month, we now partially implement @page
rules, which allows websites to control the page styling when they are printed. Printing isn’t supported yet, but once it is, the groundwork for applying styles will already be in place.
Performance work
We’ve also done a fair bit of performance work this month, targeting Speedometer and various websites that are slower than we’d like.
The following sections give more detail on some of the optimizations, which in total led to a 10% speed-up on Speedometer 2.1.
JS optimization: polymorphic inline caches
Until now, all our inline caches (IC) have been monomorphic. While this doesn’t matter very much for the classic JavaScript benchmarks, real-world web content tends to do a lot of polymorphic access.
To better support this, inline caches have been expanded to remember up to 4 different object shapes per property access. This greatly improved cache hit rates on Speedometer (from 88% to 97%).
Here’s a simple example to illustrate how a polymorphic IC helps us.
The o.foo
property access here would keep missing in a monomorphic IC.
The miss happens because o.foo is stored in property slot #0 for o1
below, but property slot #1 for o2
, so we keep flip-flopping between the two shapes.
With polymorphic IC, we can remember both shapes and always hit the cache!
function readFoo(o) {
return o.foo;
}
const o1 = { foo: 123 };
const o2 = { bar: 123, foo: 456 };
for (let i = 0; i < 1000; ++i) {
readFoo(o1);
readFoo(o2);
}
JS optimization: faster bound function calls
A bound function is a bundled function call with a pre-populated set of arguments.
Previously, we used a trampoline function to dispatch these calls. This meant that two stack frames were created. By avoiding the need for a trampoline, we’ve cut it down to a single stack frame.
JS optimization: faster iteration over built-in types
We’ve added new bytecode instructions that avoid creating temporary { done, value }
“result” objects when iterating over built-in types like Array
, Map
, Set
and String
. This means for..of
, for..in
and the spread operator now run with significantly less overhead.
We’ve also applied a similar optimization to async
functions, which internally rely on generators, and made them faster as well by avoiding temporary object allocations.
Array element assignment now has a fast path. If objects in the array’s prototype chain haven’t been modified and the array is not a proxy, we skip the prototype lookup required for potential setters and instead write the value directly into the array’s storage.
Credits
We’d like to thank everyone who contributed code this month:
Ahmed Elawad, Ali Mohammad Pur, Aliaksandr Kalenik, Andreas Kling, Andrew Kaster, aplefull, Ashton, ayeteadoe, Aziz Berkay Yesilyurt, Bastiaan van der Plaat, Ben Eidson, blukai, Callum Law, Chris Mulder, Colin Reeder, Dan Berglund, Daniel Bertalan, devgianlu, enkvadrat, Francesco Gazzetta, Gingeh, Glenn Skrzypczak, Golho, InvalidUsernameException, Jelle Raaijmakers, Jess, Kenneth Myhra, Khaled Lakehal, Lion Kortlepel, Lucien Fiorini, Luke Wilde, Lyra, Manuel Zahariev, Mark Langen, mikiubo, Mikkel Krautz, Noah, Psychpsyo, R-Goc, rmg-x, Rocco Corsi, Ruben, Sam Atkins, Shannon Booth, sideshowbarker, stelar7, Tim Ledbetter, Timothy Flynn, Veeti Paananen