はじめに
Reactベースのフレームワークを使うと、些細なページでも大量のJavaScriptがクライアントに送られてしまう——そんなもどかしさを感じたことはないでしょうか。ハイドレーションのために不要なコードまで一緒に配信され、初期表示が重くなってしまうケースは少なくありません。
Freshは、この課題に「デフォルトではJSを一切送らない」という思い切った設計で応えるDeno向けのフルスタックWebフレームワークです。本当に対話的な処理が必要な部分だけをピンポイントでハイドレーションすることで、軽量さと開発体験の良さを両立しています。
Freshとは
Freshは、Web標準に基づいて設計された小型・高速・拡張可能なフルスタックWebフレームワークです。サーバー側でHTMLを生成し、対話的なUIが必要な部分にだけJavaScriptを配信する「Islands Architecture(アイランドアーキテクチャ)」を採用しており、Denoランタイムおよび Deno Deploy との組み合わせで真価を発揮します。
主な特徴
- ゼロランタイムオーバーヘッド - デフォルトではクライアントにJavaScriptを一切送信せず、必要な「島(island)」だけを個別にハイドレーションします
- ファイルシステムルーティング -
routes/ディレクトリの構造がそのままURLになる、Next.jsライクな直感的なルーティングです - 設定不要 - ビルド設定ファイルを書かなくても、
deno task devだけで開発サーバーが起動します - TypeScript標準サポート - 追加の設定なしでTypeScriptがそのまま動きます
- Preactベース - 軽量なPreactを内部で利用しており、Reactに近い書き味でコンポーネントを記述できます
インストール
Freshを使うには、まずDeno CLIをインストールしておく必要があります。インストール後、初期化スクリプトを実行するだけで新規プロジェクトを作成できます。
# Deno CLIのインストール(未導入の場合)
curl -fsSL https://deno.land/install.sh | sh
# Freshプロジェクトの作成
deno run -Ar jsr:@fresh/init
# プロジェクトディレクトリへ移動して開発サーバーを起動
cd fresh-project
deno task dev
npmやyarnは不要です。FreshはJSR(jsr:@fresh/core)経由で配布されており、依存関係もDenoが自動的に解決してくれます。
基本的な使い方
Freshではroutes/ディレクトリ内のファイル構造がそのままURLパスに対応します。以下は最もシンプルなルートの例です。
// routes/index.tsx
import { define } from "../utils.ts";
export default define.page(function Home() {
return (
<div class="px-4 py-8 mx-auto max-w-screen-md">
<h1 class="text-3xl font-bold">こんにちは、Fresh!</h1>
<p class="mt-2 text-gray-600">
このページはサーバーでレンダリングされ、クライアントには
追加のJavaScriptが一切送られていません。
</p>
</div>
);
});
動的なパスパラメータも、ファイル名を[param]の形にするだけで受け取れます。
// routes/greet/[name].tsx
import { define } from "../../utils.ts";
export default define.page(function Greet(ctx) {
const { name } = ctx.params;
return <h1>Hello, {name}さん!</h1>;
});
Fresh自体はDenoランタイム専用のフルスタックフレームワークであり、ルーティングやサーバーサイドレンダリングはブラウザ単体では動作確認できません。そのため本記事ではブラウザ実行サンプルの代わりに、実際のプロジェクトにそのまま貼り付けて使えるコード例で解説します。
実践的なユースケース
Islandsによる部分的なインタラクティブ化
Freshの最大の特徴が、この「island(島)」です。ページの大部分は静的なHTMLとして配信しつつ、カウンターやフォームのように状態を持つ部分だけをislands/ディレクトリに切り出すことで、そこだけがクライアントでハイドレーションされます。
// islands/Counter.tsx
import { useSignal } from "@preact/signals";
export default function Counter() {
const count = useSignal(0);
return (
<div class="flex items-center gap-4">
<button
class="px-3 py-1 border rounded"
onClick={() => count.value--}
>
-
</button>
<span class="text-xl font-mono">{count}</span>
<button
class="px-3 py-1 border rounded"
onClick={() => count.value++}
>
+
</button>
</div>
);
}
// routes/index.tsx
import Counter from "../islands/Counter.tsx";
import { define } from "../utils.ts";
export default define.page(function Home() {
return (
<div class="px-4 py-8 mx-auto max-w-screen-md">
<h1 class="text-2xl font-bold">静的なページの中のIsland</h1>
<p class="text-gray-600">この見出しと本文はJSなしで配信されます。</p>
<Counter />
</div>
);
});
islands/配下に置いたコンポーネントだけがJavaScriptとして配信対象になり、それ以外のページ本体は完全に静的なHTMLのままです。
サーバーサイドでのデータ取得(ハンドラー)
ページ表示前にデータを取得してpropsとして渡したい場合は、define.pageにハンドラーを組み合わせます。APIリクエストやDBアクセスはすべてサーバー側で完結し、クライアントに余計なコードは送られません。
// routes/posts/[id].tsx
import { define } from "../../utils.ts";
interface Post {
id: string;
title: string;
body: string;
}
export const handler = define.handlers({
async GET(ctx) {
// 実際にはDBやAPIから取得する処理に置き換えます
const post: Post = {
id: ctx.params.id,
title: `記事タイトル #${ctx.params.id}`,
body: "本文はサーバー側で組み立てられます。",
};
return { data: post };
},
});
export default define.page<typeof handler>(function PostPage({ data }) {
return (
<article class="px-4 py-8 mx-auto max-w-screen-md">
<h1 class="text-2xl font-bold">{data.title}</h1>
<p class="mt-4">{data.body}</p>
</article>
);
});
ミドルウェアによる横断的な処理
認証チェックやロギングなど、複数のルートに共通する処理は_middleware.tsにまとめられます。特定のディレクトリ配下すべてに自動的に適用されるため、個々のルートに処理を重複して書く必要がありません。
// routes/admin/_middleware.ts
import { define } from "../../utils.ts";
export const handler = define.middleware(async (ctx) => {
const isAuthed = ctx.req.headers.has("authorization");
if (!isAuthed) {
return new Response("Unauthorized", { status: 401 });
}
// 認証済みなら後続のハンドラーへ処理を委譲
return await ctx.next();
});
まとめ
Freshは、「デフォルトでJSを送らない」という潔い設計思想と、ファイルベースルーティングによる分かりやすさを兼ね備えたDeno向けフルスタックフレームワークです。Islandsアーキテクチャによって、静的なページと対話的なUIを自然に共存させられるため、パフォーマンスと開発体験を両立させたいプロジェクトにフィットします。
すでにDenoやDeno Deployを使っている、あるいはこれから採用を検討しているなら、まずはdeno run -Ar jsr:@fresh/initで小さなプロジェクトを立ち上げて、Islandsの感覚を実際に触って確かめてみてください。