Planet Igalia Chromium

May 15, 2026

Gyuyoung Kim

Blink for Apple tvOS: 2026 Update

At BlinkOn 20 in 2025, I introduced our experimental work on bringing Blink to Apple tvOS. You can also find a blog post covering that initial work here: https://blogs.igalia.com/gyuyoung/2026/05/09/introduce-blink-for-apple-tvos/.

If you’re interested in the background and early prototype, you can find more details in my previous post on Blink for iOS and related work: https://blogs.igalia.com/gyuyoung/2024/08/08/chrome-ios-browser-on-blink/.

Over the past year, we have continued developing this effort, and I recently had a chance to share an update along with a demo running on a real Apple TV device at BlinkOn 21 in 2026. In this post, I’d like to walk through what has changed since the initial prototype, what works today, and what challenges still remain.

A quick recap

Apple TV runs tvOS, which is derived from iOS, but it comes with important differences. Most notably, tvOS does not provide a WebKit WebView for third-party applications. This means that any application requiring web functionality needs to embed its own web engine. This constraint was one of the key motivations behind exploring whether Blink, originally being ported to iOS, could also be adapted to tvOS.

While the idea sounds straightforward, the reality is more complicated. tvOS lacks several low-level system APIs required for Chromium’s multi-process architecture, which makes it impossible to use the standard process model. On top of that, BrowserEngineKit, which the iOS Blink effort relies on, is not available on tvOS. There are also platform restrictions such as the lack of JIT support, and the input model is fundamentally different since Apple TV relies on a remote control rather than touch or pointer-based interaction. Because of these constraints, our goal has not been to build a full-featured browser, but rather to enable Blink-based web capabilities in a way that works within the limitations of the platform.

Progress over the last year

Over the past year, we have made steady progress toward that goal. One of the most significant milestones is that we have upstreamed the initial tvOS implementation. This means that the work is no longer just an isolated experiment, but part of the upstream Chromium codebase. As part of this effort, we enabled content_shell running on the tvOS simulator and on actual Apple TV devices. Moving from simulator-only execution to running on real hardware was an important step, as it allowed us to validate real-world behavior and platform integration.

We have also improved platform integration in several areas. Crashpad support has been added, and input handling for the Apple TV Remote has been significantly improved. The latter is particularly important because navigating web content with a remote requires a focus-based interaction model, which is quite different from what Blink typically assumes on desktop or mobile platforms.

On the web platform side, we have enabled a number of features that make it possible to run more realistic content. WebAssembly now works in interpreted mode, which allows execution within the constraints of the platform. We have also enabled VP9 software decoding and verified hardware-accelerated decoding for H.264 and H.265. These improvements are essential for media playback scenarios and were necessary to support the demo content.

To support ongoing development, we also set up reference bots for tvOS builds and tests. This helps ensure that the port can be maintained over time and reduces the risk of regressions as upstream Chromium continues to evolve.

Demo on a real device

This demo shows playing a YouTube video in content_shell running on a real Apple TV device. The demo also showed that we can navigate the video using the remote controller, which highlights the progress we’ve made in adapting Blink to the tvOS interaction model. While simple, this demonstration is an important milestone because it proves that Blink can run real-world web content on actual hardware.

Current limitations

Despite this progress, several challenges remain. One of the most noticeable issues is build stability. The tvOS port has been broken frequently, mainly because both the iOS and tvOS ports are still experimental and not always considered in upstream changes. In particular, configurations such as the WebAssembly interpreter mode are not consistently handled by all changes, leading to breakage.

Testing is another area where limitations are evident. Since the port relies on a single-process model, running web tests is currently not supported, which makes it harder to validate correctness and compatibility. There are also platform-level gaps, such as missing accessibility support and the absence of certain UI components like file choosers and color pickers. As a result, some web pages do not behave as expected on tvOS.

Next steps

