Over the past year, we’ve invested heavily in web platform test suites and benchmarks, and that work has paid off — giving us a solid, well-tested foundation with good coverage of edge cases and spec corners. With an alpha release on the horizon, we’re now building on that foundation by making real websites and daily-driver usability our primary focus. We’ll continue tracking test scores and fixing test failures, but the day-to-day priority is now getting real sites working well. January’s 324 merged PRs from 40 contributors reflect that shift, with much of the month spent profiling, fixing, and optimizing real-world sites.
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:
- Andreas Mähler with $11,000
- Anonymous Chromium Developer with $5,000
- Tyler Horvath with $1,000
We’re incredibly grateful for their support. If you’re interested in sponsoring the project, please contact us.
Playing the (dumb) User-Agent game
An unfortunate reality of the modern web is that many websites check your User-Agent string and serve different content (or no content at all) based on what browser they think you are. With our previous UA string, we were getting degraded UIs, “your browser is not supported” pages, network throttling, and outright HTTP 403 errors from many prominent websites.
We fixed this by adding “Chrome/140.0.0.0” (#7493) and “AppleWebKit/537.36 Safari/537.36” (#7626) to our UA string. “Ladybird” is still in there. We’re not trying to hide, but we do need to play the game if we want websites to serve us their real content. This immediately unlocked modern versions of Google Search, Gmail, Instagram, and many other sites.
Instagram before and after the UA string change:
| Before | After |
|---|---|
![]() | ![]() |
Gmail performance push
Gmail loaded before January, but Google was serving us a degraded UI due to the old User-Agent string, performance was poor, and much of the interface wasn’t properly clickable. With the UA fix giving us the real Gmail UI, we spent a good chunk of the month on performance and correctness.
Through a series of profiling-driven PRs, we knocked roughly a second off the inbox load time:
- Eliminated
HashMapoperations in function instantiation by caching parser data, saving ~400ms alone (#7622) - A mixed bag of layout and style optimizations for another ~500ms (#7623)
- Optimized
getComputedStyle()to avoid layout when possible, plus other style computation speedups (#7633) - Various JS engine optimizations found through Gmail profiling, including scoping
eval()deoptimization per-identifier (#7638) - Scoped pseudo-class invalidation to the common ancestor instead of walking the entire document tree, which was over 10% of runtime when hovering mailboxes (#7639)
- Fixed percentage height resolution against
min-height, which was causing layout issues (#7618)
We also fixed three hit testing bugs that made the compose window fully mouse interactive (#7613).
More sites working
Gmail wasn’t the only site that got attention this month:
- Instagram loads again after implementing
ServiceWorkerContainer.ready(#7403) - Slack hero layout fixed by resolving percentage max-width in intrinsic height calculations (#7529)
- Photopea no longer crashes and has correctly centered buttons (#7614, #7616)
- diagrams.net is now usable after implementing
@-webkit-keyframesas an alias for@keyframes(#7617) - TripAdvisor loads after fixing
MessagePortclose behavior (#7559) - Google Street View now shows street names after fixing
Canvas.measureText()(#7474) - Roundcube got multiple fixes: toolbar button alignment, message list layout, and popover menu clickability (#7562, #7558, #7555)
- hypr.land now renders after adding MIME type sniffing for streaming HTTP responses (#7606)
Web Platform Tests (WPT)
Our WPT scores continued to climb alongside the real-world work. This month we’ve added 13,690 new test passes, bringing our total to 1,991,061.
For context, here are the current top 6 browser engines and their WPT scores today vs. one month ago.

Painting and rendering
We replaced the old ClipFrame / PushStackingContext / PopStackingContext system with a new AccumulatedVisualContext tree (#7471). This tree pre-computes accumulated transforms, clips, scroll offsets, and effects for each paintable, eliminating per-frame recalculation during display list playback and hit testing.
The new architecture fixes a class of bugs where hit testing through nested transforms, scroll containers, and clip-paths would produce incorrect results. It also enables skipping off-screen display list commands with effects like blur, which is particularly beneficial for pages like Discord’s landing page that have many blurred decorative images (#7576).
On the performance side, we also started caching GPU textures for bitmap-backed images instead of re-uploading every frame (#7610), caching SkTextBlobs so text blobs are built once during display list recording and reused across paints (#7607), and moved SVG mask/clip composition from CPU to GPU (#7503).
Media playback
Building on December’s streaming media work, we added HTTP range request support for media playback (#7473). Video can now start playing without waiting for the entire file to download, and seeking to an unbuffered position triggers a new range request instead of waiting for sequential download.
We also made video frames go to Skia as subsampled YUV data instead of converting to RGB on the CPU (#7549). This offloads color conversion to the GPU, allowing high-resolution video playback without interruptions on machines where the CPU can keep up with decoding. Paused videos now also release their decoded data and decoders after a timeout to save memory (#7566).
Fullscreen mode now works on YouTube (#7649), building on the initial fullscreen implementation from #4330. We don’t yet support MediaSource Extensions, so YouTube serves us 360p H.264 fallback video, but it plays and fullscreens correctly:
JavaScript performance
-
JSON.parsenow uses simdjson for parsing, andJSON.stringifywas optimized to use a singleStringBuilderinstead of intermediate allocations (#7436) - Shape caching for object literals : When a function creates object literals with the same property names, we now cache and reuse the resulting shape, avoiding repeated shape transitions (#7406)
- Constant folding for
LogicalExpressionand double-boolean-not!!xoptimization - Dead code elimination for branches with always-truthy or always-falsey conditions (#7587)
- Reduced bytecode size for template literals by eliminating redundant opcodes (#7389)
Regex engine improvements
- Consecutive single-character comparisons are fused into string comparisons (#7142)
- A new seek operation replaces the expensive
/.*/fork-and-backtrack pattern - Forks that cannot possibly produce a match are skipped entirely
- Bytecode buffers are flattened before execution for better cache locality
- Proper lookbehind support via new
StepBackopcode (#7119) - Regex modifiers (
(?i:...),(?m:...), etc.) are now supported, gaining 64 new test262 passes (#7318)
HTTP caching
- Implemented all
Cache-Controlrequest directives:no-store,no-cache,max-age,max-stale,min-fresh, andonly-if-cached(#7630) - Added support for the HTTP
Varyheader, so cached responses are correctly keyed by request headers (#7325) - Integrated the Fetch API’s cache mode, gaining 148 new WPT passes (#7546)
- Moved cache revalidation entirely to RequestServer, removing a redundant implementation in WebContent (#7430)
CSS features
-
:has()result caching : The:has()pseudo-class now caches match results per element, avoiding redundant descendant/sibling traversals (#7314) - Textarea resizing : The
resizeproperty is now functional, allowing users to drag-resize textarea elements (#7206) -
animation-composition: Keyframe composition values are now extracted and applied, gaining 684 WPT subtests (#7337) -
<basic-shape-rect>improvements : Border radius support ininset(),rect(), andxywh(), gaining ~1000 WPT passes (#7339) - Grid layout fixes : Flexible track intrinsic sizing,
fit-content()with zero limits, named grid line resolution, and grid track size list composition (#7456) -
@propertyrules : Custom properties registered via@propertynow have their initial values read in computed style, and rules are properly managed when stylesheets change
IPC hardening
We did a security hardening pass on LibIPC (#7569). Previously, malformed IPC messages could crash the receiving process. Now, decode failures disconnect the peer gracefully, message sizes and file descriptor counts are bounded, and checked arithmetic prevents integer overflows in size calculations. The guiding principle: suspicious incoming messages cause a clean disconnect; encoding errors (our own bugs) still crash immediately so they’re caught quickly.
Firefox DevTools network panel
Our Firefox DevTools integration gained network monitoring support (#7472): request and response body viewing, request initiator types, navigation events, and streaming console messages. You can now inspect network traffic in Ladybird using Firefox DevTools.
Other notable changes
- System font fallback : When no font in the cascade contains a glyph, we now query Skia’s font manager for a system font, fixing rendering of non-Latin text (#7536)
- SQLite WAL mode : Enabling WAL mode for our database reduced a
localStorage.setItemloop from ~3.5 seconds to ~50 milliseconds (#7656) - XML parser replaced with libxml2 : Our homegrown XML parser was replaced with libxml2 for better spec compliance and robustness (#7348)
- Inline layout performance : ASCII fast paths for bidi lookups and grapheme segmentation, plus pre-generated text chunks, improving Speedometer scores (#7422)
- GC heap explorer : A new interactive HTML tool for visualizing and exploring LibGC heap dumps, useful for investigating memory leaks (#7444)
- Cryptography : ML-KEM key import/export in all formats plus decapsulation (estimated ~1000 WPT passes), ChaCha20-Poly1305 AEAD, Argon2 key derivation, and SHAKE digest support
Credits
We’d like to thank everyone who contributed code this month:
Adam Colvin, Ali Mohammad Pur, Aliaksandr Kalenik, Andreas Kling, Andrew Kaster, aplefull, ayeteadoe, Ben Eidson, Callum Law, Christoffer Haglund, Colleirose, CountBleck, dosisod, Estefania, Federico Tedin, Gingeh, InvalidUsernameException, Jelle Raaijmakers, Jonathan Gamble, Kenneth Myhra, Lorenz A, Luke Wilde, matjojo, Michael Watt, mikiubo, Psychpsyo, pwespi, R-Goc, Reimar, Rocco Corsi, Sam Atkins, Samq64, Shannon Booth, sideshowbarker, Tete17, Tim Ledbetter, Timothy Flynn, Totto16, Undefine, Zaggy1024

