📝

エッジランタイムを考える

2024/12/26に公開

はじめに

昨今CDNのようなエッジでJSを動かす環境(以下エッジランタイムと書く。エッジコンピューティング、エッジでJSを動かす系、エッジサイドフロントエンドなど色々言われているが統一されていない。)をよく目にします。フロントエンドの視点から活用事例をまとめてみたので備忘として残しておきます。

このページの構成

代表的なエッジランタイムであるcloudflare workersについて掘り下げていきます。cloudflare workers の仕組みや特徴、最近のアップデートに触れます。
最後にエッジランタイムを使ったフロントの構成についてあれこれ考えてみたいと思います。

エッジランタイムとは

エッジランタイムの定義はcloudflareの定義を拝借すると以下のようになります。

CDNエッジサーバーは、ネットワークの論理的な端または「エッジ」に存在するコンピューターです。多くの場合、エッジサーバーは別々のネットワーク間の接続として機能します。CDNエッジサーバーの主な目的は、リクエスト元のクライアント機械のできるだけ近くにコンテンツを保存することです。これにより、レイテンシーが短縮され、ページ読み込み時間が改善されます。

従来のCDNはWeb サイトのコンテンツ配信を高速にすべく、地理的に近いキャッシュサーバーを挟むことで効率よくユーザーに配信するためのネットワークことを指しました。
エッジランタイムはCDN のエッジサーバーでアプリケーションを実行することで静的コンテンツ配信だけでなく動的なサーバーとして利用できるサービスを指します。

主な用途としてはA/Bテストのコンテンツ出し分け、HTTPヘッダーの付け替え、特定のURLのredirectなど前段で処理しておきたいこと全般になります。

さてエッジランタイムは種々のベンダーが開発しており、

  • cloudflare workers
  • AWS cloudfront functions、lamda@edge
  • netlify edge functions
  • vercel edge functions (中身はcloudflare workers)
    などなど多くのサービスが存在します。

今回はその中でも最も覇権を握っていそうなcloudflare workersを一例として取り上げてみようと思います。

cloudflare workersについて

cloudflare workersの特徴

cloudflareは世界130ヶ所にデータセンターを持っており、日本にも東京、大阪、福岡、那覇の4か所にあります。おそらくcloudflare workersはそのサーバー上で動いているので距離的に近い場所でサーバー処理の実行ができます。

cloudflare workersの内部ではJavaScript実行エンジンであるV8が動いています。さらにV8のisolate機能にを利用し、エッジ上で大量にIsolateを動かすことで、コンテナよりも軽量な分離環境でコードを実行できます。
V8 Isolateとは実行環境のインスタンスのことを指し、基本的に1 Isolateにつき1スレッドが対応します。
JSはもちろん、wasm、最近はPyodideを用いてpythonまで実行可能になっています。

フレームワークとしてはHonoがもともとcloudflare workers用に作られ始めたということもあり相性が良いです。
またエッジで動くのが強みのRemixとの相性も良いです。RemixはWeb 標準APIに基づいているため実行環境を選ばないという特徴があり、workersで動かす場合にはNextJSよりも選ばれることが多い印象です。

他のcloudflare workersの特徴としては、その他cloudflareサービスとの親和性があります。cloudflareはworkersの他にcloudflare KV(キャッシュ)、R2(ストレージ)、D1(DB)などがあり、Bindingsという機能でworkersから呼び出すことができます。やり方は簡単でworkersの設定ファイル(waranger.toml)に以下を記載するだけで、ソースからenv.XXXという形で呼び出すことができます。

[[services]]
binding = "MY_SERVICE"
service = "my-service"

最近だとworker aiというサービスも出てきており、workersからLLMや画像生成のAPIを呼び出すことができるようです。

メリット・デメリット

メリットはやはりその安さです。cloudflare workersには無料枠(1日10万リクエスト)があり、アイドル時間の課金はありません。
そして前述の通りv8 isolateを使っているためコールドスタートがなく起動が非常に高速です。これはlamda@edgeなどと比べたときの優位な点になります。
cloudflare KV、D1など周辺のサービスが充実しており、親和性が良いサービスで一括で構築できることもcloudflareならではの利点でしょう。

デメリットも3つほど挙げます。

  • (cloudflare workersに限らずエッジランタイム全体のデメリットですが)ツール周りの癖があるかなと思いました。
    他にもcloudflareのサービスを使っているのであればまだ許容できますが、cloudflareをCLIで触ろうとするとWranglerというツールを使い、その設定ファイルを用意しないといけません。若干特有の癖になれる必要があり、Dockerfileを書いてインスタンスにデプロイする方がやはり汎用的でポータビリティは高いかなと思います。

  • 厳しいリソース制限もデメリットとしてあげられます。無料プランではメモリ128MB、CPUの実行時間は10msの上限が設けられています。そのため利用用途としては小規模な処理にとどまるかと思います。

  • 三つ目はNodeJSとは無関係にv8上で実行環境を提供したので一部のNodeJS APIと互換性がないという点です。
    不便ではありますが、昨年今年とアップデートが入り一部のNode.js APIと互換性を持たせられるようになりました。

cloudflare workersの実行ランタイムはworkerdという名前でOSS化されています
https://blog.cloudflare.com/workerd-open-source-workers-runtime/

エッジランタイムとフロントの構成いろいろ

フロントエンドの観点からエッジランタイムの利用用途を探っていきます。

