Next.js, Typescript, Notionで個人ブログを開発しました!のときの開発メモです。
*あくまでも私のメモですので、私の間違った理解や解釈が含まれている可能性もあります。閲覧される方は、引用元のダブルチェックのもとご参考にしてください。
このブログは、Next.js 12で作られていて、SSG(Static Site Generation)とSSR(Server Side Rendering)の”組み合わせ技”で作られています。
SSGとSSRの違いについては、リクエスト時にレンダリングするのか、ビルド時にレンダリングするのかの違いと思っておけばいいかと思います。
*当ブログは、ISR(Incremental Static Regeneration)を実装することで上記の組み合わせ技のようなものを実現しています。
Notionデータベースは、以下のようになっています。
ここでpublishedにチェックマークをいれたものがビルド時にSSGされ、リクエスがきたときにはSSRされてブラウザ上に表示されます。
このまま、一番左にあるPageカラムに記事をそのまま書いてpublishできちゃうので普段notionを使っている人にとっては、超絶楽です。こちらがヘッドレスCMSの代わりになっています。
しかし、開発過程でNextjs, Typescript, nodejsを触っていて、負に落ちない部分が沢山でてきました。
そもそも、レンダリングの仕組みがわからん、と。俺が知ってるレンダリングは、毎回リクエスト投げてjsファイルもらって描画するやつや。そんでブラウザ上でデバックしちゃうやつ。ってことでまずは、ブラウザの仕組みの復習からはじめました。
大枠の前提知識として、WebブラウザがWebサーバへ向けてデータを送信するときは、 OSI参照モデルのアプリケーション層から物理層に向けて処理が行われ、逆にWebサーバがWebブラウザからのデータを受信するときは OSI参照モデルの物理層からアプリケーション層に向けて処理が行われます。
[1] 引用元:Constructing the Object Model
[2] 引用元:Render-tree Construction, Layout, and Paint
Browser Process:
Render Process:
Plugin Process:
GPU:
Chromium ではマルチプロセスアーキテクチャが採用されています。まず各タブに相当する Renderer プロセスというものがいます。Blink によるレンダリング処理や JavaScript の実行などは Renderer プロセスの中で行われます。一方、ネットワークやディスクへのアクセスといった特権を必要とする処理はメインプロセスである Browser プロセスで行われます。Browser プロセスと Renderer プロセスは 1:n の関係になっています。この他にも GPU プロセスや Plugin プロセスなどがあります。 ... Browser プロセスと Renderer プロセスはそれぞれ browser/ ディレクトリと renderer/ ディレクトリに対応しています。プロセスモデルに従い、これらディレクトリはソースコードレベルでお互いに依存することはできず、IPC (Inter-Process Communication) 機構によってやりとりを行います。common/ は browser/ と renderer/ で共通して使われるヘッダーファイルなどが格納されていて、例えば IPC でやりとりされる enum や構造体、共通して呼び出されるヘルパー関数などが定義されています。 (引用:Chromium のソースコードの歩き方)
Chromeのディレクトリは下記
src/
- base/
- chrome/
- browser/
- common/
- renderer/
- content/
- browser/
- common/
- renderer/
- third_party/
- WebKit/
- blink/
- v8/
下記のスライドもとても参考になりました。面白くて食い入って読みました。
こちらの動画もどうぞ
レンダリングには、クライアントサイドで行うレンダリングとサーバーサイドでレンダリングする場合の二通りがあります。下記は、Reactベースのフレームワークを前提として書いていきます。
クライアントPC上の開いているブラウザ上のレンダリングエンジンをつかってレンダリングする。もっというと、Chromeつかっていたら、Blinkでレンダリングする。こちら上記で述べたRender Processで行っている内容です。これをいわゆる、CSR(Client Side Rendering)という。ブラウザの仕組みを理解していたら言葉の通りなだけですね。
CSRの逆でサーバー側で”レンダリング”をします。ダブルクォーテーションをつけたのは、ちょっとCSRで起きていることと同じような感じと認識していると理解しずらいからです。下記のチャートにあるようにReactDOMserver.renderToStringでReactで要素をHTML へと変換し、ブラウザ側に送り、ブラウザ側でもReactDOM.renderで差分をマウントし、描画しています。結構な処理を実装しなければいけませんし、CPU負荷もあるというボトルネックがありますが、Nextjsでは、そのあたりが改善されています。
NextjsでもReactDOMは使われています。ラッピングしている箇所はここです。しかし、微妙に”レンダリング”の仕方が違います。Nextjsの場合は、Next.jsはSSRの結果を画面へ表示する前にCSRを実行します。また、ReactDOM.render ではなく ReactDOM.hydrateを利用します。CSRの過程でSSRによって作成されたHTMLをクライアント側で再利用するReactDOM.hydrate()を利用して実行します。そして、ReactDOM.hydrate()はSSRとCSRのレンダリング結果が一致することを期待し、結果が一致する場合はCSRによる再レンダリングをスキップ、不一致の場合は再レンダリングをします。
ここで言えるのは、クライアント側でもサーバー側でもReactDOMが動いています。前者のJavaScriptの実行環境は、V8です。後者の実行環境は、nodejsです。つまり、実行環境が異なるにもかかわらず、クライアントサイドでもサーバーサイド側でも動くように設計していこうという考えのもとに作られているようです。その考え方がIsomorphicやUniversal JavaScriptと呼ばれるようです。
アプリのビルド時にあらかじめページ表示に必要なデータを取得し、各ページごとに静的なHTMLファイルを出力しておく。
*Generatorなので「SSGする」とは言えなく言語化が不便なのとNextjs公式ではSSGという単語は使っていないので、以後「SG(Static Generation)する」っていう表現を使います。厳密には、SSがServer SideでSGにはその意味合いが単語には含まれていませんが、日本語文章のコンテキストに沿って英語のまま理解すればいいと思うので一旦厳密な差分については言語化をサボります。
英語がちょっと難しいですね。直訳すると段階的な静的再生成です。インクリメンタル静的再生成と呼んだほうがいいというVercelで働いてる方はおっしゃられています。
私も英語呼んだ時に「Re-」の部分が気になりましたが、その点についてもVercelで働いている方の回答がありました。なるほど。
私は、このtweetで大体ISRが想像できました。あー、つまり乱暴ないい方すると、ビルド時ではなく、リクエストごとにSG(Static Generation)する、って話かな、と。
つまり、SGは速いけど内容を更新させられない。ISRはそれを解決する方法で、SSRとSGのハイブリッドといえるものなのかなと。一方で、それって従来のMPAと何が違うんだろうと思ったので深堀りしました。
ISRではSSRのようにリクエストに応じてHTMLを生成するのですが、生成はバックグラウンドでやりつつ、すでに生成されている古いほうのHTMLを返却します。
具体例をみてましょう。こちらは、当ブログの一部のコードを抜粋したものになります。以下のように実装することでISRを実現しています。
export async function getStaticProps({ params: { slug } }) {
const post = await getPostBySlug(slug)
const blocks = await getAllBlocksByBlockId(post.PageId)
...
...
return {
props: {
post,
blocks,
},
revalidate: 60,
}
}
export async function getStaticPaths() {
const posts = await getAllPosts()
return {
paths: posts.map(post => getBlogLink(post.Slug)),
fallback: 'blocking',
}
}
リクエストがあったときには、getStaticPropsの結果がpropsに格納され、Revalidateで設定した時間、上記のコードでいうと60秒待つとSSRかのごとく、getStaticPathsで取得した[slug]を元にビルドし直し、サーバー側でレンダリングしたHTMLをブラウザ側に返します。同時にCDNのキャッシュも更新されます。
getStaticPaths関数内にあるfallbackオプションでblockingを指定することで、getStaticPaths関数の実行完了を待ちます。
fallbackは、falseだとビルド時に静的ページが作成されなかった場合、その後に静的ページが追加されても404になります。trueだとビルド時に静的ページが作成されなかった場合、その後に静的ページが追加されるとアクセスできます。
*Vercel は Vercel Edge Network という CDN をデフォルトで備えています。Vercel にデプロイすれば、世界各地に存在する Vercel のエッジネットワーク(CDN)に自分のアプリケーションのコンテンツが自動的にキャッシュされ、ユーザーに対して高速にコンテンツを提供することができるようになります。
クライアント側に送る前に事前にサーバー側でHTMLファイルをレンダリングしておくこと。文字通りのそのままです。
Pre-renderingについての正確な理解は、Nextjsの公式ドキュメントの以下ののページを一読ください。
SPAは、Single Page Applicationといって、英語そのままです。毎回、コンテンツ挿入済みのHTMLをブラウザに返すのではなく、SPAの場合は、HTML、JavaScript、CSSが読み込まれるのは初回アクセス時のみ。ブラウザの履歴でブラウザバックができる。また、必要に応じてデーターを非同期でサーバーに問い合わせ、JavaScriptによってブラウザで動的に描画される。従来のWEBアプリのレンダリングまでのフローを比較すると下記になります。
MPA(Multiple or Multi Page Application): 従来のWEBアプリ
SPAの場合のWEBアプリ
SPAの場合、クライアントサイドでコンテンツ更新があるたびにコンテンツをマウントしてレンダリングをする必要があるため、表示速度に影響がでます。また、SNSのクローラーについては、Javascirptを読み込む前、つまり、コンテンツのないHTMLをもとにインデックスしてしまいます。TwitterやFacebookのシェア時のサムネイル画像なんかは、表示ができません。
その解決策として、上記のNextjsの仕様のようなPre-renderingの仕様があります。事前にサーバー側で一度全ページをビルド時にレンダリングしておくことで、両者の課題を解決できます。しかし、これだと画像を差し替えたり、記事の編集を行ったりなどのコンテンツ側の変更を行った場合に、変更後のリクエスト時に新しいコンテンツが反映がされません。そのため、SSRの仕組みをつかってそれらの差分更新を反映するということをしています。
Nextjsでは、これらを下記のAPIで実現しています。ちょっと説明が雑なので詳細は、それぞれの単語にリンク付けされている公式ドキュメントを参照ください。
じゃー、getServerSidePropsとgetStaticProps+getStaticPaths、どちらがいいの、という問いに対しては、これもVercelの方が答えてくれています。
なるほど。swrについても深堀りたいですが、文字数の関係上省略します。
仕事でバックエンドもTypescript書いてるっていうとおそらく、マイクロサービスとBFFアーキテクチャ前提のもとで言っているのかなと思います。BFFとは、フロントエンドのためのバックエンド(サーバ)で以下のチャートをみるとイメージがわきます。
nodejsを起動して、色んなバックエンドから来るデータを受け取りつつ、ユーザーからのインプットデータも受け取りつつ、組み合わせて事前にHTMLレンダリングしておいて好きなように各クライアントごとに表示できる、かつパファーマンスも維持できる設計なのかと解釈しています。WEBブラウザが起動するデバイスが多様化してきた、扱うデータ構造のモーダル化してきたのが起因かなと察しております。
以下は、BFFの代表的な5つのユースケースです。
引用元: BFF(Backends For Frontends)の5つの便利なユースケース
BFFの実践アンチパターンはこちらの記事
具体的な事例として、ZOZOさんのBFFを適用してでてきた課題については参考になるなと思いました。どれぐらいのDAUとDailyのRPSをみて意思決定をしたのかは気になりました。
ぼやき:マイクロサービス。。。。難しいですね。