Remix x Cloudflare Workers めも

- (2024/09/27) 静的ファイル配信を対応したCloudflare WorkersにRemixを移行してログ出力を容易にする方法
- Remix とは
-(直接関係ないけど)イベントループ、Web Worker、Service Worker

サーバーレス環境と DB 接続、コネクションプールについて
ここらへんが参考になる:
Vercelのようなサーバーレス環境では起動させっぱなしのインスタンスからDBに持続的にコネクションを張り続けるというプロセスがなく、リクエスト毎にDB接続がopen/closeされ、複数のリクエスト間でコネクションを使い回すことは保証されていません(プラットフォームごとにグローバル変数を使ったキャッシュテクニックなどはある)。
しかし@vercel/postgresのPoolClientを使った場合は、内部でNeon serverless driverの実装のWebSocket接続を通して、neon.techサーバー内でTCP接続に変換され、その背後のPgBouncerによって維持されたPostgreSQLへのコネクションが取得できます。
Neon によるコネクションプール
ざっくり書くと、Edge → Neon(PgBouncer → PostgreSQL) という構成。
Edge 環境はコネクションプールを維持できないので、Edge の外にコネクションプールを維持してくれる層がある。

いまだと Prisma Accelerate、Supabase の Pool Mode というのがあるみたい。
Prisma Accelerate を使うパターンと、Supabase の Pool Mode に TCP Connection で直接接続するパターンがある。両方とも動くが、前者はコードが綺麗で無駄が多く、後者はコードがややこしいが理論上はここがゴール。

Cloudflare Pages x Remix 構成でのリクエストフロー by Claude 3.7 Sonnet
Prompt:
Cloudflare Pages x Remix 構成のとき、このアプリケーションにリクエストが来たときはソースコードのエントリーポイント、そしてそこから実装したコンポーネントの表示までどのような経路になる? どのファイル、ソースコードを通るのか解説、図解(シーケンス図)して。mermaid がレンダリングできるように文法エラーに気をつけて。
➜ tree app functions build
app
├── entry.client.tsx
├── entry.server.tsx
├── lib
│ └── i18n.ts
├── root.tsx
├── routes
│ ├── _index.tsx
│ ├── api.episodes.$number.test.ts
│ ├── api.episodes.$number.ts
│ └── episodes.$number.tsx
└── tailwind.css
functions
└── [[path]].ts
build
├── client
│ ├── _headers
│ ├── _routes.json
│ ├── assets
│ │ ├── _index-CWhSQhKm.js
│ │ ├── components-CWrbwnVj.js
│ │ ├── entry.client-CHmt2Ukc.js
│ │ ├── jsx-runtime-0DLF9kdB.js
│ │ ├── manifest-0eef3245.js
│ │ ├── root-CS6YCbpS.css
│ │ └── root-DvypXt6e.js
│ ├── favicon.ico
│ ├── logo-dark.png
│ └── logo-light.png
└── server
├── assets
└── index.js
9 directories, 23 files

Cloudflare Pages x Remix アプリケーションへのリクエスト処理の流れを解説します。
Cloudflare Pages × Remix リクエスト処理フロー
Cloudflare Pages と Remix を組み合わせたアプリケーションでは、リクエストが以下の経路で処理されます。
テキストによる解説
- ユーザーがブラウザからリクエストを送信
- リクエストが Cloudflare のエッジネットワークに到達
- Cloudflare Pages が
functions/[[path]].ts
をエントリーポイントとして実行 - このファイルが Remix の
createPagesFunctionHandler
を呼び出し -
app/entry.server.tsx
が実行され、サーバーサイドレンダリング処理を開始 -
app/root.tsx
がアプリのルートレイアウトを定義 - URL に基づいて
app/routes/
内の適切なルートファイルが読み込まれる - レンダリングされた HTML がブラウザに返される
- ブラウザで
app/entry.client.tsx
が実行され、ハイドレーションが行われる
シーケンス図(Mermaid)
詳細解説
functions/[[path]].ts
1. エントリーポイント: このファイルは Cloudflare Pages Functions のエントリーポイントとして機能します。すべてのリクエストはまずここを通ります。一般的に以下のような実装になっています:
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "../build/server/index.js";
const handleRequest = createPagesFunctionHandler({
build,
mode: process.env.NODE_ENV,
getLoadContext: (context) => context,
});
export function onRequest(context) {
return handleRequest(context);
}
app/entry.server.tsx
2. サーバーサイドレンダリング: このファイルでは、Remix のサーバーサイドレンダリングが処理されます:
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
export default function handleRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}
app/root.tsx
3. ルートレイアウト: このファイルはアプリケーション全体のレイアウトを定義します:
import { Links, LiveReload, Meta, Outlet, Scripts } from "@remix-run/react";
import styles from "./tailwind.css";
export function links() {
return [{ rel: "stylesheet", href: styles }];
}
export default function App() {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet /> {/* ここで各ルートのコンテンツが描画される */}
<Scripts />
<LiveReload />
</body>
</html>
);
}
app/routes/_index.tsx
または他のルート
4. ルートコンポーネント: URLに基づいて適切なルートファイルが選択され、レンダリングされます:
export default function Index() {
return (
<div>
<h1>ホームページ</h1>
{/* コンポーネントの内容 */}
</div>
);
}
app/entry.client.tsx
5. クライアントサイドの処理: ブラウザでの初期化とハイドレーションを担当します:
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document, <RemixBrowser />);
ビルド済みファイルとの関連
build/
ディレクトリには、ビルド済みのファイルが含まれています:
-
build/server/index.js
- サーバーサイドのエントリーポイント -
build/client/assets/
- クライアントサイドのJSバンドルやCSS
これらは開発時に作成したソースコードがビルドされたものであり、実際の実行時にはこれらのファイルが使用されます。
以上がCloudflare Pages × Remix アプリケーションにおけるリクエスト処理の流れです。

