v-tokyo24 発表資料の原稿. スライド版はあとで公開。
(このままのが読みやすいかも)
mizchi | v-tokyo 26 | 2026
- フリーランス | Node.jsとフロントエンドが専門
- 最近は専ら AI で開発パイプラインを自動化することが専門
- 2020年版: なぜ仮想 DOM / 宣言的 UI という概念が、あのときの俺達の魂を震えさせたのか
- CLINE に全部賭けろ
- MoonBit でバイブコーディング楽しすぎる
mizchi/crater— 自作ブラウザ (WPT css-* 系ほぼ通過)mizchi/vibe-lang— 自作 WebAssembly 言語 (セルフホスト達成)mizchi/kagura— 2D/3D ゲームエンジン (WebGPU Renderer)mizchi/actrun— GitHub Actions 互換タスクランナーbit-vcs/bit— Git 互換 (JS 60k / WASM 340k)
せっかく AI 使うんなら、でかいことをしようぜ
- https://mizchi.github.io/kagura/hacknslash_3d/
- https://mizchi.github.io/kagura/crater_renderer/
- https://moonlight.mizchi.workers.dev/
MoonBit 製フルスタック SSR フレームワーク (WebComponents + SSR たぶん世界初)
https://github.com/mizchi/sol.mbt
moon install mizchi/sol/cmd/sol
sol new myapp
cd myapp && pnpm install
sol dev # http://localhost:7777Demo https://sol-example.mizchi.workers.dev/
- いくら待っても自分が欲しいものが生まれない => 作る
- 軽量ランタイム + island の選択的ハイドレーション
- WebComponents First — Declarative Shadow DOM への SSR 対応
- 型の契約によるセキュリティ境界 — サーバー/クライアントの混在をコンパイルエラーに(後述)
- Island Architecture — 不要な JS は読み込まない
- SSR を制御するにはサーバーとの垂直統合しかない
Rust 風の型システムを持つ関数型言語。JS / WASM / Native にビルド可能。
- TypeScript より関数型の機能が多く、表現力が豊か
- パイプライン
|>, パターンマッチmatch, 代数的データ型, trait
- パイプライン
- Minify フレンドリーな JS を生成 → バンドルサイズが肥大化しない
- 1つのコードから 3つの選択肢:
- JS — 今まで通り npm で配布。エコシステムと共存
- WASM — ポータブルなサンドボックス実行
- Native — 起動速度・スループットを追求
- (そもそも何作っても最初なので)好き勝手作っても怒られにくい
pub async fn handle(route : Route) -> Response raise ServerError {
match route {
Get(path) => path |> load_page |> render_html
Post(_, body) => body |> parse_json |> save_data
NotFound => { status: 404, body: "Not Found" }
}
}|>F#スタイル関数パイプライン- パターンマッチ
- 明示的な例外
function _M0TP25slide7example8Response(param0, param1) {
this.status = param0; this.body = param1;
}
const _M0FP25slide7example19handle_2erecord_2f5
= new _M0TP25slide7example8Response(404, "Not Found");
function _M0FP25slide7example6handle(route) {
switch (route.$tag) {
case 0: return _M0FP25slide7example12render__html(
`<h1>${route._0}</h1>`);
case 1: return _M0FP25slide7example10save__data(route._1);
default: return _M0FP25slide7example19handle_2erecord_2f5;
}
}minify に優しい => MoonBit で書いて npm で配布可能
| 名前 | 役割 | 概要 |
|---|---|---|
| mizchi/signals | リアクティブ基盤 | alien-signals (Vue 3.6で採用) の MoonBit 移植 |
| mizchi/luna | UI ライブラリ | Solid 風 API、9.4KB gzip |
| mizchi/mars | HTTP サーバー | Hono クローン |
| mizchi/sol | フルスタック SSR | Luna + Mars + Island Architecture |
graph RL
Sol["mizchi/sol"] --> Luna["mizchi/luna"]
Sol --> Mars["mizchi/mars"]
Luna --> Signals["mizchi/signals"]
pub fn counter(props : CounterProps) -> DomNode {
let count = @signal.signal(props.initial_count)
div(class="counter", [
text(count),
button(on=events().click(_ => count.update(n => n + 1)), [text("+")]),
])
}- TreeShake 次第で preact より軽量 (~3kb)
- Demo https://luna-examples.mizchi.workers.dev/playground/game/
Hono API を模倣したサーバーフレームワーク
async fn main {
@mars.Server::new()
..get("/", async fn(ctx) { ctx.text("Hello, Mars!") })
..get("/users/:id", async fn(ctx) {
let id = ctx.param("id").unwrap()
ctx.json({ "id": id })
})
..serve(host="127.0.0.1", port=3000)
}- サーバー用の型とクライアント用の型を分離 — 混在するとコンパイルエラー
- サーバーコンポーネントは 最初から async — DB アクセスも自然に書ける
- クライアントは Signal ベース — 必要な部分だけ hydrate Luna + Mars で構成。対応: Node.js / Cloudflare Workers / Native
pub fn routes() -> Array[@sol.SolRoutes] {
[
@sol.with_mw([@mw.logger()], [
@sol.wrap("", root_layout, [
@sol.route("/", home, title="Home"),
@sol.route("/docs/[...slug]", docs_page),
]),
@sol.api_get("/api/health", api_health),
]),
]
}async fn user_page(props : @sol.PageProps) -> @server_dom.ServerNode {
@server_dom.ServerNode::async_(fn() {
let id = props.params["id"].unwrap()
let user = db_find_user(id) // async DB アクセス
@luna.fragment([
h1([text(user.name)]),
@server_dom.client(@types.profile(user.to_props()), [
div([text("Loading...")]), // SSR フォールバック
]),
])
})
}- 2025末: Next.js RSC にリモートコード実行 (RCE) 脆弱性
- クライアントコードを部分的にサーバーでレンダリング → 境界が曖昧
"use client"/"use server"— ディレクティブ依存は人間のミスを防げない
graph LR
SN["ServerNode"] --> CR["ComponentRef (JSON)"]
CR --> DN["DomNode"]
- サーバーとクライアントで 型が違う → 混在するとコンパイルエラー
- 唯一の接点は
ComponentRef[T]— Props を JSON で橋渡し
戻り値 ServerNode — イベントハンドラが型的に書けない
async fn home(_props : @sol.PageProps) -> @server_dom.ServerNode {
@server_dom.ServerNode::async_(fn() {
let props : @types.CounterProps = { initial_count: 42 }
@luna.fragment([
h1([text("Welcome")]),
// ↓ クライアント Island を埋め込む
@server_dom.client(@types.counter(props), [
div(class="counter", [text("42")]), // SSR フォールバック
]),
])
})
}// サーバーとクライアントで共有する Props 型
pub(all) struct CounterProps {
initial_count : Int
} derive(ToJson, FromJson) // JSON 変換を自動導出する trait
// クライアント側: Props を受け取って DomNode を返す
pub fn counter(props : CounterProps) -> DomNode {
let count = @signal.signal(props.initial_count)
div(class="counter", [
text_of(count),
button(on=events().click(_ => count.update(n => n + 1)), [text("+")]),
])
}Props は ToJson + FromJson を実装した型のみ → 関数やコールバックは渡せない
graph LR
SSR["SSR"] --> HTML["HTML"]
HTML --> Display["静的表示"]
Display --> Loader["loader.js"]
Loader --> Hydrate["hydrate"]
- サーバーが完全な HTML を返す (JS なしで表示可能)
loader.jsがluna:trigger条件に応じて段階的に hydrate- 不要な Island の JS は読み込まない
SSR が生成する HTML:
<div luna:url="/static/counter.js"
luna:trigger="visible"
luna:state='{"initial_count":42}'>
<!-- SSR 済み HTML (JS なしで表示される) -->
<div class="counter">
<span class="count-display">42</span>
<button class="inc">+</button>
</div>
</div>luna:url = JS モジュール, luna:trigger = hydration 条件, luna:state = Props JSON
| trigger | 発火 | ユースケース |
|---|---|---|
load |
即座 | ファーストビュー必須 |
idle |
requestIdleCallback | 非優先 |
visible |
IntersectionObserver | スクロールで見えたら |
media:(query) |
matchMedia | モバイルのみ等 |
不要な Island は JS を読み込みすらしない
Declarative Shadow DOM で WC SSR に対応済み
| 項目 | Plain DOM | WC | 劣化 |
|---|---|---|---|
| イベントバブリング | 451K ops/s | 39K ops/s | 11.7x |
| 初期化 (100個) | 3,425 ops/s | 972 ops/s | 3.5x |
| Context (10階層) | — | — | 10-12x |
末端の装飾拡張には有効、データ伝搬には使わない
クロスコンパイル: SSR の一致を言語レベルで保証
| Metric | Native | JS (Node.js) | Ratio |
|---|---|---|---|
| Requests/sec | 12,376 | 16,820 | 73.6% |
| Avg latency | 768us | 556us | 1.38x slower |
- Native コンパイラは発展途上
- 現時点では V8 + libuv が優秀
- 型が厳密 → コンパイラがガイド → AI が書きやすい
- ライブラリ不足は AI で踏み倒す
- 「テストコード移植して出力一致まで実装して」
- 「
moon benchでベンチマーク書いて改善して」
AI が書きやすい言語 = 型が厳密でコンパイラがガイドしてくれる言語
- Sol のドッグフーディング
- API は予告なく変わります
- MoonBit だけではない JS API 層を追加
- 部分的に Vite Environment 化(WIP)
MoonBit 最高