コンテンツにスキップ

Popcorn Web

net/http の上で動くサーバーレンダリングのウェブアプリケーション。型付きテンプレートと型付き SQL を事前生成します。

このフレームワークの狙いは 2 つです。ひとつは、素直な実装がそのまま速くて安全な 実装になること。当たり前の net/http ハンドラを書けば、本来なら後から足しに戻る はずだった性能とセキュリティが、最初から付いてきます。もうひとつは、Web フロントエンドを Go で作る価値のあるものにすること。JSON の境界で JavaScript エコシステムへ渡してしまっていた半分を、Go の側に取り戻します。

どちらも、Go がすでに提供している処理単位、1 つの http.Handler の上に乗ります。 リクエストをバインドし、アプリケーションロジックを実行し、通常のナビゲーションと フォームのセマンティクスで完全なレスポンスを書き出す。コンポーネントグラフも パッチプロトコルもハイドレーションもブラウザランタイムもありません。それが、 成果物を小さく実用的な TinyGo ターゲットに保っている理由でもあります。

Go のエコシステムを尊重

ルーティングは net/httpServeMux そのもの。パターンもワイルドカードも 優先順位も同じで、ハンドラは素の http.HandlerFunc です。手元のミドルウェアも httptest のテストも http.Handler も、そのまま動きます。

1 コマンド、その先は型付きの Go

pw init がハンドラ、型付きページ、ドキュメントシェル、SQL、マイグレーション、 エラーページ、Devbox まで含む完全なプロジェクトを作り、pw dev が監視します。 その裏で pw generate が、テンプレート、SQL、リクエストバインディング、OpenAPI を型付きの Go へコンパイルします。テンプレート引数の改名も、結果型と食い違った SELECT も、ビルドエラーです。リクエストのたびにリフレクションで形を探し直す ことがないので、同じコードが TinyGo でも動きます。

フロントエンドも Go で書く

.pw.html は、型付きパラメータとスコープ付き スタイルを持つコンポーネントへコンパイルされます。Tailwind は pw add tailwind ひとつ、ルートはディレクトリで表せます。非同期レンダリングと ライブレンダリングはまだ返ってこないクエリを迂回してページを流し始め、同一 オリジンのリンクにはマークアップが変わった領域だけを返す。すべてサーバー レンダリングの HTML です。

最新の Web 標準がプリセット

検証済みセキュリティヘッダと CSRF、WebAuthn パスキー、ネゴシエーション付きの zstd と gzip 圧縮、SQL 1 文まで届く OpenTelemetry のトレース。どれも設定の スイッチひとつで、自分で積み上げる依存関係ではありません。

後からより高度なインタラクションが必要になっても、オプトインのサーバー駆動 UI レイヤーが同じ net/http の土台を置き換えずに拡張します。アプリケーションがその レイヤーをインポートしなければ、コストも発生しません。

同じ Todo アプリを二通りで測る

Section titled “同じ Todo アプリを二通りで測る”

examples/todo には、ひとつの PostgreSQL テーブルに対して二通りに書いた Todo リストが入っています。 片方は net/httphtml/templateencoding/jsonpgx、もう片方は Popcorn Web。 ルートは同じ 5 本、行も同じで、JSON の中身が食い違ったら計測ハーネスが測るのを 拒否します。

まずスループット。同じ 50 行に対して k6 を 20 VU で当て、交互に 5 パス走らせた 中央値です。フレームワーク側の結果は、セッションをどこに置くかでほぼ決まります。

requests/s HTML p95
net/http + pgx 約 14,700 3.15〜3.39 ms
Popcorn Web・cookie セッション 約 13,700 2.90〜3.21 ms
Popcorn Web・サーバー側セッション 約 15,800 2.46〜2.66 ms

下 2 行はアプリケーションのコードが同一で、最初のバイトはどちらもフレームワークが 速い。cookie セッションはレコード全体を毎リクエスト 2 回線に載せます。 Set-Cookie が 631 バイト対 299 バイト。スループットを食っているのはこれで、 AES-GCM の封印ではありません(暗号処理は実測 0.5 µs)。

1 リクエストあたりの CPU は 203 µs 対ベースライン 221 µs。しかもフレームワーク側は セッション、CSRF チェック、セキュリティヘッダ、リクエスト ID という、手書き版が 持たない仕事もこなしたうえでの数字です。ミドルウェア鎖全体で 2.7 µs、 ループバックのクエリ 1 本が 37 µsパフォーマンスに全内訳と、 チューニングすべき残り 4 項目があります。

これだけの仕事をするコードは、当然バイナリに入っています。増えるとしたらそこです。 Go 1.26.5 と TinyGo 0.41.1、darwin/arm64。

ビルド net/http + pgx Popcorn Web
ホスト go build 18.98 MB 19.66 MB
ホスト -ldflags="-s -w" 13.06 MB 13.57 MB
TinyGo ネイティブ ビルド不可 6.13 MB
TinyGo ネイティブ -no-debug ビルド不可 6.13 MB
TinyGo wasip1 ビルド不可 ビルド不可

値札はストリップ済みの行です。セッション、CSRF、セキュリティヘッダ、 ネゴシエーション付き圧縮、SQL 1 文まで届く OpenTelemetry、生成されたテンプレートと 生成されたクエリ。それらをひとつも持たないサービスに対して、合計 0.51 MB、 3.9% 増。上のスループットを出していたのは、この差分込みのバイナリです。

TinyGo の行に来ると、表はもう比較ではなくなります。pgx はそもそもコンパイルが 通りません。tls.Conntls.X509KeyPairnet.Resolver を使っていて、TinyGo には どれもないからです。この列が存在するのはフレームワークが作り直したドライバ群の おかげで、しかもホストのストリップ済みビルドに対して半分以下になります。

wasip1 にはどちらも届きませんが、フレームワーク側の理由は「未実装」ではなく プラットフォームの制約です。PostgreSQL エンジンは TinyGo のスレッドスケジューラ を要求します。協調スケジューラだとブロッキングソケット呼び出しがランタイム全体を 握ってしまい、クエリがコンテキストの期限を超えても黙って返るからです。WASI preview 1 にスレッドはないので、エンジンは意図的なコンパイルエラーで拒否します。 その識別子の名前自体が対処法になっています。

なぜ Popcorn Web なのかでは、2 つの狙いをそれぞれ 最後まで書いています。素直な実装が何を無料で受け取るのか、そしてフロントエンドの 仕事が Go に属するには何が必要なのか。