Looking ahead, our focus is on improving the robustness and maintainability of the port. This includes stabilizing the build, expanding test coverage, and investigating ways to run web tests in a single-process environment. We also plan to keep the port up to date with the latest tvOS SDK and continue maintaining it in upstream Chromium.

Closing thoughts

Over the past year, Blink for tvOS has evolved from an initial experiment into a working upstream port that can run on real devices. While it is still early and many challenges remain, the progress so far shows that it is possible to bring Blink-based web capabilities to a constrained platform like tvOS. We will continue exploring this space and see how far this effort can go.

Finally, I would like to thank all the contributors, reviewers, and sponsors who made this work possible.

Igalia Contributors

  • Abhijeet Kandalkar
  • Gyuyoung Kim
  • Jeongeun Kim (Julie)
  • Raphael Kubo da Costa

by gyuyoung at May 15, 2026 03:31 AM

May 08, 2026

Gyuyoung Kim

Introduce Blink for Apple tvOS

At BlinkOn 20 in 2025, I gave a short lightning talk about an experimental project called Blink for Apple tvOS. Although the presentation took place about a year ago, I wanted to take some time to provide more context on why we started this work, what challenges we encountered along the way, and where the project stands today in this blog again.

Motivation

Apple TV runs tvOS, which is based on iOS, but it differs in some important ways. One of the most notable differences is that tvOS does not provide a WebKit WebView for third-party applications. This limitation has significant implications, as applications that need web functionality must embed their own web engine.

A well-known example is the YouTube app on Apple TV, which uses a custom web engine called Cobalt. This engine is based on an outdated Chromium fork, and maintaining such a fork becomes increasingly difficult over time, especially as the web platform continues to evolve.

At the same time, the Chromium community has been exploring Blink-based implementations on Apple platforms, including the experimental Blink for iOS project. As that work progressed, it naturally led to a new question: whether Blink could also be brought to tvOS. Beyond that, we also started wondering if it would be possible to eventually upstream Blink support for tvOS. These questions became the starting point of this project.

Challenges

Although tvOS is derived from iOS, porting Blink to this platform turned out to be far from straightforward. One of the biggest challenges comes from the lack of support for the multi-process architecture that Chromium relies on. Several low-level system APIs, such as fork(), mach_msg(), and posix_spawn_*(), are not available on tvOS, which makes it impossible to adopt the standard process model.

Another major limitation is the absence of BrowserEngineKit, which the Blink port on iOS uses for process management and integration. Without this framework, alternative approaches are required to make the system work on tvOS.

In addition, tvOS does not allow JIT compilation due to platform restrictions, which directly affects the execution model of V8. This requires running JavaScript in a more restricted mode compared to other platforms.

The input model also differs significantly. Apple TV primarily relies on a remote control, which leads to a focus-based navigation model rather than pointer-based interaction. This affects how web content needs to be handled and navigated.

Given all these constraints, it became clear that the goal of this project should not be to build a fully-featured browser. Instead, we focused on enabling Blink-based web capabilities on tvOS in a way that is both practical and maintainable.

Current progress

To get Blink running on tvOS, we made a number of changes across both the build system and the runtime. On the build side, we introduced tvOS-specific configurations, including a new toolchain, the IS_IOS_TVOS build flag in C++ and Objective-C code, and a target_platform = "tvos" setting in GN.

On the runtime side, the lack of multi-process support required us to enable a single-process mode. We also removed or disabled code paths that depend on BrowserEngineKit and ensured that unsupported low-level system APIs are not used in the tvOS build.

Several modifications were also necessary in core components. For example, JIT was disabled in V8, and build configurations were adjusted for third-party libraries such as ANGLE, V8, and Dawn to make them compatible with tvOS.

At the same time, we worked on integrating platform-specific features. This includes support for hardware-accelerated graphics, media codecs, and Crashpad, as well as improvements to input handling to better support remote-based interaction.

As a result of these efforts, content_shell is now able to run in our internal repository, and work toward upstreaming is currently in progress.

Demo

