Closed14

Remix+CloudflareでWebサイトを作る 42(1から作りなおしてCloudflare PagesのTTFBが遅い原因を探る)

saneatsusaneatsu

【2024-11-13】TTFBが3.8sかかる理由がわからないので1から作り直す

偶然にも有給をとったのでざっくり以下の各ステップに分けてLighthouseで測定していく。

  1. Remixのテンプレートを作ってCloudflareにデプロイ
  2. ライブラリを全部インストールする
  3. スキーマファイル作ってDBと接続できるようにする
  4. トップページでなにかのテーブルの内容を一覧取得する
  5. ページ数を増やす
  6. 機能を増やす
saneatsusaneatsu

【2024-11-13】1. Remixのテンプレートを作ってCloudflareにデプロイ

アプリの作成

Cloudflareのドキュメントに書いてあるコマンドを使ってアプリ作成。

pnpm create cloudflare@latest my-app --framework=remix --experimental

...
╭ Deploy with Cloudflare Step 3 of 3
│
╰ Do you want to deploy your application?
  Yes / No # Github Actionsを使ってデプロイしたいのでNoを選択した

ローカルからデプロイ

とりあえずmainブランチにpushして以下のコマンドでデプロイ。

pnpm run deploy

Cloudflareの「Workers & Pages」にアプリが作成された。
なんかタブの構成から色々と変わってる。
既存のアプリにも対応させて欲しいな。。

GitHub Actionsでデプロイ

GitHub Secretsの値を元のリポジトリからコピー。
本番環境へデプロイするGithub Actionsをmainにpushしてデプロイしようとしたら以下のエラーが発生。
アプリがないとのこと。
pnpm run deployを実行したら勝手に作成してくれたのでは...?

  ✘ [ERROR] A request to the Cloudflare API (/accounts/***/pages/projects/my-app) failed.
  
    Project not found. The specified project name does not match any of your existing projects. [code: 8000007]

「Workers & Pages」を見ると元のはCloudflare Pages(アイコン:⚡️)だけど、新たに作成したものはCloudflare Workers(アイコン:💠)になっている。

production.yml
...

      - name: 🚀 Deploy
        id: cloudflare-wrangler
        uses: cloudflare/wrangler-action@v3
        with:
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-         command: pages deploy ./build/client --project-name=my-app
+         # command: pages deploy ./build/client --project-name=my-app

また、今まではCloudflare Pages用のAPI Tokenを使用していたが、新たにCloudflare Worker用のものを作成してGitHub Secretsに登録しておく。

結果

120msだった。

saneatsusaneatsu

【2024-11-13】2. ライブラリを全部インストールする

元のアプリの pacakge.json に書いてあるものを全部コピペしてデプロイする。
このタイミングで package.jsonenginesも元のアプリに合わせて修正。

package.json
  },
  "engines": {
-    "node": ">=20.0.0"
+    "node": ">=18.0.0"
  }
}

結果

110ms。

$ npx wrangler deploy './server.ts' --outdir bundled/ --dry-run

 ⛅️ wrangler 3.86.1
-------------------

Total Upload: 2071.89 KiB / gzip: 351.08 KiB
--dry-run: exiting now.

saneatsusaneatsu

【2024-11-13】3. スキーマファイル作ってDBと接続できるようにする

Tursoをつなげる

Cloudflareの「Workers & Pages」のインテグレーションタブからTursoと接続。

Prismaのスキーマを作成してDBへ接続する

以下コマンドで prisma/schema.prisma を作成

npx prisma init

スキーマファイルを丸まるコピペ。
以下コマンドで型定義ファイルを作成。

npx prisma generate

マイグレーションファイルを作成。

npx prisma migrate dev --name init

結果

サイズは一気に跳ね上がった。
TTFBは240ms。

$ npx wrangler deploy './server.ts' --outdir bundled/ --dry-run

 ⛅️ wrangler 3.86.1
-------------------

Total Upload: 4717.43 KiB / gzip: 1261.67 KiB

saneatsusaneatsu

