このサイトについて

このサイトは Bojin Li の個人サイトです。

これまで

年

技術スタック

2019

GitHub Pages + Jekyll + Disqus、Huxpro テーマ

2020

Vercel + Jekyll + Disqus、Huxpro テーマ

2021

Vercel + React 17 + Tailwind CSS + Vite + TypeScript

2022

Vercel + Next.js 12 + Notion API + Giscus + p5 + three.js

2024

Vercel + Next.js 14/15 + Tailwind CSS + shadcn/ui

2026(現在のバージョン、Haku)

Cloudflare Workers + TanStack Start + Vite + Tailwind CSS + Durable Objects

Haku について

Haku は Yohaku を参考にしています。余白や紙の質感、細かなアニメーションが生む雰囲気など、Yohaku の読書体験を大切にする姿勢を引き継ぎ、リッチテキスト記事も同じように美しく表示します。名前の Haku は Yohaku の一部を残したもので、ちょうど私の名前にある「帛」の日本語の読みでもあります。

Yohaku は Next.js ベースですが、このサイトのデプロイ先は Cloudflare Workers で、中国本土特有のネットワーク環境にもできる限り最適化する必要がありました。そこで TanStack Start、TanStack Router、Vite を採用し、Durable Objects も導入しました。Worker 上の SSR 性能を、できる限り低レイヤーまで踏み込んで最適化でき、Next.js の重さや、Vercel 以外のプラットフォームで動かす際の不確実さも避けられます。

バックエンドについては、Yohaku 向けの機能にはこのサイトで使わないものが多く、Haku 独自のカスタマイズや最適化も少なくありません。そのため、Yohaku を参考に CMS を書き直し、プロトコルは Yohaku と組み合わせて使う mx-space にできる限り合わせています。

全体構成

モノレポ内は、Web、Core、Admin の 3 つのアプリに分かれています。

アプリ

役割

技術

デプロイ先

Web

トップページ、記事、固定ページなどの公開サイト

React、TanStack Start / Router、Vite、Tailwind CSS

Cloudflare Workers

Core

コンテンツ、メディア、認証、コメントなどのバックエンドサービス

NestJS、Fastify、Drizzle、PostgreSQL、Redis

自前のサーバー、Dokploy で管理

Admin

コンテンツ管理画面

React、Vite、CodeMirror、Haklex / Lexical

自前のサーバー、Dokploy で管理

Mermaid diagram

3 つのアプリは、TypeScript と Zod による共通のデータ定義で連携しています。この定義を使い、リクエストとレスポンスの境界でもデータを検証します。

サーバーサイドレンダリング

Haku はサーバーサイドレンダリングを使っています。初めてページを開くときは、Worker がルートと言語に応じて必要なデータを取得し、内容を含む HTML を生成して、ストリームとしてブラウザに送ります。スクリプトの読み込みが終わるのを待たずに、先に内容を読めます。

その後、ブラウザで hydration を行い、サーバーが生成したページを引き継いで、ナビゲーションやメニュー、コメントなどの操作を有効にします。以後のサイト内の移動は TanStack Router が処理し、次のページに必要なデータだけを読み込みます。移動のたびにページ全体を再読み込みする必要はありません。

Mermaid diagram

サーバー側の描画とブラウザでの引き継ぎがずれないように、Router、QueryClient、Jotai store はサーバーのリクエストごとに独立して作り、ブラウザ側にもそれぞれのインスタンスを持たせています。そのため、別の読者のデータが混ざることはありません。ページ表示時のアニメーションもルートと合わせて管理し、初回表示、通常の移動、戻る・進むによる復元で、それぞれ動作を変えています。データのプリフェッチやバックグラウンド更新で、アニメーションが勝手に再生されることもありません。

キャッシュ

サイト設定やナビゲーション、トップページの各種集計情報は、すべての読者に共通するため共有できます。これらは Cloudflare の Durable Objects に公開スナップショットとして保存し、同じデータへの同時読み込みは 1 回にまとめています。スナップショットが期限切れになった場合は、保持している古い内容を先に返し、バックグラウンドで更新する stale-while-revalidate 方式で待ち時間を減らしています。また、新しいコンテンツを公開すると、古いスナップショットを期限切れとして扱います。キャッシュの更新時にはバージョンも確認するため、先に始まったリクエストが遅れて完了しても、古いデータで新しいデータを上書きすることはありません。

記事本文はこの共有キャッシュには入れず、毎回、公開済みのコンテンツから読み込みます。HTML とサーバー関数のレスポンスも private にしています。

トップページに表示する各種アクティビティや統計も、考え方は同じです。バックエンドで定期的に集計するため、読者がトップページを開くたびに外部サービスへ問い合わせる必要はありません。外部サービスが一時的に遅くなっても、本文の表示まで遅くなることはありません。

表示速度の最適化

Haku では、表示速度を上げるために次の工夫をしています。

  • サイトの初期データをできるだけ小さくし、ナビゲーションと現在のページに必要な情報だけを送ります。無関係なページの内容は含めません。
  • ルートの loader とクエリキャッシュを連携させ、1 回のページ移動で同じデータを繰り返し読み込むのを減らします。
  • リッチテキスト、地図、3D などの重い依存モジュールは、用途ごとに分割して必要になったときだけ読み込みます。
  • 本文に使うフォントや言語、描画モジュールをあらかじめ用意し、再読み込みや hydration の際に不整合が起きないようにします。
  • 背景や 3D のアニメーションの動作を制御し、ページが非表示のときや、読者が動きを減らす設定にしているときは停止するか、動きのない表示に切り替えます。記事を読むためのリソースをできるだけ圧迫しないようにしています。

また、Chrome 開発者ツールで通信速度を制限し、SSR と初期表示を繰り返し検証しました。運用中のエラーや性能の問題を特定するため、Sentry も導入しています。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...