To demonstrate the current state of the project, we prepared a simple demo showing Blink running on tvOS. In this demo, a YouTube video is played inside content_shell on the tvOS simulator, which illustrates that Blink is capable of rendering and running real-world web content in this environment.

Next steps

There is still a significant amount of work ahead. In the short term, our focus is on making content_shell build and run in upstream Chromium, setting up a reference bot for tvOS builds, and passing relevant unit tests, browser tests, and web platform tests.

In other words, we are currently transitioning from a prototype that “works” to something that is stable, maintainable, and ready for upstream integration.

Closing thoughts

One of the most interesting aspects of this project is how different tvOS is, despite being closely related to iOS. Even relatively small platform restrictions can have large architectural implications when working with a complex system like Blink.

While tvOS is a constrained environment, that is precisely what makes it an interesting engineering challenge. We will continue exploring how far we can take this effort and whether Blink on tvOS can eventually become part of upstream Chromium.

Acknowledgements

This work would not have been possible without the support of many contributors, including Blink and Chromium reviewers, the Google YouTube team, and many collaborators in the community.

Thank you all!

Igalia Contributors

  • Abhijeet Kandalkar
  • Gyuyoung Kim
  • Jeongeun Kim (Julie)
  • Raphael Kubo da Costa

by gyuyoung at May 08, 2026 03:46 PM

April 09, 2026

Andy Wingo

wastrel milestone: full hoot support, with generational gc as a treat

Hear ye, hear ye: Wastrel and Hoot means REPL!

Which is to say, Wastrel can now make native binaries out of WebAssembly files as produced by the Hoot Scheme toolchain, up to and including a full read-eval-print loop. Like the REPL on the Hoot web page, but instead of requiring a browser, you can just run it on your console. Amazing stuff!

try it at home

First, we need the latest Hoot. Build it from source, then compile a simple REPL:

echo '(import (hoot repl)) (spawn-repl)' > repl.scm
./pre-inst-env hoot compile -fruntime-modules -o repl.wasm repl.scm

This takes about a minute. The resulting wasm file has a pretty full standard library including a full macro expander and evaluator.

Normally Hoot would do some aggressive tree-shaking to discard any definitions not used by the program, but with a REPL we don’t know what we might need. So, we pass -fruntime-modules to instruct Hoot to record all modules and their bindings in a central registry, so they can be looked up at run-time. This results in a 6.6 MB Wasm file; with tree-shaking we would have been at 1.2 MB.

Next, build Wastrel from source, and compile our new repl.wasm:

wastrel compile -o repl repl.wasm

This takes about 5 minutes on my machine: about 3 minutes to generate all the C, about 6.6MLOC all in all, split into a couple hundred files of about 30KLOC each, and then 2 minutes to compile with GCC and link-time optimization (parallelised over 32 cores in my case). I have some ideas to golf the first part down a bit, but the the GCC side will resist improvements.

Finally, the moment of truth:

$ ./repl
Hoot 0.8.0

