📑

fastly/next-compute-jsの内部アーキテクチャを調べる

2022/10/19に公開

https://github.com/fastly/next-compute-js

Fastlyから新たなNext.jsインテグレーションツールがリリースされていたので調べてみた。

モチベーションとしてはServerless Nextjs Pluginに移植してCloudflare Workersでも動かしたい。

fastly/next-compute-jsとは

Compute@Edge でNext.jsアプリケーションを動かしたい時に使うツール。

以下に解説がある

https://www.fastly.com/jp/blog/run-your-next-js-app-on-fastly

使い方

  1. Next.jsプロジェクトを作成
  2. npx @fastly/next-compute-js@latest init でサブディレクトリcompute-js以下にCompute@Edge固有の構成を追加する
  3. fastly compute serve でローカルで起動する
  4. fastly compute publish でデプロイする

compute-jsに何ができるのか

WebpackとFastly CLIでビルドとデプロイするための設定ファイル群ができる。

❯ tree . -L 1
.
├── bin
├── default-content-types.cjs
├── fastly.toml
├── node_modules
├── package-lock.json
├── package.json
├── pkg
├── src
├── static-publish.rc.js
└── webpack.config.js

この雛形はnext-compute-js/resources/compute-js に定義されている。

重要なのはfastly.tomlwebpack.config.jssrc/index.jsfastly compute serve を実行した時にこれらの設定ファイルが参照されて、実際にデプロイされるjsとwasmバイナリが生成される。

ビルドステップ

細かい検証フローは省略するけど fastly compute serve を実行した時に内部的には fastly compute build が実行され以下のコマンドが走っている(fastly.tomlに記述がある)

  1. npx @fastly/compute-js-static-publish --build-static --suppress-framework-warnings
    a. これによって src/statics.js ができる
  2. $(npm bin)/webpack
    a. これによって ./bin/index.js ができる
  3. $(npm bin)/js-compute-runtime ./bin/index.js ./bin/main.wasm
    a. これによって ./bin/main.wasm ができて pkg/package.tar.gzindex.jsと一緒にパッケージされる

compute-js-static-publish

compute-js-static-publishで生成される src/statics.jsnext build で生成された静的ファイルを読取り、パスのマッピングとメタ情報を含めている

import file0 from "../../.next/BUILD_ID?staticBinary";
import file1 from "../../.next/build-manifest.json?staticText";
import file2 from "../../.next/export-marker.json?staticText";
// ...

export const assets = {
  "/.next/BUILD_ID": { contentType: undefined, content: Buffer.from(file0, "base64"), module: null, isStatic: false },
  "/.next/build-manifest.json": { contentType: "application/json", content: file1, module: null, isStatic: false },
  "/.next/export-marker.json": { contentType: "application/json", content: file2, module: null, isStatic: false },
// ...

これがfastly/next-compute-jsのエントリーポイントの./src/index.js内部のリクエストハンドラーに渡されている

import createServer from "@fastly/next-compute-js";

import { assets } from './statics';

const server = await createServer({
  dir: '../../',
  computeJs: {
    assets,
    backends: {
      'httpbin': { url: 'https://httpbin.org/anything/' },
    },
  }
});

webpack build

Webpackのレイヤーでは上記エントリーポイントをtarget: "webworker"でビルドして、Fastly環境でのWeb API(streamやbufferやcryptoや)呼び出しをNode.js版のpolyfillを使うようにバンドルしている。

この時にNEXT_RUNTIME: 'edge' を指定している。

すべてが詰め込まれた3MB超のbin/index.jsが吐き出される。

js-compute-runtime

js-compute-runtimeはjsを入力してwasmを吐くコマンド。

bin/main.wasm の段階で30MBのファイルになるため .next/ から読み取ったアセットファイルがインライン化して内部に固められていると思われる。それを実行時にインメモリで解決している?(予想)

NextComputeJsServer

Compute@Edgeは任意のWasmファイルをランタイムで実行することでリクエストに応答する。それがビルドした時に生成されるbin/main.wasmになる。

この実行ファイルの中でJavaScriptのレイヤーで見るとNext.js内部APIのNextNodeServerと互換性のあるNextComputeJsServerがリクエスト処理をハンドリングしている。

つまりCompute@Edge環境で動作するNextNodeServerが独自に実装されている(やば)。

ComputeJsAsset

静的ファイルの参照はsrc/server/require.tsに書かれていた。やっぱりパスを与えるとファイルの中身が取得できる仕組みのよう。それ以上のことは js-compute-runtime のコンパイラ側の実装を読まないと分からない。

Discussion