Cloudflare Pages と Cloudflare Workers との違い
yusukebe さんの記事見た方が早かったかも:
Cloudflare Workersとは?
本題に入る前に、そもそも「Cloudflare Workersとは?」を簡単に紹介しておきます。
Cloudflare WorkersとはCloudflareのエッジで動くサーバーレス環境です。 基本的にJavaScript/TypeScriptでアプリケーションを書きます。 V8というJavaScriptエンジンの上でアプリを動かすのですが、これはWebブラウザのGoogle Chromeに搭載されているものです。 感覚としては、Chromeのタブが開く感じで、Workers上のアプリが立ち上がります。 JavaScriptと言ってもNode.jsではなく、ブラウザでも動く標準的なAPIを使います。 開発、デプロイにはWranglerというCLIを使います。
…というのがダイジェストです。
ではいってみよう!
Cloudflare Pagesとの統合
Cloudflare Workersの他にPagesというのがありまして、一般的にはNext.jsやAstroなどのフレームワークを用いて「フルスタック」なアプリケーションを作るプラットフォームです。 以前はWorkersとPagesは明確に分かれていたのですが、それが統合されつつあります。
Pagesはもともと静的なページをホストするのが主なユースケースだったのが、「SSR」というキーワードから分かる通り、ダイナミックな処理が増えてきました。 そのPagesのダイナミック処理はWorkersで動いているので、その境界線が曖昧になるんですね。
実際、ダッシュボードではWorkersもPagesも同じセクションになりました。
なので、我々がCloudflare Workersと呼ぶ時にはCloudflare Pagesも含むケースが多いです。
Workers vs. Pages (compatibility matrix)
そしてこういうのは公式が出しているのよな。AI に聞く前に公式の情報を探そう。
You can deploy full-stack applications, including front-end static assets and back-end APIs, as well as server-side rendered pages (SSR), to both Cloudflare Workers and Pages. The compatibility matrix below shows which features are available for each, to help you choose whether to build with Workers or Pages. Unless otherwise stated below, what works in Pages works in Workers, and what works in Workers works in Pages. Think something is missing from this list? Open a pull request ↗ or create a GitHub issue ↗.
We plan to bridge the gaps between Workers and Pages and provide ways to migrate your Pages projects to Workers.
日本語訳 with DeepL:
フロントエンドの静的アセットやバックエンドのAPI、サーバーサイドレンダリングページ(SSR)を含むフルスタックアプリケーションをCloudflare WorkersとPagesの両方にデプロイできます。以下の互換性マトリックスは、WorkersとPagesのどちらで構築するかを選択するのに役立つように、それぞれで利用可能な機能を示しています。以下に特に記載がない限り、Pagesで動作するものはWorkersでも動作し、Workersで動作するものはPagesでも動作します。このリストに足りないものがありますか?プルリクエストを開くか、GitHub issue を作成してください。
WorkersとPages間のギャップを埋め、PagesプロジェクトをWorkersに移行する方法を提供する予定です。
公式も統合していくと言っているね。
オブザーバビリティ関連の違いは大きそう。これ見ると自分がやろうとしているのは Workers 一択かも。
Pages Functions は Cloudflare Workers では使えない、というより Pages Functions の内部がそもそも Cloudflare Workers。
GitHub リポジトリとの簡単な連携は Workers ではできないみたい。自分で GitHub Actions で pnpm run deploy
をする必要がある。
日本語訳見て思ったけど、どっちも静的アセットの配信できるのよな。ますます違いがわからなくなってきた。「gap を埋めて Pages から Workers に移行していく」とあるから当然か。
フロントエンドの静的アセットやバックエンドのAPI、サーバーサイドレンダリングページ(SSR)を含むフルスタックアプリケーションをCloudflare WorkersとPagesの両方にデプロイできます。
このアナウンスで、Workers で静的アセット(HTML、CSS、クライアントサイド JavaScript)を Workers の一部としてアップロードして提供できるようになった。
もともとの立ち位置として「Workers がフル機能」、「Pages が Workers の一部機能使える。その代わり Workers より設定、初動が楽。」だと思っている。
静的アセットの配信が Workers でできるようになれば、Workers に Pages と同じアセットをアップロードして Workers で使えていたログ、DB、Queue Consumer あたりが使えるようになるということか。
You can now deploy applications to Workers that previously could only be deployed to Cloudflare Pages and use features that are not yet supported in Pages, including Logpush, Hyperdrive, Cron Triggers, Queue Consumers, and Gradual Deployments.

実際に Pages と Workers は統合されていく流れらしい。
デプロイ先の推奨がCloudflare PagesだったのをClooudflare Workersにしていきます。これはCloudflareの意向で、PagesとWorkersを統合する計画があり、Workersに寄せていくからです。スターターテンプレートではnpm run deployを実行すれば、スイスイとWorkersにデプロイすることができます。
MPAのシンプルなアプリケーションをデプロイが早いCloudflare Workersに置くことは非常に開発者体験がよいです。

Pages
これ見ると公式は Pages を静的ページの配信とは一言も言ってないのか。「フルスタックアプリケーションを作れますよ」と言っている。
Create full-stack applications that are instantly deployed to the Cloudflare global network.
Pages Functions は Pages の一機能という立ち位置。
Pages Functions は 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 });
Pages Functions
公式の Pages Functions の概要:
Pages Functions allows you to build full-stack applications by executing code on the Cloudflare network with Cloudflare Workers. With Functions, you can introduce application aspects such as authenticating, handling form submissions, or working with middleware. Workers runtime features are configurable on Pages Functions, including compatibility with a subset of Node.js APIs and the ability to set a compatibility date or compatibility flag. Use Functions to deploy server-side code to enable dynamic functionality without running a dedicated server.
日本語訳 with o3-mini-high:
Pages Functions は内部的には Workers で動的に JavaScript を実行している。
Pages Functions は、Cloudflare Workers を使用して Cloudflare ネットワーク上でコードを実行することで、フルスタックアプリケーションの構築を可能にします。Functions を使えば、認証、フォーム送信の処理、ミドルウェアの活用など、さまざまなアプリケーションの側面を取り入れることができます。Workers のランタイム機能は Pages Functions で設定可能で、Node.js API の一部との互換性や、互換性日付、互換性フラグの設定が含まれます。専用サーバーを用意することなく、動的な機能を実現するためにサーバーサイドコードをデプロイする際に、Functions をご利用ください。
Middleware はよく使いそう。Pages Functions 自体はファイルベースルーティングで /functions/_middleware.js
のように特定の名前のファイルを作ると特定のルーティングにミドルウェアを実装できる。
Hono とか Express でよく見る next
でチェーンしてくやつ
async function errorHandling(context) {
try {
return await context.next();
} catch (err) {
return new Response(`${err.message}\n${err.stack}`, { status: 500 });
}
}
function authentication(context) {
if (context.request.headers.get("x-email") != "admin@example.com") {
return new Response("Unauthorized", { status: 403 });
}
return context.next();
}
export const onRequest = [errorHandling, authentication];
ソースマップとスタックトレース