SSR+エッジランタイム

単純ですがエッジでSSRしてやればば速いだろうという考え方です。Cloudflare workersでは前述の通りNextJSのサポートが進んでいることから、NextJSを使ってもこのエッジでSSRは比較的容易に実装できるようになりました。当然Remixでも可能です。
さて、確かにエッジでSSRするだけで速くなりそうかもと思う反面、2点ほど問題があります。

  • レンダリング、CPU消費しすぎ問題
    cloudflare workersはCPU実行時間が10ms、メモリ128MBというリソース制限があります。他のエッジランタイムでも性質上リソース上限は設けられると考えた方が良さそうです。
    一方、サーバーサイドでレンダリングするということはそこそこのCPUリソースを消費してしまうため、大規模なアプリになるとリソースが枯渇してしまいます。

  • そもそもエッジでレンダリングしてもあまり速くない問題
    意気揚々とエッジにデプロイしたものの、エッジでSSRしたものはオリジンサーバー側でレンダリングした時と比べてもあまり速くないと言われています。結局DBなどにアクセスするのであれば、SSRする場所はクライアントではなくデータの近くにあったほうが良いというわけですね。
    このことはVercelの開発者のツイートでも話題になりました。

ということで結局単純にSSRするだけであればあえてエッジを使わなくて良さそうかなーという感触です。

ISR+エッジランタイム

ISR(Incremental Static Regeneration)はビルド時に静的サイトを生成(SSG)し、その後リクエストの際は
・一定の期間は静的ファイルを返す
・一定の期間が過ぎた場合は古いファイルを返しつつ、裏で新たにファイルを生成しキャッシュしておく。その後のリクエストでは新しく生成した方のファイルを返すという手法です。

Nextjsで書いたものをVercelにデプロイすることで簡単に実現できるのですが、他のデプロイを指定した場合には実現に手間がかかりました。
エッジランタイムに上記の機構、つまりキャッシュのファイルが古いかどうかを判断し、freshであればキャッシュの静的ファイルを返す、古ければオリジンに問い合わせるという機構を備えることが容易にできます。(詳しくはこちらがとても参考になりました、)

エッジとキャッシュを用いることで高速にレスポンスを返すことができるのですが、リアルタイムで最新の情報を返すことはできなくなります。そのためユースケースとしては定期的にしか情報が更新されず、あまり整合性が求められないようなブログ記事やニュースサイトなどになりそうです。

PPR+エッジランタイム

ナウでヤングでトレンディな方法です。
PPRはNextJSが実装中のレンダリング手法です。
Suspenceを境界に、静的なコンポーネントはあらかじめレンダリングしたものが配信され、動的なコンポーネントについてはstreamで配信されるというものでした。(多分)

先ほどのSSRの話を踏まえると、静的なコンポーネントについては逐一オリジンに問い合わせるよりエッジで返却してもらい、SSRされるコンポーネントはオリジン側で作るというのが効率が良さそうです。

PPRはNextJSの機能なので、おそらくVercelにデプロイするとよしなに最適化してくれるはずです。しかしvercel以外の環境でも、もっというとNextJSを使わなくても静的なコンポーネントに関してはCDNで返すような方式を取りたいものです。

先日(experimentalではありすが)Reactから、このようなニーズに応えるprerender APIが発表されました。

https://github.com/facebook/react/blob/main/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js#L67

こちらを使うとvercelなしでエッジ+PPRできそうなのですが、解読に苦戦しているので動かしてみた記事はまた後日...
(中身としてはsuspenceを検知して静的な部分のHTMLフレームを生成し、その後resumeToPipeableStream()関数を用いてサーバー側でのレンダリングを再開しているように見えます。)

おまけ

AstroというフレームワークでPPRと似たような構成であるServer Islandsというものが発表されています。PPRと同じく静的な部分はあらかじめレンダリングしておき、それ以外の部分についてはサーバーサイドでレンダリングされます。その判別は<Component server:defer /> のように末尾にディレクティブを記載したコンポーネントか否かです。

PPRとの大きな違いは、PPRはsteraming HTMLで一回のリクエストで「事前レンダリングされたHTML+SSRされた部品」をレスポンスします。一方でServer Islandsでは静的なHTML、動的なコンポーネントではコンポーネントごとそれぞれ別のリクエストが飛びます。

PPRと比べてHTTPリクエストが増えるという欠点はあるのですが、PPRのところで記載したサーバー側でのトリッキーな実装は不要で静的な部分はシンプルにCDNにキャッシュが可能になり、全体の構成としてはシンプルになるというメリットがあります。

まとめ

cloudflare workersの簡単な紹介と、エッジを使った時のフロント構成をまとめました。時代の流れとしてフロントのレンダリング手法とインフラ構成が密接に関わっていくように感じました。

今後どれだけエッジコンピューティングが流行り、技術がいい意味で枯れるかは未知数です。とはいえ小さいアプリを触る分にはお手軽・安価にデプロイできたり高速化できるので引き続き追っていきたいと思います。

参考

https://www.cloudflare.com/ja-jp/learning/cdn/glossary/edge-server/ https://blog.cloudflare.com/builder-day-2024-announcements/
https://yusukebe.com/posts/2022/dcs/
https://zenn.dev/sumiren/articles/8156bab8c95fcf
https://sunilpai.dev/posts/ppr-for-everyone/

Discussion