Enter `,help' for help.
(hoot user)> "hello, world!"
=> "hello, world!"
(hoot user)>

statics

When I first got the REPL working last week, I gasped out loud: it’s alive, it’s alive!!! Now that some days have passed, I am finally able to look a bit more dispassionately at where we’re at.

Firstly, let’s look at the compiled binary itself. By default, Wastrel passes the -g flag to GCC, which results in binaries with embedded debug information. Which is to say, my ./repl is chonky: 180 MB!! Stripped, it’s “just” 33 MB. 92% of that is in the .text (code) section. I would like a smaller binary, but it’s what we got for now: each byte in the Wasm file corresponds to around 5 bytes in the x86-64 instruction stream.

As for dependencies, this is a pretty minimal binary, though dynamically linked to libc:

linux-vdso.so.1 (0x00007f6c19fb0000)
libm.so.6 => /gnu/store/…-glibc-2.41/lib/libm.so.6 (0x00007f6c19eba000)
libgcc_s.so.1 => /gnu/store/…-gcc-15.2.0-lib/lib/libgcc_s.so.1 (0x00007f6c19e8d000)
libc.so.6 => /gnu/store/…-glibc-2.41/lib/libc.so.6 (0x00007f6c19c9f000)
/gnu/store/…-glibc-2.41/lib/ld-linux-x86-64.so.2 (0x00007f6c19fb2000)

Our compiled ./repl includes a garbage collector from Whippet, about which, more in a minute. For now, we just note that our use of Whippet introduces no run-time dependencies.

dynamics

Just running the REPL with WASTREL_PRINT_STATS=1 in the environment, it seems that the REPL has a peak live data size of 4MB or so, but for some reason uses 15 MB total. It takes about 17 ms to start up and then exit.

These numbers I give are consistent over a choice of particular garbage collector implementations: the default --gc=stack-conservative-parallel-generational-mmc, or the non-generational stack-conservative-parallel-mmc, or the Boehm-Demers-Weiser bdw. Benchmarking collectors is a bit gnarly because the dynamic heap growth heuristics aren’t the same between the various collectors; by default, the heap grows to 15 MB or so with all collectors, but whether it chooses to collect or expand the heap in response to allocation affects startup timing. I get the above startup numbers by setting GC_OPTIONS=heap-size=15m,heap-size-policy=fixed in the environment.

Hoot implements Guile Scheme, so we can also benchmark Hoot against Guile. Given the following test program that sums the leaf values for ten thousand quad trees of height 5:

(define (quads depth)
  (if (zero? depth)
      1
      (vector (quads (- depth 1))
              (quads (- depth 1))
              (quads (- depth 1))
              (quads (- depth 1)))))
(define (sum-quad q)
  (if (vector? q)
      (+ (sum-quad (vector-ref q 0))
         (sum-quad (vector-ref q 1))
         (sum-quad (vector-ref q 2))
         (sum-quad (vector-ref q 3)))
      q))

(define (sum-of-sums n depth)
  (let lp ((n n) (sum 0))
    (if (zero? n)
        sum
        (lp (- n 1)
            (+ sum (sum-quad (quads depth)))))))


(sum-of-sums #e1e4 5)

We can cat it to our repl to see how we do:

Hoot 0.8.0

Enter `,help' for help.
(hoot user)> => 10240000
(hoot user)>
Completed 3 major collections (281 minor).
4445.267 ms total time (84.214 stopped); 4556.235 ms CPU time (189.188 stopped).
0.256 ms median pause time, 0.272 p95, 7.168 max.
Heap size is 28.269 MB (max 28.269 MB); peak live data 9.388 MB.

That is to say, 4.44s, of which 0.084s was spent in garbage collection pauses. The default collector configuration is generational, which can result in some odd heap growth patterns; as it happens, this workload runs fine in a 15MB heap. Pause time as a percentage of total run-time is very low, so all the various GCs perform the same, more or less; we seem to be benchmarking eval more than the GC itself.

Is our Wastrel-compiled repl performance good? Well, we can evaluate it in two ways. Firstly, against Chrome or Firefox, which can run the same program; if I paste in the above program in the REPL over at the Hoot web site, it takes about 5 or 6 times as long to complete, respectively. Wastrel wins!

I can also try this program under Guile itself: if I eval it in Guile, it takes about 3.5s. Granted, Guile’s implementation of the same source language is different, and it benefits from a number of representational tricks, for example using just two words for a pair instead of four on Hoot+Wastrel. But these numbers are in the same ballpark, which is heartening. Compiling the test program instead of interpreting is about 10× faster with both Wastrel and Guile, with a similar relative ratio.

Finally, I should note that Hoot’s binaries are pretty well optimized in many ways, but not in all the ways. Notably, they use too many locals, and the post-pass to fix this is unimplemented, and last time I checked (a long time ago!), wasm-opt didn’t work on our binaries. I should take another look some time.

generational?