【2024-11-13】Cloudflare PagesにデプロイしなおしたらTTFB遅いの再現できた

今気づいたけどCloudflare WorkerはCloudflare Pagesと違ってプロダクションとプレビュー環境が分かれていなのか。

同じ環境にしないと検証できないしCloudflare Pagesで作り直す。

修正

Github Actionsは以下のようにPagesにデプロイするようにもどす。
secrets.CLOUDFLARE_API_TOKEN もCloudflare Pages用のものに戻す。

production.yml
      - name: 🚀 Deploy
        id: cloudflare-wrangler
        uses: cloudflare/wrangler-action@v3
        with:
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: pages deploy ./build/client --project-name=my-app # ここ追加

functions/[[path]].tsを新たに作成。

functions/[[path]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
import { getLoadContext } from "../load-context";
export const onRequest = createPagesFunctionHandler({ build, getLoadContext });

wrangler.tomlのmainを更新する。

wrangler.toml
compatibility_date = "2024-05-10"
name = "my-app"
account_id = "xxx"
main = "functions/[[path]].ts"

結果

遅くなった!!!
一気に240msから3,120msになってしまった。
最初間違ってCloudflare Workersにデプロイして良かった(?)

主なコード自体は同じで差分は以下。

  1. CLIで作られたvite.config.tsではなく、もとのアプリで使っていたものを使用した
  2. CLIで作られたserver.tsからfunctions/[[path]].tsにした(それにともないwrangler.tomlを修正)
    • そういや初めのほうでみた記事を真似してこう書いてあるけどPagesには/build/clientをデプロイするわけだしここは関係無い気がする
  3. Cloudflare WorkersではなくCloudflare Pagesにデプロイした

次はこれらを順番に元に戻していって原因を特定する。

saneatsusaneatsu

【2024-11-13】vite.config.tsをCLIで作成したときの状態に戻す

戻す

CLIでRemixアプリを作った際にあったオプションをもどしてみる

vite.config.ts
import {
+ cloudflareDevProxyVitePlugin,
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig } from "vite";
import { envOnlyMacros } from "vite-env-only";
import tsconfigPaths from "vite-tsconfig-paths";

import { getLoadContext } from "./load-context";

declare module "@remix-run/cloudflare" {
  interface Future {
    v3_singleFetch: true;
  }
}

export default defineConfig(({ mode }) => {
  return {
    plugins: [
+     cloudflareDevProxyVitePlugin({
+       getLoadContext,
+     }),
      envOnlyMacros(),
      remixCloudflareDevProxy({ getLoadContext }),
      remix({
        future: {
          v3_singleFetch: true,
+         v3_fetcherPersist: true,
+         v3_relativeSplatPath: true,
+         v3_throwAbortReason: true,
+         v3_lazyRouteDiscovery: true,
        },
      }),
      tsconfigPaths(),
    ],
    build: {
+     minify: true,
      rollupOptions: {
        plugins: [
          mode === "analyze" &&
            visualizer({
              open: true,
              filename: "stats.html",
              gzipSize: true,
              brotliSize: true,
            }),
        ],
      },
    },
    server: {
      port: 5173,
    },
+   resolve: {
+     mainFields: ["browser", "module", "main"],
+   },
    ssr: {
+     resolve: {
+       conditions: ["workerd", "worker", "browser"],
+     },
      noExternal: [
        "remix-i18next",
      ],
    },
  };
});

結果

3710msかかった。
ここは関係なし。

saneatsusaneatsu

【2024-11-13】wrangler.tomlmainを戻してCloudflare Pagesにデプロイ

main は実行される Worker のエントリーポイントへのパスだから関係ないと思うけど一応...。

ref: Configuration - Wrangler

wrangler.toml
compatibility_date = "2024-05-10"
name = "my-app"
account_id = "xxx"
- main = "functions/[[path]].ts"
+ main = "./server.ts"

結果

4,210ms

saneatsusaneatsu

【2024-11-13】ここまでのまとめ、ネクストアクション

まとめ

  • Cloudflare Workers
    • ./server.ts をエントリーポイントとしてデプロイすると240ms
  • Cloudflare Pages
    • pages deploy ./build/client --project-name=my-app でデプロイすると 3500ms
  • vite.config.tsは無関係

Cloudflare Pagesにデプロイしたいけどこれじゃない方法がわからない。
あるとしたらGithub ActionsではなくてCloudflare PagesとGitHubを連携させてpush時に自動デプロイする方法だけど多分何も変わらないと思うんだよな。

ネクストアクション

  • もう1回初めから作り直して、CLIで作ったばかりのコードをCloudflare PagesにGithub Actionsでデプロイしてみる
    • 遅い場合
      • こうなったらもうどうすればいいかわからない
    • 早い場合
      • 元のコードのvite.config.tsを使う
      • その後はまた少しずつ元のアプリに近づくようにコードを足していきながら遅くなるポイントを探る

もしかしたらPrisma入れてサイズが一気に大きくなったところで遅くなっているのかもしれないしな〜。

saneatsusaneatsu

【2024-11-14】RemixをCloudflare Pagesにデプロイ

アプリ作成

https://zenn.dev/link/comments/3dbfd6783f0086

ここと同じことをやる。

pnpm create cloudflare@latest my-app --framework=remix --experimental

Cloudflare Pages用の最低限の修正

https://blog.stin.ink/articles/remix-on-cloudflare-pages-tutorial

パッケージのインストール

pnpm add @remix-run/cloudflare-pages

functions/[[path]].ts の作成

Pages Functions でリクエストを受けてそれを Remix に流すような形になるので、functions/[[path]].ts にアダプターを設置する。Pages Functions では onRequest 関数を named-export するルールとなっている。[[path]].ts は catch-all のファイル名ルールで、すべてのリクエストが Remix に流されることになる(HTML はもちろん JS などの静的アセットも含む)。

Remix Build によって build/server/index.js ファイルが生成されるため、それを Pages Functions につなぐイメージ。ビルド前には存在しないファイルを import するので型エラーを潰す必要がある。ビルド後は存在するファイルになって型エラーが出なくなるため、@ts-expect-error ではダメ。eslint を黙らせてでも @ts-ignore を使用する。

functions/[[path]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the server build file is generated by `remix vite:build`
// eslint-disable-next-line import/no-unresolved
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({ build });

entry.server.ts の修正

entry.server.ts
/**
 * By default, Remix will handle generating the HTTP Response for you.
 * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
 * For more information, see https://remix.run/file-conventions/entry.server
 */

import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";

export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  // This is ignored so we can keep it in the template for visibility.  Feel
  // free to delete this parameter in your app if you're not using it!
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  loadContext: AppLoadContext,
) {
  const body = await renderToReadableStream(
    <RemixServer context={remixContext} url={request.url} />,
    {
      signal: request.signal,
      onError(error: unknown) {
        // Log streaming rendering errors from inside the shell
        console.error(error);
        responseStatusCode = 500;
      },
    },
  );

  if (isbot(request.headers.get("user-agent") || "")) {
    await body.allReady;
  }

  responseHeaders.set("Content-Type", "text/html");
  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

ローカルで立ち上げてみる

npx wrangler pages dev ./build/client

これで http://localhost:8788 でRemixの画面が表示されるようになっているはず。

デプロイ

以前はCloudflare Workersにデプロイしたが、今回はCloudflare Pagesにデプロイする。

# 以前はCloudflare Workersにデプロイした
# pnpm run deploy

# 今回はCloudflare Pagesにデプロイ
pnpm run build
npx wrangler pages deploy ./build/client

結果

110ms。

saneatsusaneatsu

【2024-11-14】Prismaの導入をしてファイルサイズが増えたらTTFBは遅くなるのか?

パッケージ全部追加

https://zenn.dev/link/comments/a991c7c8d5d5fe

Prismaを入れると一気にファイルサイズが大きくなるがこのときのTTFBの変化を見てみる。

package.json に元のアプリで使っているパッケージをすべて入れる。
schema.prismaを元のアプリに合わせて更新。

pnpm i
npx prisma init
npx prisma generate

デプロイコマンドを追加してデプロイ。

package.json
{
  "scripts": {
    "build": "remix vite:build",
+   "deploy": "pnpm run build && wrangler pages deploy ./build/client --project-name=APP_NAME",

DBに接続

トップページでloaderを呼び出しTursoのDBの1つのテーブルから適当な値をとってくる。

結果

サイズ(gzip) TTFB
アプリ作成時 354K 110ms
パッケージ全部追加 354K 130ms
DBに接続 354K 670ms

saneatsusaneatsu

【2024-11-16】Tursoに課金してCold Startをなくすのは効果があるのか?

背景

https://x.com/techtalkjp/status/1857059332170326047

Xで呟いたら @coji さんからこんな返信が。
Turso Database Pricing がこちら。

ということで、とりあえず年間ではなく1ヶ月9ドルの方のプランに入ってみる。

課金して測定

3つ目に作成したアプリ

130msに下がったっぽい?
と思いきやDB接続の箇所でエラーが発生しているので以下のようにするとエラーが出なくなる。
ローカルで開発しているときは問題なかったんだけどな。

functions/[[path]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the server build file is generated by `remix vite:build`
// eslint-disable-next-line import/no-unresolved
import * as build from "../build/server";
+ import { getLoadContext } from "../load-context";

- export const onRequest = createPagesFunctionHandler({ build });
+ export const onRequest = createPagesFunctionHandler({ build, getLoadContext });

こうするとエラーが出なくなるが1,190msかかった。

https://zenn.dev/link/comments/1f6478cba473a2

2つ目に作成したアプリ

https://zenn.dev/link/comments/b1fae8aa65a7f8

このときに3120msを叩き出した方のサイトで試してみる。
5070msかかった。2回目試すと3530ms。

そもそもサイトに「最初にアクセスする」だけが遅いのではなく「アクセス後に別ページに遷移する」ときにも遅くなるのでTursoのコールドスタートは関係ないのかも?

元のアプリ

有料にしたけど3840ms。

loader()を削除する

この時Tursoは無料プラン。
230msになった。

loader()はあるがDBから値は取ってきていない

ここではURLのクエリパラメータをloader()から取ってきて画面に表示するだけしてみた。
Tursoは無料プラン。
210ms。

loader() で1行だけデータがあるテーブルの値を取得する

Tursoは無料プラン。
1220ms。

有料プランにする。
1240ms。

う〜ん、やっぱTursoのClod startは関係なさそう。

まとめ

デプロイ先 TTFB サイズ
Cloudflare Workers Remixアプリ作成時 120 ms 350 KB
Prismaを導入してDBに接続する処理を追加 240 ms 1260 KB
Cloudflare Pages Remixアプリ作成時 110 ms 350 KB
Prismaを導入してDBに接続する処理を追加 670 ~ 1240 ms 1260 KB

Tursoを無料版から有料版にすることによるTTFBの変化は見られない。
何回も試してたからすでにあったまっているんだろうか。

saneatsusaneatsu

【2024-11-16】DBへの接続方法が問題なのか?

https://dev.classmethod.jp/articles/remix-on-cloudflare-workers-w-kv/

この記事を見ながら思ったけどDBに接続している方法が問題なのか、loader周りのなにかがおかしいのかわからない。
以下のように適当に200件のデータを取得して表示してみる。

これでTTFBが早ければDBへ接続する処理周りが原因かも?

コード書き換え

async function getTodos() {
  // 20件取得
  // const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=20');
  const todos = await response.json();
  return todos;

  // 200件取得(デフォルト)
  const response = await fetch('https://jsonplaceholder.typicode.com/todos');
  const todos = await response.json();
  return todos;

  // 400件取得
  const response1 = await fetch('https://jsonplaceholder.typicode.com/todos');
  const response2 = await fetch('https://jsonplaceholder.typicode.com/todos');
  const allTodo = await Promise.all([response1.json(), response2.json()]);
  return allTodo.flat();
}

export async function loader({ request, context }: LoaderFunctionArgs) {
  const todos = getTodos()
  return { todos }
}

export default function IndexPage() {
  const { todos } = useLoaderData<typeof loader>();

  return (
    <Suspense
      fallback={
        <div>
          <p>loading...</p>
        </div>
      }
    >
      <Await resolve={todos} errorElement={<p>error</p>}>
        {(todos) => {
          console.dir(todos);
          if (!todos) {
              return <p>no data</p>
          }

          return <ul>
            {todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}
        </ul>
        }}
      </Await>
    </Suspense>
  )
}

Cloudflare Pagesで測定

時間を空けて何回か計測。

20件取得

810ms(最初だからかかっただけ?)、400ms、210ms、230ms。

200件取得

1000ms、270ms、420ms。

400件取得

310ms、440ms、470ms。

ネットワークで見たらDNS lookupにかなり時間かかってる

Cloudflare Workerで測定

400件取得

890ms(最初だからかかっただけ?)、220ms、390ms

まとめ

今までのまとめ

Prismaを使ってTursoからデータを取得

デプロイ先 処理 TTFB サイズ
Cloudflare Workers Remixアプリ作成時 120 ms 350 KB
Prismaを導入してDBに接続する処理を追加 240 ms 1260 KB
Cloudflare Pages Remixアプリ作成時 110 ms 350 KB
Prismaを導入してDBに接続する処理を追加 670 ~ 1240 ms 1260 KB

Tursoを無料版から有料版にすることによるTTFBの変化は見られない。

JSONPlaceholderからデータを取得

デプロイ先 処理 TTFB サイズ
Cloudflare Workers 400件取得 220 ~ 890ms 1260 KB
Cloudflare Pages 20件取得 210 ~ 810ms 1260 KB
200件取得 270 ~ 1000ms 1260 KB
400件取得 310 ~ 470ms 1260 KB
saneatsusaneatsu

【2024-11-17】再現用のリポジトリ作る

https://x.com/__saneatsu/status/1855873587896627294

明後日Meetupあるし再現できるようにパブリックなリポジトリ作る。

Cloudflare Worker

https://github.com/saneatsu/remix-on-cf-worker

https://remix-on-cf-worker.w-saneatsu-e8c.workers.dev/

# 0. アプリ作成
pnpm create cloudflare@latest remix-on-cf-worker --framework=remix --experimental

# 1. 準備
pnpm add prisma @libsql/client @prisma/adapter-libsql concurrently @remix-run/cloudflare-pages
npx prisma init

# 2. schema.prismaを更新

# 3. モデルファイルを作成
npx prisma generate

# 4. マイグレーションファイルを作成
npx prisma migrate dev --name init

# 5. ローカルで適用
turso db shell http://127.0.0.1:8888 < prisma/migrations/20241117081513_init/migration.sql

Cloudflare Pages

https://github.com/saneatsu/remix-on-cf-pages
https://remix-on-cf-pages.pages.dev/

結果

  • /json-placeholder-200: JSON Placeholderから200件取得して表示
  • /json-placeholder-1000: JSON Placeholderから1000件取得して表示
  • /db-connect: Turso(DB)に接続してUserテーブルから5件、Companyテーブルから5件取得して表示
デプロイ対象 URL TTFB
Cloudflare Worker /json-placeholder-200 390, 400ms
/json-placeholder-1000 510, 520ms
/db-connect 1800, 2120, 2200, 2330ms
Cloudflare Pages /json-placeholder-200 240, 430ms
/json-placeholder-1000 500, 600msms
/db-connect 1970, 2250,2310ms
saneatsusaneatsu

Cloudflare Worker

/json-placeholder-200


/json-placeholder-1000

/db-connect




Cloudflare Pages

/json-placeholder-200



/json-placeholder-1000


/db-connect



このスクラップは2日前にクローズされました