Pages のデモアプリケーション。WebSocket、Queue 使ったクローラ、Wasm モジュールの import あたりが気になる。
デモとアーキテクチャ
既存のアプリケーションやアーキテクチャ内で Pages をどのように活用できるかを学びましょう。
デモ
以下は Pages 用のデモアプリケーションの例です。
-
Jobs At Conf: ↗
対面カンファレンスで見つけた求人情報を掲載する求人情報サイト。Cloudflare Pages、R2、D1、Queues、および Workers AI を使用して構築されています。 -
Upload Image to R2 starter: ↗
Next.js アプリケーションから Cloudflare R2 に画像をアップロードするスターターアプリです。 -
Hackathon Helper: ↗
ハッカソン用スターターのシリーズです。より早く開発を始めましょう!Python、Streamlit、Workers、および Pages のスターターが、あらゆる AI のニーズに応えます。 -
NBA Finals Polling and Predictor: ↗
このステートフルな投票アプリケーションは、Cloudflare Workers AI、Cloudflare Pages、Cloudflare Durable Objects、および Hono を使用して、ユーザーが各バスケットボールチームに投票した結果を追跡し、シリーズの個別予測を生成します。 -
Floor is Llava: ↗
Cloudflare Workers AI にホストされた AI ビジョンモデル Llava を利用するための例リポジトリです。Pages にホストされた SvelteKit アプリとなっています。 -
Whatever-ify: ↗
自分自身を…なんでもに変身させましょう。写真を撮影し、説明を取得、シーンとキャラクターを生成し、そのカレンダーに基づいて画像を生成します。 -
Staff Directory demo: ↗
バックエンドロジックに HonoX、迅速かつ安全なホスティングに Cloudflare Pages、シームレスなデータベース管理に Cloudflare D1 の強力な組み合わせを使用して構築されています。 -
Vanilla JavaScript Chat Application using Cloudflare Workers AI: ↗
Cloudflare Pages 上に構築されたウェブベースのチャットインターフェースで、Cloudflare Workers AI 上のテキスト生成モデルを探索できます。デザインは Tailwind を使用して作られています。 -
Wildebeest: ↗
Wildebeest は ActivityPub および Mastodon と互換性のあるサーバーで、インフラの維持が不要で、最小限のセットアップとメンテナンスで数分で稼働できる、自分のドメイン上で Fediverse サーバーおよびアイデンティティを運用できることを目的としています。 -
Multiplayer Doom Workers: ↗
Workers、WebSockets、Pages、および Durable Objects を使用して Cloudflare のグローバルネットワーク上で実行される、マルチプレイヤー対応の WebAssembly Doom ポートです。 -
Queues Web Crawler: ↗
ブラウザレンダリングと Puppeteer を使用して構築されたウェブクローラー「Queues」のユースケース例です。サイト上の Cloudflare.com へのリンク数を検出し、スクリーンショットを Workers KV にアーカイブします。 -
Pages Functions with WebAssembly: ↗
Pages Functions のコード内で Wasm モジュールのインポートを利用する例を示すデモアプリケーションです。

Remix の Cloudflare Pages のデフォルトのテンプレートだと、/functions/[[path]].ts
になにもしない Pages Functions が定義されている。