This week I dotted all the t’s and crossed all the i’s to emit write barriers when we mutate the value of a field to store a new GC-managed data type, allowing me to enable the sticky mark-bit variant of the Immix-inspired mostly-marking collector. It seems to work fine, though this kind of generational collector still baffles me sometimes.

With all of this, Wastrel’s GC-using binaries use a stack-conservative, parallel, generational collector that can compact the heap as needed. This collector supports multiple concurrent mutator threads, though Wastrel doesn’t do threading yet. Other collectors can be chosen at compile-time, though always-moving collectors are off the table due to not emitting stack maps.

The neat thing is that any language that compiles to Wasm can have any of these collectors! And when the Whippet GC library gets another collector or another mode on an existing collector, you can have that too.

missing pieces

The biggest missing piece for Wastrel and Hoot is some kind of asynchrony, similar to JavaScript Promise Integration (JSPI), and somewhat related to stack switching. You want Wasm programs to be able to wait on external events, and Wastrel doesn’t support that yet.

Other than that, it would be lovely to experiment with Wasm shared-everything threads at some point.

what’s next

So I have an ahead-of-time Wasm compiler. It does GC and lots of neat things. Its performance is state-of-the-art. It implements a few standard libraries, including WASI 0.1 and Hoot. It can make a pretty good standalone Guile REPL. But what the hell is it for?

Friends, I... I don’t know! It’s really cool, but I don’t yet know who needs it. I have a few purposes of my own (pushing Wasm standards, performance work on Whippet, etc), but you or someone you know needs a wastrel, do let me know at wingo@igalia.com: I would love to be able to spend more time hacking in this area.

Until next time, happy compiling to all!

by Andy Wingo at April 09, 2026 01:48 PM

April 02, 2026

Miyoung Shin

Extension Migration Progress Update – Part 1

Background

Following up on my previous post, I would like to share an update on the progress of the Extension migration work that has been underway over the past few months.

To briefly recap the motivation behind this effort: Igalia’s long-term goal is to enable embedders to use the Extension system without depending on the //chrome layer. In other words, we want to make it possible to support Extension functionality with minimal implementation effort using only //content + //extensions.

Currently, some parts of the Extension system still rely on the //chrome layer. Our objective is to remove those dependencies so that embedders can integrate Extension capabilities without needing to include the entire //chrome layer.

As a short-term milestone, we focused on migrating the Extension installation implementation from //chrome to //extensions. This phase of the work has now been completed, which is why I’m sharing this progress update.


Extension Installation Formats

Chromium supports several formats for installing Extensions. The most common ones are zip, unpacked and crx.

Each format serves a different purpose:

  • zip – commonly used for internal distribution or packaged deployment
  • unpacked – primarily used during development and debugging
  • crx – the standard packaged format used by the Chrome Web Store

During this migration effort, the code responsible for supporting all three installation formats has been successfully moved to the //extensions layer.

As a result, the Extension installation pipeline is now significantly less dependent on the //chrome layer, bringing us closer to enabling Extension support directly on top of //content + //extensions.

Patch and References

To support this migration, several patches were introduced to move installation-related components into the //extensions layer and decouple them from //chrome.

For readers who are interested in the implementation details, you can find the related changes and discussions here:

These links provide more insight into the design decisions, code changes, and ongoing discussions around the migration.


Demo

Below is a short demo showing the current setup in action.

This demo was recorded using app_shell on Linux, the minimal stripped-down browser container designed to run Chrome Apps and using only //content and //extensions/ layers.

To have this executable launcher, we also extended app_shell with the minimal functionality required for embedders to install the extension app.

This allows Extensions to be installed and executed without relying on the full Chrome browser implementation, making it easier to experiment with and validate the migration work.


Next Steps

The next short-term goal is to migrate the code required for installing Extensions via the Chrome Web Store into the //extensions layer as well.

At the moment, parts of the Web Store installation flow still depend on the //chrome layer. The next phase of this project will focus on removing those dependencies so that Web Store-based installation can also function within the //extensions layer.

