コンテンツにスキップ

なぜ Popcorn Web なのか

このプロジェクトがやろうとしていることは2つあります。

1つは、素直に書いた実装が、そのまま速くて安全な実装になること。当たり前の net/http ハンドラを書けば、本来なら後から足しに戻ることになる性能と安全性が付いてくる、 という状態です。

もう1つは、Go におけるフロントエンド開発の地位を上げること。いまの Go は API を 書く場所で、フロントエンドは JavaScript のエコシステムのもの、という住み分けになって います。この分け方は自然法則ではありません。このフレームワークは、それに対する反論です。

素直な実装と、それが黙って手に入れるもの

Section titled “素直な実装と、それが黙って手に入れるもの”

アプリケーションは Go のまま読めます。境界にあるのは net/http のハンドラ、 context.Context、通常のミドルウェア、database/sql です。だからハンドラは標準の ミドルウェアで包めますし、別の http.Handler と同じ mux に載せられ、テストには httptest が使えます。フレームワークの採用と同時に、独自のリクエストモデルまで コードベースへ持ち込む必要はありません。

Go がすでに持っているインターフェースの上に建てる

Section titled “Go がすでに持っているインターフェースの上に建てる”

ハンドラは http.HandlerFunc で、それを包むのは Go のコードベースがすでに書いている func(http.Handler) http.Handler です。クエリの結果は database/sql を通って返ります。 リクエストスコープの値は、Go が 1.7 から持っている context.Context に乗ります。 ルーティングは、Go 1.22 がメソッドとパスパラメータの照合を教えた http.ServeMux です。 GET /users/{id} はこのフレームワークが発明した記法ではなく、標準ライブラリのパターン そのものです。

プロファイリングの道具も、ミドルウェアの慣習も、すでに go.mod にあるライブラリも、 そのおかげで動き続けます。ハンドラが入ってきたときと同じ手軽さでこのフレームワークから 出ていけるのも、同じ理由です。

別の形にしたものもいくつかあり、理由はどれも1つ、2つ目のトランスポートです。fasthttp には http.ResponseWriter も自前のルーティングもありません。だから本来ならそれを経由する読み取りや 制御——r.PathValuehttp.NewResponseController——は pw の呼び出しになっています。 pw.PathValuepw.QueryValue、そして Stream.Write が代わりに行う flush です。これらを フレームワーク経由にしてあることが、1つのハンドラのソースをどちらのビルドでも コンパイルできる理由です。

コード生成、reflect フリー、そして TinyGo

Section titled “コード生成、reflect フリー、そして TinyGo”

pw generate はテンプレート、SQL、リクエストバインディング、レスポンス型、設定宣言を 型付きの Go に変換します。クエリ結果の型が変わった、テンプレートの引数を渡し忘れた。 そうした不一致は本番のリクエストではなく生成時かコンパイル時に止まりますし、実行時に リフレクションでその形を発見し直すこともありません。

いちばんはっきり出るのが描画です。同じ在庫テーブルを2通り——起動時に解析する html/template と、型付きコンポーネント——で書き、同じバイト列を出しました。Go 1.26.5、 Apple M3、15回の中央値です。

行数 html/template 生成コード
1 2.55 µs · 1032 B · 41 allocs 393 ns · 560 B · 5 allocs
50 78.5 µs · 26.1 KiB · 1282 allocs 6.82 µs · 611 B · 21 allocs
500 780 µs · 259 KiB · 12 956 allocs 70.4 µs · 2.14 KiB · 472 allocs

効いてくるのはメモリの列です。50行のページが 26 KiB ではなく 611 バイトで済み、 確保しなかったものは後から回収されることもありません。回収のコストは本来、たまたま そのとき走っていたリクエストに請求されるものです。

リフレクションを外したことには、もう1つ帰結があります。このプロジェクトが賭けている のはそちらです。WebAssembly がサーバコードの配備先として当たり前になるなら、そこへ 連れて行くコンパイラは TinyGo です。そして html/template はそこでは動きません。 コンパイルは通り、パッケージ初期化の時点で、TinyGo が実装していないリフレクションに 当たって panic します。1バイトも描画される前です。生成されたコンポーネントには リフレクションする対象が無いので、ただ動きます。

Go のサーバーでよく使われるパッケージの多くが、同じ問題を抱えています。crypto/tls、Go 1.22 の ServeMuxaws-sdk-go-v2、Google のクライアントライブラリ。TinyGo を劣化モードとして 扱うのではなく、依存のほうを作り直しました。 tinygodriver がホストのネットワーク ドライバ、OS の TLS スタックを使う HTTPS、SQLite・PostgreSQL・MySQL の database/sql ドライバ、S3・DynamoDB・Datastore のクライアントを供給します。1人が背負う量としては 多すぎますが、AI の支援があって現実的になりました。

独自ルールではなく標準に従う

Section titled “独自ルールではなく標準に従う”

ワイヤに出るものは仕様が言うとおりの形なので、向こう側にいる人も機械も、読み方を すでに知っています。エラーは RFC 9457 の problem ドキュメント、レート リミットは Retry-After、非推奨は RFC 9745 と RFC 8594、ログインは OpenID Connect と WebAuthn、トレースは W3C Trace Context、API は OpenAPI 3.1 で自分を説明します。 ウェブ標準に一覧があり、同じくらい有用なこととして、 意図的に採用しなかったものとその理由も並んでいます。