Remix x Cloudflare Pages の全体像分かってきた気がする。
functions/[[path]].js
(build/server/index.js
(entry.server.tsx
からトランスパイルしたもの)を読み込む) は誰に認識されるんだろう。
Cloudflare Pages 側で認識して、functions
配下に存在すればそれを実行するとかやってるのかな。
おっしゃる通りです。全体像の中でこれらのファイルがどう扱われるかという点は重要なポイントです。by Claude 3.7 Sonnet
functions/[[path]].js
と build/server/index.js
の認識と実行の仕組み
Cloudflare Pages の処理フロー
-
functions/[[path]].js
の認識:- Cloudflare Pages のインフラストラクチャは、デプロイされたプロジェクト内で
functions/
ディレクトリを特別に扱います -
[[path]]
というファイル名はワイルドカードルートを意味し、あらゆるパスへのリクエストをこのファイルで処理できることを示します - このファイルは Cloudflare Pages Functions の仕様に基づいて自動的に検出されます
- ルートディレクトリの認識は、Cloudflare 管理コンソール > Workers & Pages > <project> > Settings > Build で設定を確認できる。
- Cloudflare Pages のインフラストラクチャは、デプロイされたプロジェクト内で
-
実行順序:
- リクエストが Cloudflare のエッジネットワークに到達すると、まず静的ファイル(
build/client/
内のアセット)があるか確認- ここ違う気がするけど、、静的ファイルがあってもなくても
functions/[[path]].js
を通っている
- ここ違う気がするけど、、静的ファイルがあってもなくても
- 静的ファイルがない場合、
functions/[[path]].js
が実行される - この仕組みは Cloudflare Pages のインフラ側で自動的に行われる「規約」です
- リクエストが Cloudflare のエッジネットワークに到達すると、まず静的ファイル(
build/server/index.js
の役割
-
誰が認識するか:
- このファイルは直接 Cloudflare に認識されるのではなく、
functions/[[path]].js
から参照されます - つまり、
functions/[[path]].js
内のコードが次のようにbuild/server/index.js
を読み込みます:
import * as build from "../build/server/index.js"; const handleRequest = createPagesFunctionHandler({ build, // ここで参照 // ... });
- このファイルは直接 Cloudflare に認識されるのではなく、
-
実行の流れ:
- Cloudflare Pages が
functions/[[path]].js
を検出し実行 - このファイルが
build/server/index.js
を import -
build/server/index.js
には Remix の SSR に必要な情報(ルート構造など)が含まれている -
createPagesFunctionHandler
関数がこの情報を使ってリクエスト処理をセットアップ
- Cloudflare Pages が
図解: ファイル間の関係と実行フロー
Cloudflare Pages の規約ベースの動作
Cloudflare Pages は規約ベースのプラットフォームで、特定のディレクトリ構造やファイル名に特別な意味を持たせています:
-
functions/
ディレクトリ:- このディレクトリ内のファイルは、サーバーサイド関数として自動的に認識されます
-
[[path]].js
や[param].js
のような命名規則は、Cloudflare Pages の規約としてルーティングパターンを表します
-
build/
の扱い:-
build/client/
はデプロイ時に静的アセットとして扱われる -
build/server/
はサーバーサイド実行のためのバンドルとして扱われ、直接クライアントにサーブされることはない
-
-
ビルドプロセスとの関係:
- Remix のビルドプロセスは上記の Cloudflare Pages の規約に合わせて出力を生成します
-
remix.config.js
で適切な設定(serverBuildTarget: "cloudflare"
)を行うことで、Cloudflare Pages に最適化されたビルド出力になります
まとめ
-
functions/[[path]].js
は Cloudflare Pages によって自動的に検出され、動的リクエストのエントリーポイントとして機能します - このファイルが
build/server/index.js
を import し、Remix のサーバーサイドレンダリング機能を利用します - この全体の流れは Cloudflare Pages の規約と Remix のビルド設定によって制御されています
この仕組みにより、Remix アプリケーションが Cloudflare のエッジネットワーク上でシームレスに動作することが可能になっています。

規約ベースって英語ではなんと呼ばれるのが一般的?他のプラットフォームの例はあったりする?ぱっと思い浮かぶのは Ruby on Rails。
規約ベースのプログラミングやシステム設計は英語では主に "Convention over Configuration" (設定より規約) と呼ばれています。このアプローチは、特定の命名規則や構造に従えば、詳細な設定を明示的に指定する必要がなく、システムが自動的に適切な振る舞いをするという概念です。おしゃるとおり Ruby on Rails は代表的な規約ベースの Web フレームワークです。
他のプラットフォームの例
-
Ruby on Rails:
- 最も有名な Convention over Configuration を採用したフレームワーク
- モデル名が単数形なら、テーブル名は自動的に複数形になる
-
app/controllers
,app/models
,app/views
などのディレクトリ構造で役割を規定
-
Laravel (PHP):
- Ruby on Rails に影響を受けた PHP フレームワーク
- 命名規則に基づいてモデルとテーブルを関連付け
- ディレクトリ構造がアプリケーションの構成要素を決定
-
Spring Boot (Java):
- Java エコシステムでの Convention over Configuration の例
- アノテーションと命名規則によって多くの設定を省略可能
- 特定のパッケージ構造とクラス命名に基づく自動設定
-
Next.js:
-
pages/
ディレクトリ内のファイル構造がルーティングを決定 -
pages/api/
はAPIエンドポイントとして自動認識 -
_app.js
,_document.js
など特殊なファイル名が特定の役割を持つ
-
-
Netlify:
- Cloudflare Pages と同様のサーバーレスプラットフォーム
-
netlify/functions/
ディレクトリ内のファイルが自動的にサーバーレス関数として扱われる -
_redirects
ファイルがリダイレクトルールを定義
-
Vercel:
-
api/
ディレクトリ内のファイルがサーバーレス関数に - ファイル名とディレクトリ構造に基づくルーティング
- 特殊なファイル(
vercel.json
)による設定の補完
-
このアプローチの利点は、開発者が「魔法のような」設定よりも、明確な規約に従うことで、コードの一貫性が高まり、新しいメンバーがプロジェクトを素早く理解できるようになることです。また、規約に従う限り、多くのボイラープレートコードや設定ファイルを書く必要がなくなります。
Cloudflare Pages も含め、最近のウェブプラットフォームやフレームワークの多くは、この「Convention over Configuration」の原則を採用することで、開発者体験を向上させています。

Remix もこれでいうと routes 配下に置く _index.tsx とか $number のようにパスパラメータ含めるのとかがあるから規約ベースのフレームワークなのかな
おっしゃる通りです!Remix も典型的な規約ベース(Convention over Configuration)のフレームワークです。Remix のファイルシステムベースのルーティングは、規約ベースの設計の良い例です。
Remix の規約ベースの特徴
-
ファイルシステムベースのルーティング:
-
routes/_index.tsx
→ ルートパス(/
)に対応 -
routes/about.tsx
→/about
パスに対応 -
routes/blog/$slug.tsx
→/blog/任意の値
のようなダイナミックルート -
routes/blog.tsx
→/blog
パスのレイアウトまたはインデックスページ
-
-
特殊なプレフィックスとその意味:
-
$
プレフィックス: ダイナミックパラメータを表す ($number
,$slug
など) -
_
プレフィックス: レイアウトルート、または特殊なページ(_index.tsx
はルートパス用) -
.
を含むファイル名: ネストしたルート構造(blog.categories.tsx
など)
-
-
特殊なファイル:
-
root.tsx
: アプリケーション全体のシェル -
entry.client.tsx
: クライアントサイドのエントリーポイント -
entry.server.tsx
: サーバーサイドのエントリーポイント
-
-
規約に基づく関数:
-
loader()
: データロード用の関数 -
action()
: フォーム送信などのアクション用関数 -
ErrorBoundary
: エラー処理用コンポーネント -
CatchBoundary
: 特定の HTTP ステータスを捕捉するコンポーネント
-
これらの規約に従うことで、Remix はルートの定義や設定ファイルを最小限に抑え、開発者はファイルを作成するだけで自動的に適切な場所にルーティングされるようになっています。
他のフレームワークとの比較
Remix の規約ベースのアプローチは、Next.js にも似ていますが、いくつかの点で異なります:
-
Next.js:
pages/
ディレクトリがルーティングのルートで、[param]
でダイナミックルートを表現 -
Remix:
routes/
ディレクトリがルーティングのルートで、$param
でダイナミックルートを表現
どちらも規約ベースですが、表記法や振る舞いに若干の違いがあります。
規約ベースのアプローチは、コードの量を減らし、開発者の意図を明確にする利点があります。Remix の場合、ファイル名や配置がそのままURLやデータの流れに直結するため、プロジェクト構造を見るだけでアプリケーションの概要が把握しやすくなっています。

Prompt: entry.server.tsx を見ると SSR してるんだろうなというのがコードから分かるけど、entry.client.tsx による hydration がどうやってクライアントサイドでロードされるのかはまだ分かってない。
entry.server.tsx
/**
* 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";
const ABORT_DELAY = 5000;
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 controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY);
const body = await renderToReadableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
signal: controller.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
// Log streaming rendering errors from inside the shell
console.error(error);
}
responseStatusCode = 500;
},
}
);
body.allReady.then(() => clearTimeout(timeoutId));
if (isbot(request.headers.get("user-agent") || "")) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
entry.server.tsx
/**
* By default, Remix will handle hydrating your app on the client 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.client
*/
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

なるほど完全理解。
- ビルドする(
remix vite:build
) -
build/client/assets
にビルド成果物が生成される➜ tree build/client build/client ├── _headers ├── _routes.json ├── assets │ ├── _index-CWhSQhKm.js │ ├── components-CWrbwnVj.js │ ├── entry.client-CHmt2Ukc.js │ ├── jsx-runtime-0DLF9kdB.js │ ├── manifest-0eef3245.js │ ├── root-CS6YCbpS.css │ └── root-DvypXt6e.js ├── favicon.ico ├── logo-dark.png └── logo-light.png 2 directories, 12 files
- Cloudflare Pages Functions にて、サーバサイドレンダリングしたときに
root.tsx
の<Scripts />
コンポーネントによってbuild/client/assets/xxx.js
というビルド成果物を読み込むための<script>
タグが追加される(下記ソースコード参照) - Cloudflare Pages Functions から HTML を返す
- クライアントサイド側で CSS, JS を読み込む(「JS を読み込む」を実行して Web ページを JavaScript によるインタラクションを有効化することをハイドレーション hydration という)
- これしかなかった、、
root.tsx
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" data-theme="fukabory">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="min-h-screen bg-base-100">
<header className="bg-base-200 shadow">
<div className="container mx-auto px-4 py-4 flex items-center">
<Link to="/" className="text-2xl font-bold text-primary">
fukabory
</Link>
</div>
</header>
<main className="container mx-auto px-4 py-6 text-base-content">
{children}
</main>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
Answer: entry.server.tsx を見ると SSR してるんだろうなというのがコードから分かるけど、entry.client.tsx による hydration がどうやってクライアントサイドでロードされるのかはまだ分かってない。
Remix におけるクライアントサイドハイドレーションの仕組み
entry.client.tsx
がどのようにクライアントサイドで実行されるかは、Remix のビルドプロセスとサーバーレンダリングの仕組みによって実現されています。詳細な流れを説明します。
クライアントサイドハイドレーションの流れ
詳細な解説
entry.client.tsx
1. ビルドプロセスにおける Remix のビルドプロセスでは、entry.client.tsx
は次のように処理されます:
-
バンドル化:
entry.client.tsx
ファイルはビルド時にクライアント用のJavaScriptバンドルに変換されます -
出力場所: バンドルは
build/client/assets/
ディレクトリに生成されます(あなたのプロジェクト構造ではbuild/client/assets/entry.client-CHmt2Ukc.js
のようなファイル)
2. HTML内のスクリプト参照
サーバーサイドレンダリング中に、root.tsx
の <Scripts />
コンポーネントが重要な役割を果たします:
// root.tsx (一部)
export default function App() {
return (
<html lang="ja">
<head>
{/* その他のhead要素 */}
<Links />
</head>
<body>
<Outlet />
<Scripts /> {/* これが重要 */}
<LiveReload />
</body>
</html>
);
}
この <Scripts />
コンポーネントは、サーバーレンダリング時に次のようなHTMLに変換されます:
<!-- Remixによって生成されるHTMLの一部 -->
<script src="/assets/entry.client-CHmt2Ukc.js" type="module"></script>
<script src="/assets/manifest-0eef3245.js" type="module"></script>
<script src="/assets/root-DvypXt6e.js" type="module"></script>
<!-- その他の必要なスクリプト -->
3. ブラウザでの実行
- ブラウザがHTMLを受け取り、解析します
-
<script>
タグが処理され、クライアントサイドのJavaScriptが読み込まれます -
entry.client-CHmt2Ukc.js
(バンドル化されたentry.client.tsx
)が実行されます -
hydrateRoot
関数によって、既に存在するDOM要素(サーバーレンダリングによって生成されたもの)に対してReactの「ハイドレーション」が行われます
4. ハイドレーションとは
ハイドレーションは、サーバーレンダリングされた静的HTMLにインタラクティブな機能を追加するプロセスです:
// entry.client.tsx の内容
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
この処理によって:
- サーバーで生成されたHTMLはそのまま利用される(再レンダリングしない)
- イベントリスナーがDOMに追加される
- Reactのコンポーネント状態が初期化される
- クライアントサイドの動的機能(ユーザーインタラクション、フォーム送信など)が有効になる
重要なポイント
-
プログレッシブエンハンスメント: Remixはプログレッシブエンハンスメントの原則に従っています。JavaScriptが無効でもHTMLコンテンツは表示され、JavaScript実行が可能な環境ではより豊かな体験が提供されます。
-
ハイドレーションの最適化:
startTransition(() => { // ハイドレーションコード });
React 18の
startTransition
APIを使用して、ハイドレーションプロセスを低優先度のタスクとして実行し、メインスレッドをブロックしないようにしています。 -
Remixの魔法:
-
<Scripts />
コンポーネントはビルド時に自動的に必要なスクリプトタグに変換される -
entry.client.tsx
は明示的に参照されなくても、ビルドプロセスによってアプリケーションに統合される
-
あなたのプロジェクトでの具体的な処理
- ユーザーがページにアクセスすると、Cloudflare Pages Functions が
functions/[[path]].ts
を実行 - Remix SSRが実行され、
<Scripts />
コンポーネントを含むHTMLを生成 - ブラウザがHTMLと必要なアセット(CSS、JavaScript)を読み込む
-
build/client/assets/entry.client-CHmt2Ukc.js
が実行される -
RemixBrowser
コンポーネントがハイドレーションを実行し、アプリケーションがインタラクティブになる
この仕組みにより、サーバーレンダリングの速いコンテンツ表示とクライアントサイドのインタラクティブな機能の両方を実現しています。

おれは何も理解してなかった。SSR streaming というのがあるらしい。
- Prompt
これしかなかったよ script タグ
<script>window.__remixContext.streamController.enqueue("[{\"_1\":2,\"_5\":-5,\"_6\":-5},\"loaderData\",{\"_3\":-5,\"_4\":-5},\"root\",\"routes/_index\",\"actionData\",\"errors\"]\n");</script>
- Answer
なるほど、興味深い発見ですね。Remixの新しいバージョンでは、ハイドレーション方法が変わっているようです。お見せいただいたスクリプトタグは、RemixのストリーミングSSRの一部として使用されています。
Remix の最新のハイドレーション方法
新しいハイドレーション方法の解説
お見せいただいたコードは、Remixの最新バージョン(v2以降)で採用されている「ストリーミングSSR」と「プログレッシブハイドレーション」の仕組みを表しています。
<script>window.__remixContext.streamController.enqueue("[{\"_1\":2,\"_5\":-5,\"_6\":-5},\"loaderData\",{\"_3\":-5,\"_4\":-5},\"root\",\"routes/_index\",\"actionData\",\"errors\"]\n");</script>
このスクリプトの役割
-
ストリーミングデータの処理:
-
window.__remixContext.streamController.enqueue()
は、サーバーからストリーミングされたデータをクライアントで処理するための関数です - これにより、ページ全体が読み込まれる前に部分的なデータを受け取り、UIを段階的に更新できます
-
-
JSON形式のデータ:
- スクリプト内の文字列は、ルートデータや状態に関する情報を含む圧縮されたJSON形式です
-
"loaderData"
,"root"
,"routes/_index"
,"actionData"
,"errors"
などの識別子を含んでいます
新しいハイドレーション方法の流れ
-
初期HTMLの配信:
- サーバーは最小限の初期HTMLと必要なインラインスクリプトを返します
- この中には、JS資産を読み込むためのコードと
window.__remixContext
オブジェクトの初期化が含まれています
-
バンドルの読み込み:
- ブラウザは、インラインスクリプトを通じて必要なJSバンドルを非同期で読み込みます
- これらは
<link rel="modulepreload">
タグや動的importを通じて行われる可能性があります
-
ストリーミングデータの処理:
- サーバーからデータがストリーミングされ、
window.__remixContext.streamController.enqueue()
を通じて処理されます - このデータは、ルートコンポーネントのloaderDataやその他のアプリケーション状態を含みます
- サーバーからデータがストリーミングされ、
-
プログレッシブハイドレーション:
- Remix Browserコンポーネントが初期化され、既存のDOMとストリーミングされたデータを使用してハイドレーションを行います
- ハイドレーションはプログレッシブに行われ、ページ全体が一度にハイドレーションされるのではなく、部分的に行われます
build/client/assets の役割
あなたのプロジェクト構造に見られる build/client/assets/
ディレクトリには、次のようなファイルがあります:
build/client/assets
├── _index-CWhSQhKm.js
├── components-CWrbwnVj.js
├── entry.client-CHmt2Ukc.js
├── jsx-runtime-0DLF9kdB.js
├── manifest-0eef3245.js
└── root-DvypXt6e.js
これらのファイルはまだ使用されていますが、読み込み方法が変わっています:
-
エントリーポイント:
entry.client-CHmt2Ukc.js
は依然としてクライアントサイドのエントリーポイントですが、インラインスクリプトから動的に読み込まれるか、<link rel="modulepreload">
によって事前読み込みされます -
マニフェスト:
manifest-0eef3245.js
はアプリケーションの構造とチャンク情報を含み、動的な読み込みをコントロールします -
ルートとコンポーネント: 他のJSファイルはルートコンポーネントやその依存関係を含み、必要に応じて読み込まれます
まとめ
最新のRemixは、より洗練されたクライアントサイド処理を採用しています:
-
ストリーミングSSR: HTMLとデータを段階的にストリーミングし、初期表示を高速化
-
プログレッシブハイドレーション: ページの一部から順次ハイドレーションを行い、インタラクティビティを段階的に提供
-
ストリーミングコントローラー:
window.__remixContext.streamController
を使用して、サーバーからストリーミングされたデータをクライアントで処理
これにより、初期ロード時間の短縮とユーザー体験の向上が図られています。具体的な実装詳細はRemixのバージョンによって異なる可能性がありますが、基本的な考え方はこのようになっています。

SSR streaming
SSR Streamingは段階的にSSRを実行しクライアントへ配信するレンダリング方法です。
SSRにはAPIサーバーの処理速度などの問題で、クライアントにHTMLが返却されるまでの時間が長くなってしまうというデメリットがあります。
SSR Streamingを使用することで、APIのフェッチが解決するのを待たず、レンダリングできる部分を先にレンダリングして、インタラクティブなページをできるだけ早くユーザーに表示することで、知覚される読み込みパフォーマンスの向上が期待できます。
RemixでもRemixが提供する defer と Await というコンポーネントを使用することで実現が可能です。
この技術ブログでもNext.js時代の全画面SGから、全画面SSRになったのを機にトップページを含む記事一覧とタグに紐づく記事を表示するページで、UXを向上させるために導入しています。

原理は理解できたと思う。
SSR による "完全な" HTML を配信するのではなく、コンポーネント単位で生成された HTML をチャンクにしてクライアントに配信し、クライアントサイドで受け取ったコンポーネントをレンダリングする。
(言うは易しだけど実装はとても大変そう)
以下は、Next.js の「Loading UI and Streaming」ページ内の「What is Streaming?」セクションに基づく SSR ストリーミングの概要です。
SSR における従来のレンダリング
-
従来の SSR の流れ
- サーバー側でページに必要な全てのデータを取得する
- データを元に HTML をレンダリングする
- 生成した HTML、CSS、JavaScript をクライアントに送信する
- 受信した HTML で一旦非対話的な UI を表示し、その後 React がハイドレートして対話性を付与する
-
課題
- 全てのデータ取得やレンダリングが完了するまで次の工程に進めないため、ユーザーが何も表示されず待たされることがある
Streaming による改善
-
ストリーミングの仕組み
- ページ全体の HTML を一度に生成するのではなく、コンポーネント単位(チャンク)に分割し、順次クライアントへ送信
- 優先度の高いコンポーネント(例:レイアウトや主要情報)は先に、低いもの(例:レビューや関連商品の情報)は後から送信
-
メリット
- ユーザーは必要な部分が早期に表示され、全体の読み込みを待たなくても何らかの UI を確認できる。すべてのデータがロードされるのを待たずに、ページの一部が先に表示されるようになる。
- Time To First Byte (TTFB) や First Contentful Paint (FCP) の改善が期待でき、特にデバイス性能が低い環境で Time To Interactive (TTI) の向上に寄与
-
React との連携
- ストリーミングは、React のコンポーネントモデルと非常に相性が良い。なぜなら、各コンポーネントを一つのチャンクとして扱えるから。優先度の高いコンポーネント(例:製品情報)やデータに依存しないコンポーネント(例:レイアウト)は先に送信され、React はより早くハイドレーションを開始できる。一方、優先度の低いコンポーネント(例:レビュー、関連製品)は、そのデータ取得が完了した後、同じサーバーリクエスト内で送信されることが可能。
- React のコンポーネントモデルと
Suspense
機能を活用することで、各コンポーネントを個別にハイドレート(対話可能に)でき、ストリーミングレンダリングと選択的ハイドレーションの両方の恩恵を受けられる。
この仕組みにより、ユーザー体験が向上し、ページの読み込み遅延が緩和されるため、特にデータ取得に時間がかかる場合でも、ユーザーには速やかに視覚的なフィードバックが提供されます。
参考: Next.js Documentation cite367

例:React の Suspense コンポーネントによる SSR streaming
<Suspense>
は、非同期の処理(例: データの取得)を実行するコンポーネントをラップすることで機能する。処理中はフォールバックの UI(例: スケルトンやスピナー)を表示し、処理が完了するとそのコンポーネントに置き換えられる。
app/dashboard/page.tsx
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
Suspense を使用することで、以下の利点が得られる:
-
ストリーミングサーバーサイドレンダリング
- サーバーからクライアントへ HTML を段階的にレンダリングする
-
選択的ハイドレーション
- React はユーザーの操作に基づいて、どのコンポーネントを先に対話可能にするかを優先する
Suspense
のその他の例や利用方法については、React のドキュメントをご参照:

技術選定

Remix Vite のビルドプロセス
PostCSS というものを知らなかった。
Remix と Vite によるビルドフローを図解します。特に PostCSS の役割を含めて説明しましょう。
Remix + Vite ビルドフローの解説
主要コンポーネントの役割
1. Vite
- 開発サーバー: 高速なHMR(Hot Module Replacement)を提供
- ビルドツール: 本番用に最適化されたバンドルを生成
- 依存関係の解決: import/exportを解決し、必要なモジュールを集める
2. PostCSS
PostCSSは「CSSのための郵便処理システム」と考えるとわかりやすいです:
- プラグインシステム: CSSを変換するためのプラグインを適用する基盤
- モダンCSS変換: 最新のCSS構文を現在のブラウザが理解できる形に変換
-
プラグイン例:
- Autoprefixer: ベンダープレフィックス(-webkit-など)を自動追加
- cssnano: CSSの最小化・最適化
3. Tailwind CSS
- ユーティリティファースト: 実はTailwindも「PostCSSプラグイン」の一つ
- 処理の流れ: クラス名のスキャン → 必要なCSSの生成 → 未使用CSSの削除
4. Remix
- Server/Client分離: サーバーコードとクライアントコードを分離して最適化
- ハイドレーション: サーバーからのHTMLにクライアントJSを「水分補給」
ビルドフロー解説
-
開発時:
- Viteの開発サーバーが起動し、オンデマンドでファイルをコンパイル
- JSX/TSXはesbuildで高速変換
- CSSはPostCSSパイプラインで処理(TailwindもここでCSSを生成)
-
本番ビルド時:
- すべてのソースファイルが収集され処理される
- CSSは最適化(未使用削除、圧縮)
- Remixがサーバー用とクライアント用のコードを分離
- 最終バンドルが生成される
Viteはesbuildを使ってJSのトランスパイルを高速化し、Remixはルーティングとサーバー/クライアント分離を担当します。PostCSSはCSSの変換パイプラインとして機能し、その中でTailwind CSSは「使う分だけ」のユーティリティクラスを生成します。

vite、esbuild ってなに?
Vite と esbuild は、モダンな Web 開発の中核となるビルドツールです。それぞれの役割を簡単に説明します。
esbuild
esbuild は非常に高速な JavaScript バンドラー・ミニファイアです。
-
特徴: Go 言語で書かれており、従来の Node.js ベースのツール(webpack や Babel など)よりも桁違いに高速
-
主な機能:
- JavaScript/TypeScript のトランスパイル
- コードの圧縮(ミニファイ)
- モジュールのバンドル(複数のファイルを一つにまとめる)
- JSX の変換
-
速さの秘密:
- マルチスレッド処理
- インタプリタ言語ではなくコンパイル言語(Go)で実装
- 最適化されたアルゴリズム
Vite
Vite は Vue.js の作者 Evan You が開発した、次世代のフロントエンドビルドツールです。
-
特徴: 開発時は高速なサーバーとHMR(Hot Module Replacement)、本番ビルド時は Rollup を使用
-
開発モードの仕組み:
- ES モジュールを直接利用(ブラウザがネイティブにサポート)
- 必要なモジュールだけをオンデマンドで変換
- 初期起動が非常に速い
-
本番ビルドの仕組み:
- Rollup(または設定によって esbuild)を使用して最適化されたバンドルを生成
- コード分割、遅延ロードなどの最適化
-
esbuild との関係:
- Vite は内部で esbuild を使用して JavaScript の変換を行う
- 開発サーバーで TypeScript や JSX の変換に esbuild を活用
簡単に言えば、esbuild は「高速なコンパイラエンジン」で、Vite は「それを使った統合開発環境」という関係です。Vite は esbuild の速さを活かしながら、より使いやすい開発体験を提供しています。

Remix のビルドシステムについて
Vite
Vite is a powerful, performant and extensible development environment for JavaScript projects. In order to > improve and extend Remix's bundling capabilities, we now support Vite as an alternative compiler. In the future, Vite will become the default compiler for Remix.
Classic Remix Compiler vs. Remix Vite
The existing Remix compiler, accessed via the remix build and remix dev CLI commands and configured via remix.config.js, is now referred to as the "Classic Remix Compiler".
The Remix Vite plugin and the remix vite:build and remix vite:dev CLI commands are collectively referred to as "Remix Vite".
Moving forwards, documentation will assume usage of Remix Vite unless otherwise stated.
この文章は Remix の公式ドキュメントからの引用で、Remix のビルドシステムの変遷について説明しています。
主なポイントは:
-
Vite の採用
- Remix が Vite をビルドシステムとしてサポートするようになった
- 将来的には Vite が Remix のデフォルトコンパイラになる予定
-
コンパイラの区別
- 従来の Remix コンパイラ(
remix build
、remix dev
コマンドで使用)は「Classic Remix Compiler」と呼ばれるようになった - 新しい Vite ベースのシステム(
remix vite:build
、remix vite:dev
コマンドで使用)は「Remix Vite」と呼ばれる
- 従来の Remix コンパイラ(
-
ドキュメントの方針
- 今後のドキュメントは特に断りがない限り、Remix Vite の使用を前提として書かれる
これは Remix が開発環境を近代化し、より高速で柔軟なビルドシステムに移行していることを示しています。つまり、「Classic Remix Compiler」が古いもので、「Remix Vite」が新しいアプローチということになります。ドキュメントにもあるように、Remix は将来的に Vite をデフォルトのコンパイラとして採用する予定です。

vite, esbuild の alternative を教えて。vite, esbuild を使っているのだけど他にはどんな物があるのか知っておきたい。
モダンな Web 開発ツールの世界には Vite と esbuild 以外にもたくさんの選択肢があります。それぞれの代替ツールを見ていきましょう。
Vite の代替ツール
-
webpack
- 最も広く使われている伝統的なバンドラー
- 非常に柔軟でカスタマイズ性が高い
- プラグインエコシステムが豊富
- Viteより起動が遅く、HMRもやや遅い
-
Parcel
- ゼロコンフィグが特徴
- 設定ファイルなしで使える
- 自動的に依存関係を検出
- Viteほど高速ではない
-
Snowpack
- Viteと同様にESモジュールをベースとした開発サーバー
- 開発が停滞気味(最近はアクティブではない)
-
Turbopack
- Vercelによる次世代バンドラー(Rustで書かれている)
- Next.jsの新しいコンパイラとして導入予定
- まだ発展段階だが、非常に高速
-
wmr
- Preactチームによる小型の開発ツール
- 単一の実行ファイルで動作
esbuild の代替ツール
-
SWC (Speedy Web Compiler)
- Rustで書かれた高速なJavaScript/TypeScriptコンパイラ
- Next.jsでも採用されている
- Babelの高速な代替ツール
- esbuildと同等かそれ以上の速度
-
Babel
- 伝統的なJavaScript変換ツール
- 非常に柔軟でプラグインが豊富
- esbuildよりかなり遅い
- 複雑な変換に対応
-
tsc (TypeScript Compiler)
- 公式TypeScriptコンパイラ
- 型チェックと変換を行う
- esbuildより遅い
-
Bun
- JavaScriptランタイム、パッケージマネージャー、バンドラーを一体化
- Zigで書かれた高速なツール
- esbuildと同様に非常に高速
- バンドルツールとしての機能も持つ
トレンドと選択肢
現在のトレンドとしては:
- 高速化: Rustや低レベル言語で書かれたツールへの移行
- 開発体験の向上: HMRの高速化、デバッグツールの改善
- 統合化: ランタイム、ビルドツール、パッケージマネージャーの統合(Bunなど)
Viteとesbuildは確かに主流となっていますが、プロジェクトの要件によっては他のツールが適している場合もあります。特に大規模プロジェクトではwebpackの柔軟性が必要な場合や、Next.jsプロジェクトではTurbopackの採用を検討するケースも増えてきています。

Remix で CSR を行う
Excalidraw の React コンポーネントを Remix で使おうとしたら SSR 時にブラウザ API にアクセスできなくて document is not defined
というエラーになった。Excalidraw はロード時にブラウザ API を使用するため、SSR できない。
この issue の内容は、Remix のコンポーネントがすべてクライアントコンポーネントとして扱われる点と、それに起因する SSR(サーバーサイドレンダリング)時の問題について説明しています。以下にポイントごとに解説します。
1. Remix と RSC の違い
-
React Server Components (RSC) の非対応
Remix は RSC をサポートしていません。つまり、Next.js のようにサーバーコンポーネントとクライアントコンポーネントを分けて使うことはできず、すべてのコンポーネントはクライアントコンポーネントとして扱われます。
.client
ファイルの役割
2. -
.client
拡張子の意味
.client
拡張子が付いたファイルは、サーバーバンドルに含まれないため、SSR(サーバーサイドレンダリング)時には読み込まれず、ブラウザでのみレンダリングされます。Remix では、Next.js の「use client」ディレクティブのような概念はなく、単にファイルがサーバーに含まれないという違いだけです。
document is not defined
エラーの原因
3. -
SSR での undefined 問題
クライアント専用のコンポーネントを通常の方法でレンダリングしようとすると、サーバーサイドレンダリングの際にそのコンポーネントは存在しない(undefined)ため、内部でdocument
や他のブラウザ固有の API にアクセスしようとするとエラーが発生します。サーバー上にはdocument
オブジェクトは存在しないためです。
4. 解決策: ClientOnly コンポーネントの利用
-
ClientOnly の基本的な使い方
この問題を回避するには、Remix Utils のClientOnly
コンポーネントを使って、クライアント専用コンポーネントのレンダリングをブラウザに遅延させます。これにより、SSR 時には代わりにフォールバック(例:<SSRFallback />
またはnull
)がレンダリングされ、クライアント側でハイドレーション後に本来のコンポーネントが描画される仕組みになります。
import { ClientOnly } from "remix-utils/client-only"
<ClientOnly fallback={<SSRFallback />}>
{() => <YourComponent />}
</ClientOnly>
-
fallback の役割
fallback
プロパティは SSR 時に一時的に表示される内容です。fallback
をnull
にすると、ハイドレーション後にコンテンツが後からレンダリングされるため、場合によってはレイアウトシフトが発生する可能性があります。
まとめ
- Remix は RSC をサポートしていないため、全コンポーネントがクライアントコンポーネントとして扱われる。
-
.client
拡張子はファイルをサーバーバンドルから除外し、SSR 時に読み込まれなくなる。 - SSR 時にクライアント専用コンポーネントをレンダリングしようとすると、
document is not defined
などのエラーが発生する。 - この問題を解決するために、
ClientOnly
コンポーネントを使ってクライアント専用コンポーネントのレンダリングをブラウザ側に遅延させる。
このような仕組みにより、SSR とクライアントサイドのハイブリッドなレンダリングが実現され、ブラウザ固有の API に依存するコンポーネントがエラーなく動作するようになります。

これって SSR するときに一度全てのコンポーネントが SSR されてコードが実行されるということ?それをブラウザ側でのレンダリングに寄せるために .client.tsx が必要ということ?
基本的にはその理解で概ね正しいです。
-
SSR(サーバーサイドレンダリング)時の挙動
Remix は React Server Components をサポートしていないため、通常のコンポーネントはサーバー側でレンダリングされ、コードが実行されます。しかし、ブラウザ依存のコード(たとえば、document
やwindow
を利用する場合)はサーバー上では存在しないため、そのままレンダリングするとエラーになります。 -
.client.tsx の役割
.client.tsx
拡張子を付けたファイルはサーバーバンドルから除外され、SSR 時には読み込まれません。つまり、これらのコンポーネントはサーバー側で実行されず、クライアント側(ブラウザ)でのみレンダリングされるように制御されます。 -
ClientOnly コンポーネントの利用
もしコンポーネントが .client.tsx になっているにもかかわらず、その部分を SSR しようとすると undefined エラーが出るので、ClientOnly
コンポーネントでラップして、SSR 時は一時的にフォールバックをレンダリングし、ブラウザ側で改めてそのコンポーネントをレンダリングするという仕組みを取ります。
まとめると、SSR では通常全てのコンポーネントがレンダリングされますが、ブラウザ依存のコードを含むコンポーネントは、.client.tsx を使うか、または ClientOnly コンポーネントでラップして、ブラウザ側でのレンダリングに寄せる必要がある、ということになります。

React Server Components てなに?
React Server Components(RSC)は、React の新しいレンダリングアーキテクチャの一部で、コンポーネントのレンダリングをサーバー側で実行できる仕組みです。以下、主要なポイントを解説します。
基本的な概念
-
サーバーサイドレンダリングとの違い
従来の SSR では、コンポーネントをサーバー上で HTML にレンダリングしてクライアントに送るため、初回表示が高速になったり SEO に有利だったりします。しかし、全てのロジックや状態をサーバーで処理するため、ブラウザ固有のコードは利用できません。 -
React Server Components の特徴
RSC では、コンポーネントをサーバー側で実行し、その結果をクライアントにストリーミングすることが可能です。これにより、クライアント側に送る JavaScript の量を減らし、パフォーマンスを向上させる狙いがあります。また、サーバー上でデータフェッチや重い計算処理を実行でき、クライアント側ではその結果だけを受け取る設計になります。
利点と留意点
-
パフォーマンスの向上
サーバー側で重い処理を済ませることで、クライアント側の負荷を軽減できるため、特にモバイル環境などでパフォーマンス向上が期待されます。 -
セキュリティ
サーバー側でデータを処理するため、クライアントに不要なロジックや機密情報を渡さずに済むというメリットもあります。 -
ブラウザ固有の API の制限
サーバー上ではwindow
やdocument
といったブラウザ固有のオブジェクトが使えないため、こうしたコードを含む場合は、通常のクライアントコンポーネントとして扱う必要があります。これが Remix などで.client
拡張子を使う理由のひとつでもあります。 -
開発者体験
RSC を使うと、クライアントとサーバーでコードを分離する必要があり、設計やデバッグの際に新たな考慮事項が増えることもあります。Next.js などでは「use client」というディレクティブを使ってクライアント専用コンポーネントを明示的に区別するのに対し、RSC はこの区別をより自然に行おうとする仕組みです。
まとめ
React Server Components は、サーバー側でコンポーネントを実行することで、クライアント側に送る JavaScript の量を削減し、パフォーマンスやセキュリティを向上させる新しいレンダリング手法です。ただし、ブラウザ依存の処理ができなくなるため、適切にクライアントコンポーネントと切り分ける工夫が必要になります。

process is not defined
Excalidraw React Integration: