WebAssembly

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars56
mentions7
02468Oct 5Oct 19Nov 3
UTC days · repositories WebAssembly owns5 repositories

Package downloads

Cumulative recorded downloads · since 2025-11-01

npm766
0192384576768Oct 5Oct 19Nov 3
Oct 30npm: 1/1 packages observed

Across 1 known packages in repositories WebAssembly owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

WebAssembly’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside WebAssembly’s account where WebAssembly opened or reviewed pull requests in the past 365 days.

Who noticed WebAssembly

Everyone who starred or forked WebAssembly’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

6posts
Nov 40 posts
  1. WASIじゃない、もう一つのインターフェース「WALI」という提案 はじめに 最近、仕事でいろんな言語やツールをWebAssembly(wasm)にして、ブラウザーの中で動かすことをやっています。RubyやPython、vimのような、もともとLinuxやMacで動いているプログラムをソースからwasmにビルドする作業です。 このとき毎回悩むのが、ファイルを開く、プロセスを増やす、といった機能をどうするかです。wasmそのものはこうした機能を持っていないので、外から用意してあげる必要があります。よく使われるのは、JavaScriptで用意した関数に任せるEmscriptenと、WASIという標準の関数のセットを使う方法の2つです。 僕はWASIを使うことが多いのですが、forkが無い、シグナルが無い、mmapが無い、と毎回のように引っかかります。CLIをひとつwasmにしたいだけなのに、ソースのあちこちに#ifdef wasiを足すことになるんですよね。そんな中で知ったのが、WALI(WebAssembly Linux Interface)という研究です。 一言でいうと「WASIのような新しいAPIを作るのはやめて、Linuxのシステムコールをそのままwasmに見せてしまおう」という提案です。最初に読んだときは「それアリなの?」と思ったのですが、調べていくと思った以上にちゃんと考えられていて、しかもLLVMやRustにもすでにターゲットとして入っていました。 この記事では、WALIがどんなものなのかを論文と実物のバイナリの両方から覗いてみます。 先に言葉を整理しておきます この記事は、wasmにしたプログラムがファイルやプロセスを使いたいときの話です。普段Webを書いていると見慣れない単語がたくさん出てくるので、先にざっくり整理しておきます。 言葉 ざっくりした意味 システムコール プログラムがLinuxのカーネルに「ファイルを開いて」「画面に書いて」とお願いするための入口です。read、write、forkなど、Linuxには数百個あります libc C言語の標準ライブラリです。printfやfopenの中で、最終的にシステムコールを呼んでいます。muslはその実装のひとつで、小さくて読みやすいのが特徴です import wasmのモジュールが「この関数は外からもらいます」と宣言しているものです。wasmは自分だけではファイルも画面も触れないので、外とのやり取りは全部importした関数を通します リニアメモリ wasmのプログラムが使える1本の大きなバイト列です。wasmの中のポインタは、このバイト列の先頭から何バイト目か、という数字です WASI wasmをブラウザーの外で動かすために標準化が進められている、ファイルや時計などを使うための関数のセットです。preview1、0.2、0.3と版を重ねています Emscripten CやC++をwasmにするコンパイラーです。ファイルなどの機能はJavaScriptで用意したものを使うのが基本で、ブラウザーで動かすときによく使われます wasm32 ポインタが32ビットのwasmのことです。普段見かけるwasmはほぼこれです ABI コンパイル済みのプログラム同士が合わせておく約束ごとです。longが何バイトか、構造体をどう並べるか、などが含まれます。ABIが違うものは混ぜて使えません ターゲットトリプル コンパイラーに「どこ向けに作るか」を伝える名前です。x86_64-linux-gnuやwasm32-wasip1のように、CPUの種類や動かす環境の名前を並べて書きます WAMR WebAssembly Micro Runtimeの略で、Bytecode Allianceが開発している小さなwasmの実行エンジンです Linuxでforkを呼ぶと何が起きているか WALIの話をする前に、普通のLinuxでCのプログラムがファイルやプロセスを使うときの流れを確認しておきます。ここがわかると、WASIやWALIが「どこを差し替えているのか」がすっと入ってくると思います。 たとえば、こんなCのプログラムがあったとします。 #include <stdio.h> #include <unistd.h> int main(void) { pid_t pid = fork(); if (pid == 0) { printf("子プロセスです\n"); } else { printf("親プロセスです(子のpidは%d)\n", pid); } return 0; } fork()もprintf()も、自分で書いた関数ではないですよね。これはlibcが用意している標準の関数です。Cのプログラムは、ファイルを開くのもプロセスを増やすのも、基本的にlibcの関数を呼ぶだけで済むようになっています。 ではlibcのfork()の中で何をしているかというと、カーネルに「プロセスを複製してください」とお願いするシステムコールを発行しています。printf()も、文字列を組み立てたあと最後はwriteというシステムコールで標準出力に書き込んでいます。図にするとこんな感じです。 つまり、自分のプログラムはlibcの関数を呼んでいるだけで、カーネルとやり取りしているのはlibcのほうです。この「libcとカーネルの境目」がシステムコールなんですね。 wasmの場合、一番下のシステムコールの命令がありません。wasmは自分だけではカーネルに何もお願いできないので、どこかで「外からもらった関数」、つまりimportに置き換える必要があります。 Emscriptenでは、この部分をJavaScriptで用意した関数が受け持ちます。WASIの場合は、libcの関数をWASIの関数に読み替える形になります。そういうlibc互換のライブラリのひとつにwasi-libcがあって、open()やwrite()を呼ぶと中でWASIの関数を呼んでくれます。ただ、WASIに対応する関数が無いものは読み替えようがないので、wasi-libcにはfork()そのものがありません。冒頭の「forkが無い」というのはこういうことなんです。 WebAssembly/wasi-libc WASIの何がつらいのか WALIの話に入る前に、WALIの論文がWASIのどこを問題にしているのかを見ておきます。 論文はカーネギーメロン大学のArjun Rameshさんたちによるもので、EuroSys '25に通っています。最初にarXivに出たときのタイトルが「Stop Hiding The Sharp Knives: The WebAssembly Linux Interface」で、直訳すると「よく切れるナイフを隠すのはもうやめよう」です。forkやシグナルのような強力だけど危ない機能をwasmから隠すのはやめよう、という意味だと思うのですが、なかなか攻めた名前なんですよね。いまは「Empowering WebAssembly with Thin Kernel Interfaces」に変わっています。 arxiv.org 論文がまず指摘しているのは、WASIがいろいろな環境をまたぐために作られた新しいAPIだという点です。新しいAPIなので、実行エンジンごと・環境ごとに実装し直す必要があります。しかもPOSIXと形が違うので、既存のソフトウェアはそのままでは動きません。メモリマップやプロセス、シグナルも無いので、そこを使っているプログラムは書き換えるしかないんですよね。エンジン側の実装も大きくなりがちで、libuvwasiのpreview1の実装はエンジン側のコードだけで6,000行を超えると書かれていました。 もうひとつが、仕様がまだ動いている最中で、長く使うものを載せるには不安定だという点です。これはここ数年WASIを追いかけてきた人なら実感があると思います。preview1から、wasmのモジュール同士を型付きでつなぐComponent Modelという仕組みを前提にしたWASI 0.2に移り、2026年6月にはasyncを入れたWASI 0.3.0が出ました。 wasi.dev どれも良い方向の進化なのですが、そのたびにツールチェーンやランタイムが追いかけることになるのは地味にしんどいところです。 WALIのアイデア そこでWALIは発想を逆にします。さっきの図でいうと「libcとカーネルの境目」、つまりすでに何十年も安定しているLinuxのシステムコールを、そのままwasmのimportとして出してしまおう、というものです。 WALIはLinuxのシステムコールの上に薄く1枚かぶせるだけの層です。WALIの関数はLinuxのシステムコールとほぼ1対1で対応していて、やることは主にwasmのリニアメモリ上のポインタとホストのアドレスの変換くらいです。 こうすると、libcはmuslをほぼそのまま使えます。muslはもともとシステムコールを発行する部分が1か所にまとまっているので、CPUの命令でカーネルを呼んでいたところをimportした関数の呼び出しに差し替えるだけで済むんです。fork()もlibcの関数としてちゃんと残ります。libcがLinuxのつもりで動くので、その上に載っているbashやvim、sqliteも、ほとんどソースを変えずにwasmになります。エンジン側も小さくて、論文ではWAMRへの実装が約2,000行のC、そのうちプラットフォーム固有の部分は100行未満だそうです。 理想のAPIを作るより、もう世の中で一番使われているAPIに合わせてしまう。この割り切りがWALIのおもしろいところなんですよね。 実物を覗いてみる 論文を読むだけだとイメージしにくいので、WALIのリポジトリに入っているビルド済みのバイナリを見てみます。 arjunr2/WALI examples/precompiledの下に、lua・bash・vim・sqliteなどがwasmになって置いてあります。WALIのランタイムはLinuxが前提なので、Macだと動かすにはDockerが要るのですが、中身を覗くだけならwasm-toolsがあれば十分です。 git clone --depth 1 arjunr2/WALI cd WALI/examples/precompiled wasm-tools print lua/lua.wasm | grep '(import' | head すると、こんなimportが並んでいます。見やすいように、関数名と型番号は省きました。 (import "wali" "SYS_read" (func (param i32 i32 i32) (result i64))) (import "wali" "SYS_write" (func (param i32 i32 i32) (result i64))) (import "wali" "SYS_open" (func (param i32 i32 i32) (result i64))) (import "wali" "SYS_fstat" (func (param i32 i32) (result i64))) (import "wali" "SYS_mmap" (func (param i32 i32 i32 i32 i32 i64) (result i64))) importの名前空間はwaliひとつだけで、関数名がSYS_read、SYS_write、SYS_mmap…と、Linuxのシステムコールの名前そのものになっています。WASIだとfd_writeに「どこから何バイト」を並べた配列を渡して、書けた長さをポインタで受け取る、という独特の形でしたが、こちらはwrite(fd, buf, count)がそのままの形で出ています。戻り値も、成功なら値、失敗ならエラーの番号をマイナスにしたもの(-errno)を返す、というLinuxのシステムコールの流儀そのままです。 同じようにbash.wasmのimportも眺めていたら、驚くものが並んでいました。 (import "wali" "SYS_fork" (func ...)) (import "wali" "SYS_execve" (func ...)) (import "wali" "SYS_wait4" (func ...)) (import "wali" "SYS_kill" (func ...)) (import "wali" "SYS_pipe2" (func ...)) wasmがforkをimportしているんですよね!WASIだとそもそもプロセスという概念が薄いので、bashをwasmにしようとしたらforkとexecまわりを全部書き換えることになります。WALIではそれをそのままホストのLinuxに投げてしまいます。論文でも、forkを含むプロセス・スレッド系のシステムコールのほとんどは素通しで実装できる、と書かれています。 ちなみにスレッドも使えます。WALIでは、スレッドを1本増やすたびに同じwasmをもう1つ起動して、メモリだけを共有させる、という方法で動かしているそうです。 名前でimportするのがうまい もうひとつ「なるほど」と思ったのが、importをシステムコールの番号ではなく名前で結んでいるところです。 Linuxのシステムコール番号はCPUのアーキテクチャごとに違います。たとえばwriteはx86_64だと1番、arm64とriscv64だと64番です。さらにopenやstatのような古いシステムコールは、arm64やriscv64にはそもそもなくて、openatやnewfstatatだけがあります。 WALIの仕様はリポジトリのspec/にPythonで書かれていて、そこを見るとこの対応表がそのまま載っています。 impl("read", ["int fd", "void* buf", "size_t count"], ArchNrs(x86_64=0, arm64=63, rv64=63)), impl("write", ["int fd", "void* buf", "size_t count"], ArchNrs(x86_64=1, arm64=64, rv64=64)), impl("open", ["char* pathname", "int flags", "mode_t mode"], ArchNrs(x86_64=2)), wasmのバイナリはSYS_openという名前でimportしておいて、エンジンがホストのアーキテクチャに合わせて番号に変えたり、無いものは他のシステムコールで代わりにやったりします。これで同じwasmがx86_64でもarm64でもriscv64でも動くわけです。 仕様にはLinuxのシステムコールがひと通り並んでいて、よく使われるものから実装が進められています。システムコール以外には、コマンドライン引数を受け取る__cl_get_argcのような、wasmならではの関数が少しだけ足されています。 wasm32なのにlongが64ビット ここが今回いちばん「細かいけど驚いた」ところです。 普通、wasm32ではポインタもlongも32ビットです。WASIのターゲットでもEmscriptenでもそうなっています。ところがWALIでは、wasm32なのにlongが64ビットなんです! RustのWALIターゲットのドキュメントには、普通のwasm32との一番の違いはlongが64ビットであること、そしてそれは64ビットのホストへソースをなるべく変えずに持っていくためだ、と書かれています。 doc.rust-lang.org ホストが64ビットのLinuxなので、カーネルとやり取りする構造体のレイアウトを64ビットのLinuxに寄せておけば、変換せずにそのまま渡せる場面が増える、という理屈です。実際、WALIの仕様の型定義はCの型の大きさを64ビットのLinuxに合わせてあって、struct statは144バイト、つまりx86_64のLinuxと同じ大きさで定義されています。 一方でポインタは4バイトのままで、size_tも32ビットです。LLVMのclangのソースを見ると、WALIのときだけこう分岐しています。 if (T.isWALI()) { // The WALI ABI is documented here: // doc.rust-lang.org // Currently, this ABI only applies to wasm32 targets and notably requires // 64-bit longs LongAlign = LongWidth = 64; SizeType = UnsignedInt; PtrDiffType = SignedInt; IntPtrType = SignedInt; } 「ポインタは32ビット、longは64ビット」というのは、普通のCの世界だとなかなか見ない組み合わせです。longにポインタを入れているような古いCのコードはこれで壊れるので、そこは割り切っているんだなと思います。 ちなみにwasmにも、ポインタが64ビットになるwasm64があります。2025年9月に確定したWebAssembly 3.0で正式に仕様に入りました。「それなら全部64ビットにすればいいのでは」と思いますよね。ただ、wasm64は遅くなりやすいんです。wasm32なら読み書きできる範囲が4GBに収まるので、エンジンはその外側に大きな空き領域を置いておくだけで、範囲外へのアクセスを止められます。wasm64ではこの手が使えないので、読み書きのたびに範囲のチェックが入ってしまいます。Firefoxの開発チームも、4GBを超えるメモリが本当に必要な場合以外はwasm32のほうがいい、と書いています。 spidermonkey.dev WALIがポインタは32ビットのままlongだけを64ビットにしているのは、速さはwasm32のまま保ちつつ、Linuxとやり取りする型の大きさだけを合わせたい、という狙いなのかなと思います。 LLVMとRustにも入っている 研究のプロトタイプで終わっているのかと思いきや、上流のツールチェーンにもちゃんと入っています。 Rustではwasm32-wali-linux-muslがTier 3のターゲットとして入っています。cfgはtarget_os = "linux"かつtarget_familyにwasmとunixの両方が立つ、というおもしろい形です。 LLVMでは2025年9月にRFCが出て、 discourse.llvm.org ターゲットの名前を何にするかでけっこう議論がありました。最初はRustに合わせて、vendorの位置にwaliを入れたwasm32-wali-linux-muslが提案されたのですが、「longの大きさが違うABIなのだから、トリプルで区別できるべき」「LLVMでは普通ABIの違いをvendorでは表さない」という意見が出て、最終的に環境の位置に入れたwasm32-linux-muslwaliに落ち着いています。この名前でのPRが2025年10月にマージされました。 llvm-project#162581 clangでこのターゲットを指定すると、linuxとunixに加えてwaliが定義されます。つまりCのプログラムから見ると「Linuxでコンパイルされている」ように見えるので、#ifdef linuxで分岐しているコードがそのままLinuxのほうに入ってくれます。WASIだとlinuxは立たないので、ここも地味に大きい違いだなと思います。 WASIはWALIの上に載せればいい ここまで読むと「じゃあWASIはいらないの?」となりそうですが、論文の主張はそうではありません。 WALIは「カーネルとの境目をできるだけ薄く・低くする」ためのもので、WASIのように「このディレクトリだけ触ってよい」と権限を渡して使うAPIは、WALIの上にwasmのモジュールとして載せればいいと言っています。実際に論文では、WASIの実装であるlibuvwasiをソースを変えずにWALIの上でビルドし、テストが全部通ることを確かめています。 こうすると、WASIの実装が信頼しないといけないエンジンの中から外に出ていくので、エンジンは小さく、安全になる、という理屈です。WASIを置き換えるというより、WASIより下にもう一段の層を作る提案なんですよね。 気になるところ もちろん良いことばかりではありません。 まず、ホストがLinux前提です。WALIのエンジンはLinuxのシステムコールを素通しする作りなので、MacやWindows、ブラウザーではそのままは動きません。リポジトリでも、Linux以外ではDockerで動かす手順になっています。 次に、WASIのバイナリと混ぜられません。longの大きさが違うので、wasm32-wasip1でビルドしたライブラリとWALIでビルドしたものはABIが合わないんですよね。既存のwasmの資産とは別の世界になってしまいます。 安全面の考え方も違います。wasmのプログラムが自分のメモリの外を壊せない、という性質はそのまま残ります。ただ、ファイルやネットワークには、wasmを動かしているプロセスが触れるものなら何でも触れてしまいます。「このフォルダだけ読める」のように権限を絞りたいなら、WASIのような層を上に載せることになります。 なので、「サーバーやエッジの上で、既存のLinuxのソフトウェアをそのままwasmで動かしたい」という用途にはかなり刺さる一方、「信頼できないコードを厳しく閉じ込めたい」「ブラウザーで動かしたい」という用途では、WALIそのものよりも考え方のほうが参考になるかなと思います。 Progateでも考え方は近い ここで少しだけProgateの話をさせてください。 Progateでは、学習者が環境構築をしなくてもコードを書いて動かせるように、いろんな言語やツールをwasmにしてブラウザーの中で動かす取り組みを進めています。以前、Node.jsの学習環境をブラウザーの中で再現した話も書きました。 zenn.dev 言語が増えてくると、冒頭に書いた「WASIに無いもの」に何度もぶつかります。たとえばWASIのpreview1には作業ディレクトリを変えるchdirが無いので、相対パスを解決するところを言語ごとに手当てすることになりました。全画面で動くvimのようなツールは、端末の大きさを取ったり入力の受け取り方を切り替えたりする機能がpreview1に無くて、そこも足す必要がありました。1つずつは小さな話なのですが、言語やツールの数だけ積み重なっていくんですよね。 そこで僕も、WASI独自の形に合わせて言語ごとに手当てを重ねるより、Linuxのシステムコールの形に寄せていく方向で考えています。考え方はWALIとかなり近いです。 ただ、ブラウザーの中には素通しする先のLinuxがないので、WALIをそのまま使うことはできません。また、すでにwasm32-wasip1でビルドした資産がたくさんあるので、longが64ビットになるWALIのABIには合わせていません。WALIの論文を読んだときは「同じことを考えている人がいて、しかも論文と上流のツールチェーンまで持っていっている」と知って、すごく励まされました。 まとめ WALIは、Linuxのシステムコールというすでにある境界をそのままwasmに出す、という割り切った提案でした。importにはSYS_writeのようなシステムコールの名前がそのまま並び、bashのwasmはforkまでimportしています。番号ではなく名前で結ぶことでCPUの違いを吸収していたり、wasm32なのにlongが64ビットだったりと、細かいところを見ていくとかなり考えて作られているのがわかります。研究で終わらず、LLVMとRustにもターゲットとして入っているのもすごいなと思います。 個人的には「新しいAPIを一から設計するより、Linuxに合わせたほうが結局みんな幸せなのでは」という問いかけがすごく刺さりました。WASIの話になるとComponent Modelやasyncの話題が多いですが、こういう「もっと下の層をどうするか」という方向の議論もあるんだなと、しみじみ思います。 Progateでも、ブラウザーの中で既存のプログラムをどこまでそのまま動かせるかは、まだまだ試行錯誤している途中です。また進んだら書きたいと思います! 参考 Arjun Ramesh, Tianshu Huang, Ben L. Titzer, Anthony Rowe. "Empowering WebAssembly with Thin Kernel Interfaces." EuroSys '25. dl.acm.org arXiv版: arxiv.org WALI のリポジトリ: arjunr2/WALI WALI の仕様: wasm-thin-kernel-interfaces.github.io Rust の wasm32-wali-linux-*: doc.rust-lang.org LLVM の RFC: discourse.llvm.org LLVM の PR #162581: llvm-project#162581 WASI 0.3.0: wasi.dev

    1 like
  2. Ускоряем self-hosted Convex в два раза: AOT-компиляция TypeScript-функций в Wasm Обзор Convex – это новый, но набирающий популярность фреймворк для реактивных веб-приложений, который позволяет писать транзакционные квери (queries) и мутации (mutations) на TypeScript с гарантиями консистентности. Вся статья ниже относится к self-hosted версии Convex: convex-backend, а точнее, к моей пропатченной версии оного, так как “официальная” версия не пригодна к работе в продакшене и вряд ли тестировалась на сколь-нибудь нагруженном кейсе. Моя версия работает на виртуальном сервере с 32 ядрами и 64Гб памяти и обрабатывает до 15 тысяч Convex-кверей и мутаций в минуту. Я навайбкодил convex-wasm-compiler, который компилирует Convex-квери и мутации в WebAssembly, и соответствующий патч для convex-backend, позволяющий выполнять скомпилированные в Wasm Convex-функции на файберах Wasmtime прямо в Tokio рантайме convex-backend. TLDR: квери и мутации в среднем работают в 2 раза быстрее, хвосты задержек сократились втрое. Объём памяти, который сервер удерживает под готовые к выполнению окружения (V8-контексты или Wasm-окружения), уменьшился на 80%. До этого для быстрого выполнения Convex-функций приходилось активно кешировать и переиспользовать V8-контексты. Без переиспользования каждая Convex-функция стартует в новом контексте: модули ещё не загружены, JIT не прогрет. Начинается загрузка модулей приложения, необходимых для выполнения функции, и интерпретация её JS-кода. Даже относительно простые Convex-функции в среднем выполняются за 100–150 мс. По мере роста приложения и размера модулей проблема усугубляется. Convex-компоненты помогают сдерживать бесконтрольный рост модулей, но сами добавляют столько накладных расходов и обвязки, что для многих приложений, на мой взгляд, от них больше вреда, чем пользы. Особенно для приложений, которые разрабатывает один человек или пара разработчиков: там нет вопросов разделения ответственности за подсистемы, стабильности API и так далее. Своё отношение к разработке на Convex в стиле «majestic monolith» и к подходу с большим количеством компонентов я описал в посте о своём приложении. Если выполнять Convex-функцию в уже прогретом V8-контексте, где все модули загружены, а код функции скомпилирован в машинный код через TurboFan, время выполнения часто сокращается очень заметно — вплоть до 10 раз. Например, со 100–150 мс до 10–15 мс, при условии, что строки базы данных, которые читает функция, уже закешированы в памяти. Так что переиспользование V8-контекстов было и остаётся необходимым для быстрых и эффективных Convex-приложений. Но у него есть две проблемы. Потребление памяти закешированными V8-контекстами Во-первых, чтобы больше 99% вызовов использовали уже готовые V8-контексты, каждому V8-изоляту в рантайме convex-backend нужно держать не один контекст. Нужен отдельный контекст для каждого модуля — точнее, группы инициализации модулей, — который convex-backend часто назначает на выполнение этому конкретному изоляту. В конфигурации self-hosted convex-backend для моего приложения сейчас используются TinyLFU-кеши размером до 6+1 контекстов (на момент публикации поста о моей конфигурации self-hosted Convex было 5+1). Всего кешируется до 400 контекстов, хотя даже на пике нагрузки одновременно выполняется гораздо меньше 400 Convex-функций. Главная причина такой неэффективности — V8-контексты привязаны к V8-изолятам, и использовать один контекст в разных изолятах нельзя. Каждый V8-контекст занимает около 35 МБ памяти, и эта цифра тоже растёт вместе с приложением: два месяца назад я писал, что в моём приложении один V8-контекст удерживал в среднем 30 МБ. В сумме эти контексты занимают около 14 ГБ памяти на моём сервере convex-backend. Волны инициализации V8-контекстов после деплоя Convex-приложения Вторая проблема переиспользования V8-контекстов в том, что при деплое Convex-приложения часть закешированных контекстов инвалидируется. Это может запустить по всему серверу волну загрузки и компиляции модулей, которая длится около минуты: сотни V8-контекстов приходится создавать и прогревать заново. V8-изоляты не делятся друг с другом оптимизированным машинным кодом. Даже если сериализованный байткод закеширован, инициализация модулей всё равно выполняется отдельно в каждом V8-контексте. Масштаб инвалидации зависит от того, какие модули изменились при деплое. Но если обновились именно ключевые модули с «горячими» Convex-функциями, на которые приходится значительная доля всей серверной нагрузки, большая часть V8-контекстов может пойти под снос и потребовать повторной инициализации. Ведь с каждым таким модулем связано множество V8-контекстов в TinyLFU-кешах разных V8-изолятов. Эта волна повторного прогрева V8-контекстов совпадает с другой волной работы, которую вызывает деплой Convex-приложения: кеш результатов кверей из обновлённых модулей инвалидируется, а квери, на которые у клиентов открыты реактивные подписки, выполняются заново. Это происходит, даже если данные в read set не менялись. То есть серверу приходится не только компилировать код и загружать модули, но ещё и выполнять больше кверей, чем в обычном режиме. На практике из-за этих волн работы после деплоя квери и мутации могут какое-то время выполняться медленнее, а при сильной перегрузке серверу приходится отклонять часть вызовов. Рабочие потоки Tokio рантайма Convex конкурируют с рабочими потоками V8-изолятов и фоновыми потоками V8 Это более общая проблема выполнения Convex-функций в V8-изолятах, не специфичная для переиспользования контекстов. convex-backend ограничивает число воркеров V8-изолятов, которые могут активно выполнять код, через переменную FUNRUN_ISOLATE_ACTIVE_THREADS. Под активным выполнением здесь понимается работа без блокировки на чтении или записи в базу данных и другом I/O. Эту переменную следует выставлять равной CPU-лимиту контейнера convex-backend или чуть ниже. При этом изолировать эти потоки от собственного Tokio рантайма convex-backend и фоновых потоков V8, как правило, не получится без существенного снижения максимальной пропускной способности сервера при длительной нагрузке. Число потоков ОС у Tokio рантайма задаётся переменной RUNTIME_WORKER_THREADS. Фоновые потоки движка V8 отвечают за компиляцию и сборку мусора в V8-контекстах; их число задаётся переменной V8_THREADS. В моей конфигурации сервера CPU-лимит convex-backend составляет 28.5 CPU, FUNRUN_ISOLATE_ACTIVE_THREADS=28, RUNTIME_WORKER_THREADS=28 и V8_THREADS=16. Кроме того, у меня есть несколько пулов выполнения Node, в каждом из которых по 1–2 активных потока. Получается, что теоретически в процессах контейнера с лимитом 28.5 CPU может быть как минимум 75 активных потоков ОС. Такой oversubscription — не катастрофа, но до оптимума далеко. За последнюю неделю, когда Convex-квери и мутации на моём сервере выполнялись в V8, в пятиминутном интервале с наибольшим контеншеном CPU pressure достигал 29%. Разумеется, это пришлось на время деплоя приложения. Практический эффект этого CPU pressure: когда сервер занят — из-за деплоя или просто роста нагрузки, — Convex-функции выполняются медленнее, чем при умеренной нагрузке. И это отдельный эффект, помимо замедления чтения и записи в базу данных, более частого вытеснения записей из кеша результатов кверей, кеша индексов и кеша V8-контекстов. Сама база под Convex, например MySQL или PostgreSQL, при росте транзакционной нагрузки тоже может испытывать повышенный CPU pressure, а её контейнер — попадать под троттлинг. Зачем компилировать TypeScript-функции Convex в Wasm Перенести выполнение горячих функций из V8 в WebAssembly-рантайм вроде Wasmtime, встроенный в Tokio рантайм Convex, — очевидный ответ на описанные проблемы. В начале 2026 года команда Convex объявила, что Wasm-рантайм есть в планах. В августе Wasm-рантайм добавили в репозиторий convex/convex-backend. Однако на следующий день его удалили, и с тех пор он не возвращался. В той реализации использовался интерпретатор QuickJS, скомпилированный в Wasm: сами Convex-функции интерпретировались внутри Wasm, а не компилировались в него. Поэтому реализация была очень медленной и не предназначалась для продакшена. Обычно предполагается — и руководство Convex озвучивало это на разных Q&A-сессиях начиная с августа, — что ради производительности WebAssembly-рантайма разработчики будут писать Convex-функции на компилируемом языке с Wasm-таргетом, прежде всего на Rust. Я начал разрабатывать компилятор обычных Convex-функций, написанных на TypeScript, в байткод WebAssembly с помощью Static Hermes — AOT-компилятора JavaScript с поддержкой Wasm-таргета — в конце июля. Тогда ещё не было известно, что команда Convex собирается ускорить работу над Wasm-рантаймом: до августа Jamie Turner говорил лишь, что тот, скорее всего, появится только в 2027 году. За весь проект, который начался в июле и занял почти три месяца, встраивание Wasmtime в convex-backend оказалось сравнительно небольшой частью работы — возможно, около 10% всех усилий. То есть само по себе добавление Wasm-рантайма в convex-backend — задача относительно простая, и это было мне более-менее понятно с самого начала. Правда, некоторые другие части задачи я сильно недооценил: возможно, я рассчитывал, что на Wasm-рантайм в convex-backend уйдёт 50% усилий. В итоге оказалось только 10%, а весь проект занял в пять раз больше времени, чем я ожидал. Так что вариант переписать части моего Convex-приложения на Rust я рассматривал уже тогда. Но не пошёл по этому пути по нескольким причинам: В приложении было около 350 тысяч строк продакшен-кода только в Convex, причём значительная часть логики была общей с другими частями системы, работавшими на Node.js. Вместе с ними объём продакшен-кода превышал полмиллиона строк. Переписать всё это казалось примерно такой же сложной задачей, как сделать компилятор существующих Convex-функций в WebAssembly. Оглядываясь назад, даже переписывание полумиллиона строк с TypeScript на Rust, вероятно, заняло бы меньше времени, чем трёхмесячная разработка компилятора. Но тогда я этого не предвидел. С другой стороны, у такого проекта наверняка были бы свои существенные риски, о которых я сейчас не знаю, потому что не пробовал. И он точно нарушил бы привычный темп выпуска фич. Меня беспокоило время компиляции Rust и то, насколько оно может замедлить цикл разработки. Эти опасения отчасти подтвердились: с тех пор я полностью написал одну часть приложения на Rust, используя добавленную мной в convex-backend функциональность native resident. Это не Convex action, кверя или мутация, а фоновый Rust-процесс, который деплоится вместе с Convex-приложением и сам вызывает Convex-квери и мутации через loopback. В этой части приложения всего 30 тысяч строк Rust, но каждый раз, когда её код меняется, я ощущаю всю боль медленной компиляции. Мне не хотелось придумывать API для работы с Convex на Rust — аналоги ctx.db.query(), insert(), defineSchema() и так далее, — которые потом вытеснят официальные API команды Convex. В целом я хотел сделать что-то, что могло бы дополнить будущие разработки Convex, а не разойтись с ними или конкурировать. Наконец, делать компилятор мне просто казалось интереснее, чем переписывать приложение на Rust! Устройство convex-wasm-compiler и Wasm-рантайма convex-backend Код Convex-приложения не меняется: это по-прежнему TypeScript с SDK convex-js. Компиляция TypeScript-кода Convex-приложения в Wasm convex-wasm-compiler управляет анализом исходников, сборкой JavaScript-бандлов и нативной компиляцией. С помощью tsrs он находит регистрации Convex-функций и связывает экспортируемые маршруты с исходным кодом, а с помощью esbuild разрешает зависимости и преобразует TypeScript-код приложения в JavaScript-бандлы. Написанный на Rust анализатор использует Oxc, чтобы проверить исходники и граф зависимостей на небезопасное сохранение состояния между запросами и определить, можно ли переиспользовать инициализированные модули. Результаты анализа определяют, что можно допустить к выполнению и как это упаковать; исполняемый код приложения берётся из JavaScript, который генерирует esbuild. Затем компилятор подготавливает этот JavaScript в виде модулей приложения, добавляет bridge к host-операциям Convex и вызывает (Static) Hermes. Здесь и далее «host» — это операции рантайма convex-backend, такие как чтение и запись в базу данных, а «guest» — выполнение функций приложения. Компилятор shermes генерирует C-код, который тулчейн Clang/LLVM из Emscripten компилирует вместе с Hermes рантаймом и нативным адаптером Convex. wasm-ld линкует эти компоненты, а Binaryen оптимизирует полученный Core WebAssembly. Прилинкованный Hermes рантайм обеспечивает внутри Wasm-guest JavaScript-объекты, строки, промисы, исключения и сборку мусора. Асинхронные JS-функции на файберах Wasmtime Static Hermes компилирует асинхронные JavaScript-функции в конечные автоматы, которые могут приостанавливаться и продолжать выполнение с опорой на промисы. Патч для Hermes переносит основную механику промисов в C++: состояние промиса хранится в объектах JSPromise, которыми управляет GC, а реализация Promise на C++ отвечает за resolve, reject и учёт continuations. Это сокращает количество операций с JavaScript-объектами и аллокаций колбэков при асинхронном выполнении. Некоторые высокоуровневые операции Promise по-прежнему используют внутреннюю JavaScript-реализацию Hermes. C+±адаптер Convex, скомпилированный в WebAssembly вместе с Hermes и приложением, содержит цикл, который доводит каждый вызов до завершения. Он вызывает хендлер, обрабатывает очередь микротасок Hermes, выполняя готовые continuations, ждёт чтения и записи в базу данных и других host-операций, а по мере поступления результатов делает resolve или reject соответствующих промисов. Если нужно дождаться асинхронной работы на стороне host, Wasmtime приостанавливает файбер вызова, освобождая рабочий поток Tokio для других задач. Когда результаты готовы, выполнение продолжается в том же файбере. Таким образом, экспортируемая точка входа Wasm возвращает итоговый статус вызова через обычный интерфейс «вызов — возврат»; JavaScript-промисы и continuations остаются внутри guest. Tokio рантайм Convex поллит call_async из Wasmtime, который выполняет точку входа guest в одном файбере Wasmtime. Проверки fuel и epoch обеспечивают соблюдение лимитов выполнения и дают CPU-bound коду внутри guest возможность делать yield. Компиляция Wasm в нативный код и деплой В этой схеме нативная компиляция происходит на машине разработчика или в CI. Компилятор Cranelift из Wasmtime переводит .wasm-модули — приложение, Hermes рантайм и адаптер Convex — в машинный код под целевую архитектуру сервера и конфигурацию Wasmtime. Wasmtime сохраняет этот код и метаданные для его загрузки в .cwasm-файлах. При деплое они отправляются на сервер вместе с переносимыми .wasm-файлами, обычным JavaScript-бандлом Convex и схемой. На сервере convex-backend проверяет, что скомпилированные файлы совместимы с его движком Wasmtime и соответствуют той версии JavaScript-приложения, которая сейчас деплоится. Сохранённые хеши содержимого не позволяют случайно совместить новый JavaScript со старой скомпилированной сборкой. convex-backend загружает скомпилированный код в память и подготавливает изолированные окружения для повторного использования, каждое со своим Hermes рантаймом и JavaScript-состоянием приложения. Инициализация модулей — выполнение кода верхнего уровня, который создаёт объекты модулей и регистрирует экспортируемые функции, — завершается для начального пула окружений ещё до того, как новая версия приложения начинает обслуживать квери и мутации. Позже пул может расти вслед за нагрузкой. Именно эта часть решает описанную выше проблему «Волны инициализации V8-контекстов после деплоя Convex-приложения», характерную для выполнения в V8. Wasm-окружения и управление памятью Wasm-рантайм экономит память по сравнению с V8 за счёт того, что изолированные окружения в пуле совместно используют скомпилированный код и инициализированную память приложения. Эти окружения похожи на V8-изоляты в рантайме Convex, но у них нет выделенных потоков ОС. Их количество ограничено общим бюджетом памяти и жёстким лимитом числа инстансов; число одновременно активных выполнений ограничивается отдельно, исходя из CPU. Каждое окружение управляется Wasmtime Store. В нём есть Hermes рантайм и собственное JavaScript-состояние, и в каждый момент оно обслуживает один вызов Convex-функции. Окружения с одинаковыми скомпилированными модулями совместно используют их машинный код. На Linux convex-backend также сохраняет инициализированную Wasm-память окружения, включая JavaScript-хип, в неизменяемый снапшот в файле. Это происходит после инициализации модулей приложения, но до выполнения в этом окружении первой квери или мутации. Другие окружения мапят этот снапшот с copy-on-write: неизменённые страницы используют общую физическую память, а при записи создаются приватные копии. Каждое окружение заново создаёт собственные таблицы функций и ссылки рантайма, а состояние конкретного вызова Convex-функции хранит отдельно. Перед переиспользованием окружения convex-backend проверяет, что конфигурация и другие зависимости, прочитанные при инициализации, всё ещё актуальны. В сборщик мусора Hermes, Hades, внесён патч, который сохраняет это совместное использование памяти. В данной Wasm-сборке Hades работает в одном потоке внутри guest-вызовов. Подготовленные области хипа исключены из последующих аллокаций и компактизации: это уменьшает количество записей в общую исходную память. При этом сохраняются обычные механизмы обхода графа объектов (tracing), очистки памяти от недостижимых объектов (sweeping) и финализации. В безопасных точках между вызовами сборка мусора и анализ состояния аллокатора находят пустые области nursery и заведомо свободные страницы с данными, чью приватную физическую память host может освободить. Бэкенд учитывает общую физическую память только один раз и резервирует память под приватное состояние и рост каждого окружения. Хуки Wasmtime отслеживают записи и рост памяти, поэтому общий бюджет Wasm-памяти соблюдается как при выполнении вызовов, так и при добавлении окружений в пул. Wasmtime также использует сборщик с отложенным подсчётом ссылок для объектов, которыми владеет сам движок, в том числе перехваченных Wasm-исключений. Это отдельный механизм, не связанный с Hades, который управляет JavaScript-объектами в линейной памяти. Вместе с ограничением числа сохраняемых инстансов и освобождением страниц эти механизмы позволяют переиспользовать инициализированные рантаймы после успешных вызовов, не удерживая бесконечно всю память, которую затрагивали предыдущие выполнения Convex-функций в этом окружении. Wasm vs. V8: результаты замеров производительности В целом среднее время выполнения кверей и мутаций в Wasm сократилось почти на 50% относительно V8: Рантайм Среднее p90 p99 Максимум V8 38.53 мс 62 мс 329 мс 7 463 мс Wasm 19.61 мс 34 мс 139 мс 2 242 мс В среднем каждое Wasm-окружение удерживает 12 МБ полностью приватной памяти, не разделяемой с другими окружениями. Это на 66% меньше, чем 35 МБ у V8-контекста. Кроме того, держать в памяти нужно гораздо меньше Wasm-окружений: 130 против примерно 400 V8-контекстов. Почему V8-контекстов нужно больше, чем одновременно выполняющихся вызовов, я объяснил выше в разделе «Потребление памяти закешированными V8-контекстами». В пятиминутном интервале с наибольшим контеншеном CPU pressure достигал 15%, когда основным рантаймом был Wasm, против 29% для V8. Даже эти 15%, вероятно, отчасти связаны с сохранившимся выполнением в V8. Как я расскажу ниже, я компилирую в Wasm не всё Convex-приложение, а лишь примерно 20% самых горячих модулей, на которые приходится 90% всей нагрузки. Это экономит время компиляции и уменьшает размер артефактов. Остальные 80% модулей по-прежнему выполняются в V8, поэтому для них всё ещё актуальны инвалидация и инициализация контекстов, работа фоновых потоков V8 и так далее. В следующих двух таблицах показано среднее относительное снижение метрик mean/p90/p99/max для отдельных кверей и мутаций в разных группах. Эти средние не взвешены по числу вызовов или суммарному времени выполнения каждой функции. Время выполнения целиком: Группа Среднее p90 p99 Максимум Все квери −57.6% −56.3% −65.3% −82.2% Квери: 20% с наибольшим числом host-операций −39.3% −37.6% −48.4% −82.2% Квери: 20% с наименьшим числом host-операций −69.0% −61.3% −78.0% −84.0% Все мутации −51.5% −33.7% −60.6% −75.1% Мутации: 20% с наибольшим числом host-операций −39.4% −43.6% −48.9% −70.5% Мутации: 20% с наименьшим числом host-операций −51.2% −55.7% −61.5% −70.6% «User» time — оценка времени, которое функции провели непосредственно в guest-рантайме, без ожидания чтения и записи в базу данных и других асинхронных host-операций: Группа Среднее p90 p99 Максимум Все квери −70.6% −66.9% −76.1% −79.8% Квери: 20% с наибольшим числом host-операций −43.5% −41.0% −56.6% −89.0% Квери: 20% с наименьшим числом host-операций −89.5% −82.9% −86.6% −69.8% Все мутации −67.7% −46.5% −72.5% −59.9% Мутации: 20% с наибольшим числом host-операций −41.0% −41.2% −47.7% −39.0% Мутации: 20% с наименьшим числом host-операций −76.9% −78.6% −82.2% −55.0% «20% с наибольшим числом host-операций» и «20% с наименьшим числом host-операций» — это функции, которые в среднем выполняют больше или меньше асинхронных операций, например чтений и записей в базу данных, по сравнению с остальными. Например, функция с единственным ctx.db.get(id) делает одну асинхронную операцию и попадёт в группу «20% с наименьшим числом». А функция, которая в среднем выполняет сотни операций ctx.db, ctx.scheduler и так далее, — в группу «20% с наибольшим числом». Из таблиц видно, что относительное преимущество Wasm перед V8 обычно уменьшается для функций, которые делают очень много асинхронных операций. Возможно, это указывает на слабое место в обработке JS Promise внутри файбера Wasmtime, описанной выше в разделе «Асинхронные JS-функции на файберах Wasmtime». И это несмотря на то, что оптимизации этой части уже уделено много внимания и я не знаю — ладно-ладно, GPT-Astra не знает, — как ускорить её ещё. Другая возможная причина: частые yield из файберов Wasmtime и переключения потока на другие задачи Tokio приводят к тому, что Convex-вызов чаще мигрирует между ядрами CPU, а значит, чаще получает промахи L1-кеша или TLB. А возможно, дело в чём-то ещё. Причина пока не установлена. Преимущество по «user» time тоже не универсально. Есть одна Convex-мутация, у которой среднее user time в Wasm-рантайме на 14% выше, чем в V8, хотя по полному времени выполнения она всё равно быстрее в Wasm. Эта мутация в цикле выполняет численные операции над Float64Array размером более 100 тысяч элементов. Возможно, в V8 для этого есть интринсики или векторизация, которых пока не хватает в пайплайне Hermes → Wasm. Операционные аспекты AOT-компиляции в Wasm Теневое выполнение для постоянного тестирования Я добавил в convex-backend целую сложную подсистему для дифференциального тестирования и теневого выполнения (shadow execution) Convex-функций в рантаймах V8 и Wasm. Она поддерживает основное (primary) и теневое выполнение — в каждой роли может быть как V8, так и Wasm. Заданная доля вызовов, например 5%, случайным образом отбирается для теневого выполнения, после чего система проверяет, что результаты совпадают. Теневые вызовы не блокируют основные. Теневое выполнение работает и для кверей, и для мутаций. В случае мутаций записывается трейс вызовов, меняющих состояние, таких как db.insert(), но сами строки в базе, конечно, не перезаписываются второй раз. Поменять местами основной и теневой рантаймы можно без повторного деплоя приложения. Эта система позволила мне уверенно переключить продакшен на Wasm и найти несколько багов в convex-wasm-compiler. Правда, большинство багов оказалось в самой подсистеме теневого выполнения, сравнения и проверки трейсов. Даже после переключения основного выполнения на Wasm я продолжаю выполнять 5% вызовов в V8 в теневом режиме, чтобы замечать расхождения, если они появятся в будущем. Выборочная компиляция в Wasm И компилятор, и convex-backend поддерживают выборочную компиляцию: в Wasm компилируется и выполняется только часть модулей. Это помогает сократить время компиляции и объём данных, передаваемых при деплое. В своём приложении я выбираю лишь около 20% модулей, на которые приходится более 90% всех вызовов кверей и мутаций. Однако ни объём передачи, ни время компиляции не уменьшаются впятеро: из-за транзитивных зависимостей компилировать и передавать всё равно приходится около 70% всех модулей приложения. Так что, возможно, выигрыш не так уж велик. Время компиляции и объём передачи Выбранные 20% модулей моего Convex-приложения — это 57 модулей и 631 Convex-функция для компиляции в Wasm: 145 кверей и 486 мутаций. Всего компилируется 528 отдельных JS-модулей: 57 выбранных плюс их зависимости. Для этого набора холодная компиляция — когда нужно перекомпилировать большую часть кода или весь код, например после очистки кеша компиляции, — с 16 воркерами AOT-компиляции и лимитом памяти 12 ГБ занимает около 6 минут. Тёплая компиляция, когда часть модулей изменилась, но большинство уже есть в кеше, занимает около 2 минут. До идеала далеко, и я продолжаю над этим работать. Когда я начинал проект, я не ожидал, что именно скорость компиляции, инкрементальность и кеширование окажутся главными инженерными проблемами. Из примерно трёх месяцев работы над проектом AI-агенты суммарно потратили около трёх недель календарного времени исключительно на это направление. При деплое с таким набором скомпилированных модулей на мой self-hosted сервер Convex передаётся около 50 МБ сжатых артефактов. Без Wasm — около 15 МБ, то есть Wasm примерно втрое увеличивает объём данных при деплое. Без выборочной компиляции он, вероятно, был бы примерно в пять раз больше, чем при деплое одного JS-бандла. Эта статья — перевод из моего блога про работу с Convex в продакшене.

  3. The WebAssembly (Wasm) Module Expiry Trap: Tracking Client Edge Runtimes and Binaries As compute moves closer to users, engineering teams and agencies increasingly ship WebAssembly (Wasm) modules to edge platforms such as Cloudflare Workers, Fastly Compute and Vercel Functions on the Edge runtime. Wasm offers near-native performance on compute-heavy work, lets teams use languages like Rust, Go and C, and runs inside a sandbox. It also creates an operational blind spot: a compiled Wasm binary has no expiry date, but the things it depends on do. A .wasm file is an opaque artifact. It is tied to the compiler toolchain that produced it, the compilation target it was built for, the system-interface version it imports, the limits of the plan it is deployed on, and sometimes the signing identity used to approve it. When any of these moves, a module that ran for months can stop building, stop deploying or stop instantiating. Nothing in the binary itself warns you. 1. Anatomy of the Edge Wasm Lifecycle Edge platforms remove server maintenance, but they move that complexity into the build pipeline and the artifact lifecycle. [Source Code (Rust / Go / C)] │ ▼ Compiler toolchain + compilation target [Wasm Binary (.wasm)] │ ▼ Optional: signing / attestation (your policy) [Bundle or package uploaded to the platform] │ ▼ Edge runtime (e.g. workerd, a Wasmtime-based host) [Client execution] The three major platforms handle Wasm differently: | Platform | How Wasm is delivered | Detail worth tracking | | --- | --- | --- | | Cloudflare Workers | A .wasm file is imported into the Worker and instantiated with WebAssembly.instantiate(). Wrangler bundles any module ending in .wasm or .wasm?module. | Worker size is capped at 3 MB (Free) or 10 MB (Paid) after gzip, with a 1-second startup limit. | | Fastly Compute | A package built by the Fastly CLI from your project, using the toolchain pinned in rust-toolchain.toml for Rust. | Fastly validates Rust versions against its SDK before supporting them. | | Vercel Functions | Wasm works with the edge, nodejs and bun runtimes. | On the Edge runtime, WebAssembly.instantiate requires the Wasm source to come from an import statement. | Cloudflare’s limits page also lists 64 MB as the pre-compression bundle limit and notes that larger bundles can slow startup. The Wasm module ships inside the deployment, so a plan or bundle-size change can break a client’s Worker even if no code changed. 2. The Silent Vectors of Failure Vector 1: Toolchain and target drift Compilation targets get renamed and removed, and compiler releases occasionally produce bad output for a given platform. Real examples: - The WASI target rename. Rust 1.78 (May 2024) introduced wasm32-wasip1 as a rename of wasm32-wasi. Rust 1.84 (January 2025) removed the old target entirely, so projects still referencing wasm32-wasi failed after upgrading the compiler. - Fastly’s CLI target. Fastly’s Rust guide says CLI 11.0.0 and later supports only the wasm32-wasip1 target for Rust services, which requires Rust 1.78.0 or higher. - A compiler release that produced broken packages. Fastly’s Rust guide states that Rust 1.91.0 is not compatible with Compute and that 1.91.1 fixed the problem. The Fastly CLI was also updated to refuse builds with 1.91.0. Fastly’s engineering team had confirmed those packages would crash when handling incoming requests. The last case shows the risk of floating toolchains. A pipeline that tracks the stable channel can pick up a compiler release the platform has not validated, and the failure appears at runtime rather than at build time. Fastly’s own guidance is to avoid updating Rust until Fastly releases an SDK that supports the new version. Vector 2: WASI and runtime version drift The system interface that Wasm modules import is still evolving, so modules built against older interface versions need deliberate migration. - WASI 0.3.0 was released on June 11, 2026. It moves async support into the Component Model itself, using built-in stream, future and async func primitives. The wasi:io package, which WASI 0.2 used for async, is replaced by these primitives. - Runtimes follow on their own schedules. The WASI project said Wasmtime 46 would ship WASI 0.3.0 with Component Model async enabled by default. The Wasmtime 46.0.0 release notes confirm that WASI 0.3.0 support and the component-model-async feature are enabled by default, and the release landed on June 22, 2026. - Not every host moves at the same time. Your edge platform’s runtime version, your local test runtime and your toolchain’s WASI target can each be on different interface versions. Cloudflare mitigates part of this with compatibility dates. Its open-source workerd runtime is versioned by date, and the README states that updating workerd will never break your JavaScript code because Workers can pin to an older compatibility date. That promise covers the JavaScript API surface. It does not cover whether your Wasm module’s own imports still match what you built it against, so you still need to test the module itself. Also note that Fastly’s original runtime, Lucet, reached end-of-life and was archived in March 2022. Its maintainers directed users to Wasmtime, which absorbed Lucet’s features. Documentation or blog posts that still refer to Lucet as a current edge runtime are out of date. Vector 3: Signing identities and embedded time-limited material None of the platform documentation reviewed for this article describes mandatory signature checks on uploaded Wasm modules. Signing is usually a policy your own team or a client’s security team adds to the pipeline. Where it exists, it creates its own expiry risk: - Module signatures are a proposal, not a universal standard. The WebAssembly module signatures proposal embeds signatures in the module so a runtime or deploy gate can verify them before instantiation. wasmsign2 is a reference implementation. - Keyless signing changes what “expired” means. Sigstore’s public Fulcio service issues short-lived certificates valid for 10 minutes, so verification has to prove the artifact was signed while the certificate was valid, using a timestamp or transparency-log entry, rather than checking whether the certificate is valid now. A verification step that checks the certificate’s validity at deploy time will reject every keyless-signed artifact. - Key-based signing carries a rotation burden. If your pipeline signs with a long-lived key or certificate and the deploy gate verifies it, an expired or rotated key will block deployments until the pipeline and the gate agree again. - Embedded time-limited material is a self-inflicted trap. If a build embeds an evaluation license or a time-limited credential, the module works until that timestamp passes. Platform documentation does not describe this behavior. It is a design choice, and it is easy to forget when a client engagement ends. Vector 4: Plan tiers, size limits and startup budgets Limits can change what a deployment is allowed to do. Cloudflare’s published limits differ between Free and Paid plans for Worker size (3 MB vs. 10 MB compressed), and both plans allow 1 second of startup time. A Wasm module that grew past the Free-plan cap, or a client account moved between plans, can produce a failed deployment that has nothing to do with your code. Separately, Workers that exceed their CPU time limit return Cloudflare’s Error 1102, which is easy to mistake for a runtime bug if you are not watching resource limits. 3. Best Practices for Mitigating Edge Wasm Drift - Pin compiler toolchains and targets. Avoid stable or latest in compilation stages. Commit a rust-toolchain.toml that names both the exact compiler version and the target: [toolchain] channel = "<version validated for your platform's SDK>" targets = ["wasm32-wasip1"] Upgrade deliberately, after the platform vendor says the new version is supported. - Read release notes for your target and your runtime. Target renames (wasm32-wasi to wasm32-wasip1) and interface changes (WASI 0.2 to 0.3) are announced well in advance. Put the dates in your team’s calendar. - Run synthetic probes against deployed modules. Exercise a cold-start path for each deployed module on a schedule, so a broken package or failed instantiation shows up in your monitoring before a client reports it. - Keep artifacts in a registry with metadata. Store each build with its compiler version, target, WASI version, build date, size and the signing identity used. Cosign can store and sign Wasm modules as OCI artifacts, which gives you a versioned, verifiable record. - Make signature verification time-aware. If you use keyless signing, verify against the signing time evidenced by the transparency log or a timestamp authority rather than the current time. If you use long-lived keys, track their expiry dates like any other certificate. - Canary and roll back. Route a small share of traffic to newly compiled modules, and keep the previous known-good build deployable so a link or instantiation failure can be reversed quickly. - Record limits and plan tier per client. Note the Worker size, startup time and plan tier against each client deployment, and re-check them when the client’s account changes. An inventory template for client edge deployments For each client edge deployment, record: | Field | Why it matters | | --- | --- | | Platform and plan tier | Size and startup limits differ by plan. | | Compiler version and target | Detects drift such as wasm32-wasi removal or an unvalidated Rust release. | | WASI version and runtime version | Detects interface mismatches such as WASI 0.2 vs. 0.3. | | Compatibility date (Cloudflare) | Shows which runtime behavior the Worker is pinned to. | | Signing method and key/certificate expiry | Prevents deploy-gate failures. | | Account owner and billing contact | Ensures someone can act when the platform plan changes. | | Last successful cold-start probe | Shows when the module was last confirmed working. | 4. Conclusion WebAssembly at the edge delivers real speed and language flexibility, but a compiled module is not a write-once file. It depends on a compiler version, a compilation target, a system-interface version, a platform’s limits and, where you use it, a signing identity. Each of these has changed within the last two years, and several changes broke previously working builds. Teams that treat Wasm modules as living components, with pinned toolchains, recorded metadata and regular health probes, can avoid most of these failures and diagnose the rest faster. Further Reading - Cloudflare Workers: Wasm in JavaScript - Cloudflare Workers limits - Fastly: Rust on the Compute platform - Vercel: Using WebAssembly (Wasm) - WASI 0.3 release notes (wasi.dev) - Wasmtime 46.0.0 release notes - Rust: removal of the wasm32-wasi target - WebAssembly module signatures proposal --- Originally published at instarenewal.com

  4. Wasm GC does not offer weak refs or finalizers at this point: github.com/WebAssembly/gc/blob/main/proposals/gc/…

  5. なんか wasm に関する 2018 年の issue を見かけた env, memory みたいな出所が謎の文字列がデファクトスタンダードとして用いられており、ドキュメントのどこにも記載されていない現状 tool-conventions#81

  6. @ico_TC Nice try. WASM doesn't use goto, structured control flow only. Totally doable, but, gonna need a lot of space. Now, if only WASM would accept #796 design#796

    2 likes · 182 views

Achievements

Awards earned by the repositories WebAssembly owns. Select an award to embed its badge.

Gold
21st place finishes
Silver
12nd place finishes
Bronze
03rd place finishes
Awarded repositories
1Owned by WebAssembly now

Repositories

Ordered by gold, then silver, then bronze.

  1. designBest: 1st by stars, 1,272 stars210

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. design
  2. design
  3. design

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.