シンプルな実装を、裸の実装にしない

Section titled “シンプルな実装を、裸の実装にしない”

ハンドラが短いのは良いことです。ただし、認証、レスポンスポリシー、圧縮、可観測性、 設定の検査を後回しにした結果として短いのでは困ります。それらは標準の経路に置いてあり、 小さなアプリケーションの周りに別のフレームワークを組まなくても使えます。

  • フェデレーテッドログインとパスキーが、外部の アイデンティティと、フィッシングに強い再ログイン手段を提供します。
  • セキュリティレスポンスヘッダは既定でブラウザの ポリシーを定めます。圧縮をプロキシではなくアプリケーションが担当するなら、スイッチ ひとつでネゴシエーション付きのレスポンス圧縮を 有効にできます。
  • 静的コード解析が、すでに書いたハンドラ、バインディング、レスポンス呼び出し、コメント から OpenAPI 3.1 ドキュメントを生成します。
  • OpenTelemetry 連携が構造化ログとトレースを送出し、 フレームワークのスパンが、アプリケーション固有のタイマーを書かずに、リクエスト、 レンダリング、データベース処理を見えるようにします。
  • 型付きのアプリケーション設定は環境変数を受け取り、 pw doctorは指定した環境での最終的な設定を解決して、欠落、 矛盾、危険な値をデプロイ前に報告します。

圧縮をプロキシに任せるか、どの IdP を信頼するかといった判断まで奪うわけではありません。 変わるのは出発点です。素直に実装するだけで、安全性、通信性能、可観測性、そして 運用の使い勝手が自然に引き上げられます。

フロントエンドも Go の仕事にする

Section titled “フロントエンドも Go の仕事にする”

いまの Go のウェブアプリケーションはどこで終わるかと聞けば、たいていの答えは「JSON の 境界で」です。テンプレートも、スタイリングも、インタラクションも、サイトの構造を記述する ルーティングも JavaScript のエコシステムへ移り、Go には画面を持たない部分が残りました。

この線引きは疑う価値があります。理由が技術的というより歴史的だからです。 html/template はビルド時ではなく描画時に失敗し、コンポーネントのモデルを持たず、 フィールド名の打ち間違いを見つけられません。型が付いて合成できるフロントエンドが欲しい チームは、Go にそれがなかったため、同じ機能を備えた別のエコシステムを選びました。

代わりにここにあるのは、次のものです。

  • 型付きパラメータを持つコンポーネント。 .pw.html は Go にコンパイルされます。パラメータの 名前を変えればハンドラがコンパイルを通らなくなり、フィールド名を打ち間違えれば、 生成が位置つきでそう言います。
  • コンポーネントに属するスタイル。 コンポーネント内のスコープ付き <style> は コンパイルされて名前空間が付きます。Tailwind CSSpw add tailwind 1つで、後からでも入ります。
  • いちばん遅いクエリを待たない描画。 非同期レンダリングはまだ読み込み中の境界を 避けてページをストリーミングし、 ライブレンダリングは読者がページを開いている あいだ、領域を更新し続けます。
  • ディレクトリがそのままルート。 探索型ルーティングが読むのは ファイルシステムで、それと同期させ続ける登録ではありません。
  • 変わったところだけを差し替えるナビゲーション。 部分更新は同一オリジンのリンクに対して、 マークアップが変わった領域だけを返します。それを可能にしているのは、再利用のために すでに書いたレイアウトのチェインです。

このうちいくつかは JavaScript のエコシステムが先に形にしたもので、その借りは認めます。 一部は元ネタより先へ行っています。非同期とライブはテンプレート上では同じ await 節で、 2つ目の構文ではなく宣言のほうが区別を持ちます。部分更新にクライアント側の状態が要らない のは、境界がサーバの描画するレイアウトそのものだからです。

そしてこのどれもクライアントサイドフレームワークではありません。読者が受け取るのは 最初の1バイトから最後までサーバが描画した HTML で、関わるブラウザ側のコードは、 出来上がったマークアップを所定の位置へ運ぶ小さなモジュール1つだけです。

サーバだけで描くという選択は、時代に逆行しているように見えるかもしれません。 ところが、ウェブフロントエンドのエコシステムを牽引してきた当の Next.js と React が、 ここ数年サーバへ回帰しています。サーバコンポーネントによる SSR を大々的に押し出し、 ブラウザへ送るコードのほうはむしろ削ろうとしています。クライアントコンポーネントの エコシステムを Go で作り直すなら、得るものより困難のほうが大きいでしょう。 けれどサーバコンポーネントなら、Go の土俵です。サーバ側で HTML を組み立てて送る 仕事に、より省エネで高速な実装で挑めるはずだと考えました。

JavaScript のエコシステムは生産性に貪欲で、Go のエコシステムは安定性を大切にして きました。Popcorn Web が目指すのは、その中間ではありません。Go の安定した境界と 予測しやすい運用を保ったまま両者の距離を詰め、その先では JavaScript 界隈が築いた 生産性の水準を追い越すことです。

はじめの一歩は、1章目で動くプロジェクトができます。 ビルドターゲットごとの数字は——fasthttp のものも含めて—— パフォーマンスにあり、切り替えたときに実際に 何が変わるかもそこにあります。