はじめに
2017年9月、CloudflareはCloudflare Workersを発表し、エッジネットワーク上でJavaScriptを実行する機能を提供しました。これは、コードを世界100カ所以上のエッジネットワーク全体に約30秒で展開できることを意味します。このテクノロジーにより、世界中のどこにいてもユーザーに近い場所でアプリケーションを記述することに集中できます(約50msのレイテンシ)。
ワーカーのランタイムはNode.jsやブラウザとは異なり、Google Chromeによって開発されたJavaScriptエンジンであるV8を使用してコードを実行します。これまでは、プラットフォームで実行できるのは、パフォーマンスを向上させたり、リクエストヘッダーに基づいてロジックを追加したりするために、サーバーにアクセスする前にエッジで実行される小さなスクリプトだけでした。
2020年11月、Nuxt 3の開発中に、**Nuxtを本番環境でエッジランタイム/V8 isolate上で実行するという賭けに出ました。**
これにより、CloudFlare Workersのようなプラットフォームを使用する場合、サーバー、ロードバランサー、キャッシングを扱うことなく、世界中から約50msでページをサーバーレンダリングできます。リクエスト100万件あたり約0.3ドルです。現在、Deno Deployなど、V8 isolate上でアプリケーションを実行できる新しいプラットフォームが登場しています。
課題
Nuxtをワーカーで実行するには、Nuxtの一部を環境に依存しないように書き直す必要がありました(Node.js、ブラウザ、またはV8で実行)。
サーバーから始めてunjs/h3を作成しました。これは、高性能と移植性を重視して構築された最小限のHTTPフレームワークです。Nuxt 2で使用していたConnectに置き換えられますが、互換性があるので、Connect/Expressミドルウェアを引き続き使用できます。ワーカーでは、着信リクエストごとに、本番環境でNuxtを起動し、リクエストを送信し、レスポンスを送信します。
Nuxt 2では、メモリ内での本番サーバーの起動時間(コールドスタートとも呼ばれる)は約300msでした。これは、リクエストを処理するためにサーバーとアプリケーションのすべての依存関係を読み込む必要があったためです。
h3に取り組むことで、サーバーに接続された各ハンドラーをコード分割し、リクエストされた場合にのみ遅延ロードすることにしました。Nuxt 3を起動すると、メモリにはh3と対応するハンドラーのみがロードされます。リクエストが到着すると、ルートに対応するハンドラーがロードされ、実行されます。
このアプローチを採用することで、**コールドスタート時間を約300msから約2msに削減しました。**
Nuxtをエッジで実行するには、もう1つの課題がありました。それは、本番バンドルのサイズです。これには、サーバー、Vueアプリケーション、Node.jsの依存関係が組み合わされています。Cloudflareワーカーは現在、ワーカーサイズに1MB(無料プラン)と5MB(月額5ドルプラン)の制限があります。
nuxt build
コマンドを実行すると、unjs/nitro(サーバーエンジン)がプロジェクト全体をバンドルし、すべての依存関係を最終出力に含めます。Rollupとvercel/nftを使用して、node_modules
で使用されるコードのみを追跡し、不要なコードを削除します。**基本的なNuxt 3アプリケーションの生成された出力の合計サイズは約700kB(gzip圧縮)です。**
最後に、開発(Node.js)とCloudflare(エッジランタイム)での本番環境の開発エクスペリエンスを同じにするために、unjs/unenvを作成しました。これは、既知の依存関係に対してモックを作成したり、ポリフィルを追加したりすることで、JavaScriptコードをどこでも実行できるようにする(プラットフォームに依存しない)ライブラリです。
Nuxtでは、最適なホスティングプロバイダーを選択できる自由があるべきだと考えています。
そのため、エッジサイドレンダリングによるNuxtアプリケーションを次のようにデプロイできます。
- NuxtHub
- Cloudflare Pages
- Deno Deploy
- Vercel Edge Functions(内部的にCloudflare Workersを使用)
- Netlify Edge Functions(内部的にDenoを使用)
また、静的ホスティングや従来のNode.jsサーバーレスおよびサーバーホストなど、多くの他のデプロイメントプロバイダーもサポートしています。
フルスタック機能の強化
Nuxtをエッジランタイムで実行できるようになったことで、Vueアプリケーションのレンダリング以外にも多くのことが行えます。serverディレクトリのおかげで、APIルートの作成はTypeScriptファイル一つで済みます。
/api/helloルートを追加するには、server/api/hello.tsファイルを作成します。
export default defineEventHandler((event) => {
return {
hello: 'world'
}
})
これで、ページとコンポーネントでこのAPIを普遍的に呼び出すことができます。
<script setup>
const { data } = await useFetch('/api/hello')
</script>
<template>
<pre>{{ data }}</pre>
</template>
useFetchと$fetchを作成した際に重要なことは、サーバーサイドレンダリング中にAPIルートを呼び出すと、リクエストをエミュレートし、関数コードを直接呼び出します。**これにより、HTTPリクエストを回避し、ページのレンダリング時間を短縮できます。**
開発エクスペリエンスに関して、サーバーファイルを作成すると、NuxtサーバーはVueアプリケーションを再構築せずに実行し続けることに気付くでしょう。**これは、Nuxt 3がAPIとサーバールートを作成する際にホットモジュール置換(HMR)をサポートしているためです。**
さらに、drizzle-ormのようなオブジェクトリレーショナルマッピング(ORM)を利用することで、開発者はD1、Turso、Neon、Planetscaleなどのエッジとサーバーレスデータベースに接続できます。
Atidoneは、認証とエッジで実行されるデータベースを備えたフルスタックアプリケーションを紹介するオープンソースのデモです。ソースコードはGitHubでMITライセンスの下でatinux/atidoneで公開されています。
結論
エッジサイドレンダリングとその可能性に興奮しています。Nuxtチームは、あなたがこれに基づいて何を構築するかを見るのが待ちきれません!
Discordサーバーに参加するか、Twitterで@nuxt_jsにメンションして、あなたの作品を共有してください。