Once this work is completed, embedders will be able to install Extension apps from Chrome WebStore with a significantly simpler architecture (//content + //extensions).

This will make the Extension platform more modular, reusable, and easier to integrate into custom Chromium-based products.

I will continue to share updates as the migration progresses.

by mshin at April 02, 2026 11:32 AM

March 10, 2026

Yeunjoo Choi

Smarter Chromium GN in Vim with gn-language-server

GN Language Server for Chromium development was announced on chromium-dev. It’s very easy to install in VSCode, NeoVim or Emacs. But how can we configure it with classic Vim + YCM?

Setup

First, install the language server with Cargo.

cargo install --locked gn-language-server

Then, add this to your vimrc.

let g:ycm_language_server = [
      \ {
      \   'name': 'gn',
      \   'cmdline': [ 'gn-language-server' ],
      \   'filetypes': [ 'gn' ],
      \ }
  \ ]

That easy, right?

What’s Working

Hover Documentation

hover

Go To Imports

jump_import

Go To Dependencies

jump_deps

Current Limitations

The following features are not working yet. They may need more configuration or further work:

Code Folding

Classic Vim and YCM don’t support LSP-based folding, and I’m not a big fan of that feature anyway. But you can configure another plugin that supports LSP-based folding, or simply rely on indent-based folding.

Go To Definition

When I try to go to the definition of template, I get an error KeyError: 'uri'. I’m not sure whether this is caused by my local configuration, but it needs further investigation. go_def_error

March 10, 2026 03:06 AM

March 04, 2026

Tiago Vignatti

Accessibility and PDF documents

Accessibility
#

When we think of accessibility, we tend to picture it as something designed for a small minority. The reality is much broader: 16% of the world’s population — 1.3 billion people — live with a significant disability¹. In Brazil alone, where I live, that means around 14.4 million people report some form of disability². And those numbers capture only permanent disabilities.

March 04, 2026 01:00 PM

February 06, 2026

Andy Wingo

ahead-of-time wasm gc in wastrel

Hello friends! Today, a quick note: the Wastrel ahead-of-time WebAssembly compiler now supports managed memory via garbage collection!

hello, world

The quickest demo I have is that you should check out and build wastrel itself:

git clone https://codeberg.org/andywingo/wastrel
cd wastrel
guix shell
# alternately: sudo apt install guile-3.0 guile-3.0-dev \
#    pkg-config gcc automake autoconf make
autoreconf -vif && ./configure
make -j

Then run a quick check with hello, world:

$ ./pre-inst-env wastrel examples/simple-string.wat
Hello, world!

Now give a check to gcbench, a classic GC micro-benchmark:

$ WASTREL_PRINT_STATS=1 ./pre-inst-env wastrel examples/gcbench.wat
Garbage Collector Test
 Creating long-lived binary tree of depth 16
 Creating a long-lived array of 500000 doubles
Creating 33824 trees of depth 4
	Top-down construction: 10.189 msec
	Bottom-up construction: 8.629 msec
Creating 8256 trees of depth 6
	Top-down construction: 8.075 msec
	Bottom-up construction: 8.754 msec
Creating 2052 trees of depth 8
	Top-down construction: 7.980 msec
	Bottom-up construction: 8.030 msec
Creating 512 trees of depth 10
	Top-down construction: 7.719 msec
	Bottom-up construction: 9.631 msec
Creating 128 trees of depth 12
	Top-down construction: 11.084 msec
	Bottom-up construction: 9.315 msec
Creating 32 trees of depth 14
	Top-down construction: 9.023 msec
	Bottom-up construction: 20.670 msec
Creating 8 trees of depth 16
	Top-down construction: 9.212 msec
	Bottom-up construction: 9.002 msec
Completed 32 major collections (0 minor).
138.673 ms total time (12.603 stopped); 209.372 ms CPU time (83.327 stopped).
0.368 ms median pause time, 0.512 p95, 0.800 max.
Heap size is 26.739 MB (max 26.739 MB); peak live data 5.548 MB.

We set WASTREL_PRINT_STATS=1 to get those last 4 lines. So, this is a microbenchmark: it runs for only 138 ms, and the heap is tiny (26.7 MB). It does collect 30 times, which is something.

is it good?

I know what you are thinking: OK, it’s a microbenchmark, but can it tell us anything about how Wastrel compares to V8? Well, probably so:

$ guix shell node time -- \
   time node js-runtime/run.js -- \
     js-runtime/wtf8.wasm examples/gcbench.wasm
Garbage Collector Test
[... some output elided ...]
total_heap_size: 48082944
[...]
0.23user 0.03system 0:00.20elapsed 128%CPU (0avgtext+0avgdata 87844maxresident)k
0inputs+0outputs (0major+13325minor)pagefaults 0swaps

Which is to say, V8 takes more CPU time (230ms vs 209ms) and more wall-clock time (200ms vs 138ms). Also it uses twice as much managed memory (48 MB vs 26.7 MB), and more than that for the total process (88 MB vs 34 MB, not shown).

improving on v8, really?

Let’s try with quads, which at least has a larger active heap size. This time we’ll compile a binary and then run it:

$ ./pre-inst-env wastrel compile -o quads examples/quads.wat
$ WASTREL_PRINT_STATS=1 guix shell time -- time ./quads 
Making quad tree of depth 10 (1398101 nodes).
	construction: 23.274 msec
Allocating garbage tree of depth 9 (349525 nodes), 60 times, validating live tree each time.
	allocation loop: 826.310 msec
	quads test: 860.018 msec
Completed 26 major collections (0 minor).
848.825 ms total time (85.533 stopped); 1349.199 ms CPU time (585.936 stopped).
3.456 ms median pause time, 3.840 p95, 5.888 max.
Heap size is 133.333 MB (max 133.333 MB); peak live data 82.416 MB.
1.35user 0.01system 0:00.86elapsed 157%CPU (0avgtext+0avgdata 141496maxresident)k
0inputs+0outputs (0major+231minor)pagefaults 0swaps

Compare to V8 via node:

$ guix shell node time -- time node js-runtime/run.js -- js-runtime/wtf8.wasm examples/quads.wasm
Making quad tree of depth 10 (1398101 nodes).
	construction: 64.524 msec
Allocating garbage tree of depth 9 (349525 nodes), 60 times, validating live tree each time.
	allocation loop: 2288.092 msec
	quads test: 2394.361 msec
total_heap_size: 156798976
[...]
3.74user 0.24system 0:02.46elapsed 161%CPU (0avgtext+0avgdata 382992maxresident)k
0inputs+0outputs (0major+87866minor)pagefaults 0swaps

Which is to say, wastrel is almost three times as fast, while using almost three times less memory: 2460ms (v8) vs 849ms (wastrel), and 383MB vs 141 MB.

zowee!

So, yes, the V8 times include the time to compile the wasm module on the fly. No idea what is going on with tiering, either, but I understand that tiering up is a thing these days; this is node v22.14, released about a year ago, for what that’s worth. Also, there is a V8-specific module to do some impedance-matching with regards to strings; in Wastrel they are WTF-8 byte arrays, whereas in Node they are JS strings. But it’s not a string benchmark, so I doubt that’s a significant factor.

I think the performance edge comes in having the program ahead-of-time: you can statically allocate type checks, statically allocate object shapes, and the compiler can see through it all. But I don’t really know yet, as I just got everything working this week.

Wastrel with GC is demo-quality, thus far. If you’re interested in the back-story and the making-of, see my intro to Wastrel article from October, or the FOSDEM talk from last week:

Slides here, if that’s your thing.

More to share on this next week, but for now I just wanted to get the word out. Happy hacking and have a nice weekend!

by Andy Wingo at February 06, 2026 03:48 PM