Remix+CloudflareでWebサイトを作る 43(TTFBが遅いの原因はPrisma、Drizzleへ移行)
【2024-11-21】結論:TTFBが遅いのはPrismaのせい
背景
Remix Meetup Tokyoに参加したので、その機会にこの「TTFB遅くて困ってる問題」を発表したところ原因はPrismaなので剥がすといいよというアドバイスを頂けた。
サイズは?
前の記事にも書いているがRemix+Cloudflareのアプリを作った直後は350iKBで、Prismaを入れると1250KiBになる。
これが今回Drizzleにするとなんと416KiBだった!!!
つまり、Prismaは900KiB、Drizzleは66KiBということになる。
Prismaは13.5倍以上でかい。すごい差だ。
Honoの作者でCloudflareの中の人でもある @yusukebeさんも参加していたんだけど大きさは速さに直結するとのこと。
また、Cloudflare Workerはアクセスしていない時間があると落ちる(ここどんな表現だったかうろ覚え)ことがありCloudflare側の設定でいじれるものではないらしい。
ということで、とにかくエッジで動かすからには小さいが大正義。
mizchiさんのスライドにも1MBをパフォーマンスバジェットにと書いてある。
Prismaの問題点
また、Prismaを使っていると遅いのには以下のような問題があると教えていただいた。
そしてPrismaもここ改善に取り組んでいるとのこと。
これは残念ながら事実だ。 しかし、私たちはこの1年間、パフォーマンスを向上させるために懸命に取り組み、成功させた。 来年はさらに努力するつもりであり、私たちが取り組んでいることを共有するのが待ちきれない。
その他
その後のXでの会話。
この話はZennで記事にする予定。
【2024-11-21】Prisma→Drizzleへ移行
実装
1. インストール
pnpm add drizzle-orm drizzle-kit better-sqlite3
db/index.ts
db/schema.ts
を作成
2. 開発環境だと.dev.vars
に記述した値、プロダクションだとCloudflare Pagesに設定した変数を使うのではなくて、.env.*
ファイルに記述したVITE_
始まりの環境変数を使う。
前者の場合は context.cloudflare.env
から取得できないがその使い方をしたくないという意図。
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
if (!import.meta.env.VITE_TURSO_URL) {
throw new Error("🚫 VITE_TURSO_URL がありません");
}
if (!import.meta.env.VITE_TURSO_AUTH_TOKEN) {
throw new Error("🚫 VITE_TURSO_AUTH_TOKEN がありません");
}
const client = createClient({
url: import.meta.env.VITE_TURSO_URL,
authToken: import.meta.env.VITE_TURSO_AUTH_TOKEN,
});
export const db = drizzle(client);
import { sql } from 'drizzle-orm';
import { integer, sqliteTable, text, } from 'drizzle-orm/sqlite-core';
export const userTable = sqliteTable('user', {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
name: text('name'),
email: text('email').notNull().unique(),
createdAt: text("createdAt")
.notNull()
.default(sql`(datetime(CURRENT_TIMESTAMP, '+9 hours'))`),
updatedAt: text("updatedAt")
.notNull()
.default(sql`(datetime(CURRENT_TIMESTAMP, '+9 hours'))`).$onUpdate(() => new Date().toISOString()),
});
export type InsertUser = typeof userTable.$inferInsert;
export type SelectUser = typeof userTable.$inferSelect;
export const companyTable = sqliteTable('company', {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
createdAt: text("createdAt")
.notNull()
.default(sql`(datetime(CURRENT_TIMESTAMP, '+9 hours'))`),
updatedAt: text("updatedAt")
.notNull()
.default(sql`(datetime(CURRENT_TIMESTAMP, '+9 hours'))`).$onUpdate(() => new Date().toISOString()),
});
export type InsertCompany = typeof companyTable.$inferInsert;
export type SelectCompany = typeof companyTable.$inferSelect;
3. drizzle.config.tsを作成
import { defineConfig } from "drizzle-kit";
if (!import.meta.env.VITE_TURSO_URL) {
throw new Error("🚫 VITE_TURSO_URL がありません");
}
// マイグレーションファイル用の設定
export default defineConfig({
dialect: "sqlite",
out: "./db/migrations",
schema: "./db/schema.ts",
dbCredentials: {
url: import.meta.env.VITE_TURSO_URL,
},
introspect: {
casing: "camel",
},
migrations: {
prefix: "timestamp",
table: "__drizzle_migrations__",
},
strict: true,
verbose: true,
});
load-context.ts
を修正
4. だいぶスッキリした。
import type { PlatformProxy } from "wrangler";
declare module "@remix-run/cloudflare" {
interface AppLoadContext extends ReturnType<typeof getLoadContext> {
// This will merge the result of `getLoadContext` into the `AppLoadContext`
cloudflare: Cloudflare;
}
}
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
type GetLoadContextArgs = {
request: Request;
context: {
cloudflare: Cloudflare;
};
}
export function getLoadContext({ context }: GetLoadContextArgs) {
return context
}
5. envファイルの作成
VITE_TURSO_URL=http://127.0.0.1:8080
VITE_TURSO_AUTH_TOKEN=xxxxxxxxxx
VITE_TURSO_URL=<任意の値を入力>
VITE_TURSO_AUTH_TOKEN=<任意の値を入力>
package.json
の更新
6. 以下のようにする。
"scripts": {
// 本番環境へデプロイ時に .env.production を見るようにする
"build": "remix vite:build --mode production",
"deploy": "pnpm run build && wrangler pages deploy ./build/client",
// 開発環境立ち上げ時に .env.developmentを見るようにする。また、TursoのDBも立ち上げるようにしておく
"dev": "concurrently \"remix vite:dev --mode development\" \"pnpm run db:dev\"",
"db:dev": "turso dev --db-file db/dev.db",
7. マイグレーションファイルを作成、適用
# マイグレーションファイルを作成
pnpm drizzle-kit generate --name=init
# ローカルで適用
turso db shell http://127.0.0.1:8080 < db/migrations/20241120151112_init.sql
# Drizzle Studioを立ち上げるとテーブルが追加されていることが確認できる
pnpm drizzle-kit studio
open https://local.drizzle.studio
8. DBの値を取得するコードを修正
import { Suspense } from "react";
import { Await, useLoaderData } from "@remix-run/react";
import { db } from "db";
import { companyTable, userTable } from "db/schema";
import type { InsertCompany, InsertUser } from 'db/schema';
async function getUsers(): Promise<InsertUser[]> {
try {
// ここらへんをDrizzle用の書き方にしている
const users = await db.select().from(userTable)
return users
} catch (error) {
console.error(error)
throw new Error('Failed to fetch users')
}
}
async function getCompanies(): Promise<InsertCompany[]> {
try {
const companies = await db.select().from(companyTable)
return companies
} catch (error) {
console.error(error)
throw new Error('Failed to fetch companies')
}
}
export async function loader() {
const users = getUsers()
const companies = getCompanies()
return { users, companies }
}
export default function DbConnectPage() {
const { users, companies } = useLoaderData<typeof loader>();
return (
<div className="space-y-10">
<h1 className="text-2xl">DB(Turso)からデータを取得</h1>
<h2 className="text-xl">ユーザー一覧</h2>
<Suspense
fallback={
<div>
<p>loading...</p>
</div>
}
>
<Await resolve={users} errorElement={<p>error</p>}>
{(users) => {
console.dir(users);
if (!users) {
return <p>no data</p>
}
return <ul>
{users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
}}
</Await>
</Suspense>
<h2 className="text-xl">会社</h2>
<Suspense
fallback={
<div>
<p>loading...</p>
</div>
}
>
<Await resolve={companies} errorElement={<p>error</p>}>
{(companies) => {
console.dir(companies);
if (!companies) {
return <p>no data</p>
}
return <ul>
{companies.map((company) => <li key={company.id}>{company.name}</li>)}
</ul>
}}
</Await>
</Suspense>
</div>
);
}
9. デプロイ
pnpm run deploy
undefined
になってしまう場合の対処方法
メモ:process.envの値がサーバー立ち上げ直後に背景
最初 load-context.ts
で DB clientを作成するコードにしていたら process.envの値がサーバー立ち上げ直後にundefined
になってしまうという問題が発生していた。
結局このコードは使用していないがメモとして回避方法を書いておく。
手順
1. dotenvをインストール
pnpm add -D dotenv-cli
2. package.jsonのscriptを更新
読み込むファイルを指定する。
"dev": "dotenv -e .env.development -- remix vite:dev --mode development",
process.env
でアクセス
3. TEST=hoge
// サーバー立ち上げ直後に procee.env.* がundefinedになるのを防ぐために明示的に指定する
// 環境ごとに読み込むenvファイルを動的に変更するために
// package.json の scripts に "dotenv -e .env.<FILE_PATH>" を追加している
dotenv.config(); // = `dotenv.config({ path: "./.env.development" })`
console.log(process.env.TEST) // hoge
Cloudflare Workerでprocess.env
使えないし備え付けのimport.meta.env
使えば良いだけなので ↑ のはなんの意味もなかった。
dotenv全消し。
【2024-11-21】測定
BeforeもAfterもTursoはHobbyプラン。
つまり、課金してClod startがない状態。
Before
デプロイ対象 | 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 👀 比較対象はココ! |
After
/db-connect
だけ見れば良い。
1000msくらい早くなった!
600msにはまだ少しあるけどまぁ許せるくらいかな。
デプロイ対象 | URL | TTFB |
---|---|---|
Cloudflare Pages | /db-connect |
1020, 1100, 1150, 1210ms |