我々はなぜSSRを使うのか

10.8K Views

March 14, 25

スライド概要

CSR、SSR、SSG、ISR、PPRなどのレンダリング方式と、Cloudflare Workersなどのエッジランタイムについて解説します。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

我々はなぜSSRを使うのか ついでにエッジランタイムも知ろう ashphy @ashphy@mstdn.nere9.help

2.

※私はSSRしてません

3.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ サーバー S3 データベース GET index.html index.html GET script.js script.js GET /api/data data SELECT data

4.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ S3 GET index.html index.html GET script.js index.html サーバー データベース <html> <head> <script type=“module” src=“script.js” /> </head> <body><div id=“root”></div></body> </html> script.js GET /api/data data SELECT data

5.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ S3 GET index.html index.html GET script.js index.html サーバー データベース <html> <head> <script type=“module” src=“script.js” /> </head> <body><div id=“root”></div></body> </html> script.js GET /api/data data ↑ 中身は書かれていない SELECT data

6.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ S3 GET index.html index.html GET script.js index.html サーバー データベース <html> <head> <script type=“module” src=“script.js” /> </head> <body><div id=“root”></div></body> </html> script.js GET /api/data data SELECT data

7.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ サーバー S3 GET index.html index.html GET script.js データベース script.js function App() { useEffect(() => { fetch(”/api/data”) }); } script.js GET /api/data data SELECT data ※よい子はuseEffectでデータフェッチしないようにしましょう

8.

Webページはどのように表示されるか? • CSR: Client Side Rendering ブラウザ サーバー S3 GET index.html index.html GET script.js データベース script.js function App() { useEffect(() => { fetch(”/api/data”) }); } script.js GET /api/data data ⇐ページが表示される SELECT data ※よい子はuseEffectでデータフェッチしないようにしましょう

9.

もっと速くならないか?

10.

もっと速くならないか? • CSR: Client Side Rendering ブラウザ サーバー S3 データベース GET index.html index.html GET script.js script.js GET /api/data 遅いなー data SELECT data

11.

もっと速くならないか? • CSR: Client Side Rendering ブラウザ サーバー S3 GET index.html index.html データベース <ここで返してくれればいいのに GET script.js script.js GET /api/data data SELECT data

12.

サーバでレンダリングすればいい

13.

Server Side Rendering ブラウザ サーバー データベース GET index.html SELECT data HTML生成 index.html

14.

Server Side Rendering ブラウザ index.html GET index.html <html> <head> <script type=“module” src=“script.js” /> </head> <body> <div id=“root”> <h1>Application</h1> <p>Test App</p> </div> index.html </body> </html> サーバー データベース SELECT data HTML生成

15.

Server Side Rendering ブラウザ サーバー index.html GET index.html <html> <head> <script type=“module” src=“script.js” /> </head> <body> <div id=“root”> <h1>Application</h1> <p>Test App</p> </div> index.html </body> </html> データベース SELECT data HTML生成 返ってきたHTMLに 表示内容が書いてある

16.

Server Side Rendering • サーバーでHTMLをレンダリングして返す方式 • メリット • 読み込みが速い • 巨大なJavaScriptを読み込んで実行しなくても内容を表示できる • SEOに有利 • OGPが使える • キャッシュが最適化できる • デメリット • TTFBが遅くなる

17.

巨大なJSを読まなくても表示できる ブラウザ サーバー S3 データベース GET index.html index.html GET script.js CSRはここが巨大になりがち script.js GET /api/data data SELECT data CSRでもWebpackでコード分割してReact.lazyで遅延読み込みとかできる

18.

SEOに有利 • Core Web Vitalsの結果が悪いとランキングが下がる可能性がある 読み込み速度 応答性 視覚的安定性 • どの程度影響があるかは公表されてないが、LCPの結果だけでランキン グは決まらない • まずはコンテンツを充実させろ • 検索エンジンのクローラーに確実にコンテンツを認識させられる Web Vitals

19.

OGPが使える • Open Graph Protocol: OGPとは コレ

20.

OGP: Open Graph Protocol • OGPは参照元ページの<head>に<meta>で書かれている • XにURLを貼るとXのクローラーがこれを取りに来る <html> <head> <meta name="og:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:site" content="@DocswellSlide" /> <meta name="twitter:title" content="You Don&#039;t Need Next.js | ドクセル" /> <meta name="twitter:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:image" content="https://bcdn.docswell.com/page/YE6YN3VGJV.jpg <meta name="twitter:label1" content="Published by" /> <meta name="twitter:data1" content="ashphy" /> </head> </html>

21.

OGP: Open Graph Protocol • OGPは参照元ページの<head>に<meta>で書かれている • XにURLを貼るとXのクローラーがこれを取りに来る <html> <head> <meta name="og:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:site" content="@DocswellSlide" /> <meta name="twitter:title" content="You Don&#039;t Need Next.js | ドクセル" /> <meta name="twitter:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:image" content="https://bcdn.docswell.com/page/YE6YN3VGJV.jpg <meta name="twitter:label1" content="Published by" /> <meta name="twitter:data1" content="ashphy" /> </head> </html> XやFacebookのクローラーはJSを実行しない

22.

OGP: Open Graph Protocol • OGPは参照元ページの<head>に<meta>で書かれている • XにURLを貼るとXのクローラーがこれを取りに来る <html> <head> <meta name="og:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:site" content="@DocswellSlide" /> <meta name="twitter:title" content="You Don&#039;t Need Next.js | ドクセル" /> <meta name="twitter:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:image" content="https://bcdn.docswell.com/page/YE6YN3VGJV.jpg <meta name="twitter:label1" content="Published by" /> <meta name="twitter:data1" content="ashphy" /> </head> </html> 固定の情報しか返せない XやFacebookのクローラーはJSを実行しない

23.

OGP: Open Graph Protocol • OGPは参照元ページの<head>に<meta>で書かれている • XにURLを貼るとXのクローラーがこれを取りに来る <html> ↓データベースにある情報 <head> <meta name="og:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:site" content="@DocswellSlide" /> <meta name="twitter:title" content="You Don&#039;t Need Next.js | ドクセル" /> <meta name="twitter:description" content="おまえにNext.jsは必要ない" /> <meta name="twitter:image" content="https://bcdn.docswell.com/page/YE6YN3VGJV.jpg <meta name="twitter:label1" content="Published by" /> <meta name="twitter:data1" content="ashphy" /> </head> </html> 固定の情報しか返せない XやFacebookのクローラーはJSを実行しない

24.

まだ速くできないか? • ユーザーのリクエストが来てからレンダリングするのは遅い • 事前にレンダリングしておけばいい

25.

Static Site Generation: SSG ブラウザ ビルドサーバ サーバー データベース SELECT data HTML生成 GET index.html index.html アップロード

26.

Static Site Generation: SSG ブラウザ ビルドサーバ サーバー データベース SELECT data 事前にHTMLを生成しておく GET index.html index.html アップロード HTML生成

27.

Static Site Generation: SSG • 事前にレンダリングしておく方式 • レンダリング済みの結果を返すだけなので爆速 • 静的な内容しか返すことができない • 更新するときは再ビルドが必要になる • 全ページビルドし直す必要がある

28.

Static Site Generation: SSG • 事前にレンダリングしておく方式 • レンダリング済みの結果を返すだけなので爆速 • 静的な内容しか返すことができない • 更新するときは再ビルドが必要になる • 全ページビルドし直す必要がある たまには更新したいが、ページが多すぎてしんどい

29.

Incremental Static Regeneration: ISR • SSGで生成されたページに有効期限を設ける • 時間で決めたり、任意のタイミングでinvalidateしたりできる • 期限切れのページをSSRで動的に再生成する • 全ページを再ビルドしなくてよい • 分散システム上で動かすとキャッシュの同期が難しくなる • アクセス先によって見える情報が異なってしまうかも • Learn Next.js (App Router) をやるとこれからやるはず

30.

SSRでも速くしたい!

31.

レンダリング以外でも速くできるぞ • Largest Contentful Paint: 読み込みパフォーマンス

32.

LCPのタイミング ブラウザ サーバー データベース GET index.html SELECT LCP data HTML生成 index.html HTML表示

33.

LCPまでの様子を詳しく見てみよう https://web.dev/articles/optimize-lcp?hl=jp

34.

LCPまでの様子を詳しく見てみよう 読み込み待ち HTML読み込み CSS読み込み https://web.dev/articles/optimize-lcp?hl=jp

35.

LCPまでの様子を詳しく見てみよう 待ってるだけだから無駄な時間 読み込み待ち HTML読み込み CSS読み込み https://web.dev/articles/optimize-lcp?hl=jp

36.

Time to First Byte: TTFB TTFB https://web.dev/articles/ttfb?hl=ja

37.

Time to First Byte: TTFB TTFB 名前解決 https://web.dev/articles/ttfb?hl=ja

38.

Time to First Byte: TTFB TTFB TCP 3wayハンドシェイク クライアント サーバー SYN SYN+ACK 名前解決 ACK https://web.dev/articles/ttfb?hl=ja

39.

Time to First Byte: TTFB TTFB TCP 3wayハンドシェイク Client Hello SYN ACK サーバー クライアント クライアント サーバー SYN+ACK TLSハンドシェイク 名前解決 Server Hello Server Certificate Server Hello Done Client Key Exchange Change Cipher Spec Finished Change Cipher Spec Finished https://web.dev/articles/ttfb?hl=ja

40.

Time to First Byte: TTFB TTFB TCP 3wayハンドシェイク Client Hello SYN ACK サーバー クライアント クライアント サーバー SYN+ACK TLSハンドシェイク 名前解決 レスポンス待ち Server Hello Server Certificate Server Hello Done Client Key Exchange Change Cipher Spec Finished Change Cipher Spec Finished https://web.dev/articles/ttfb?hl=ja

41.

光の速さを感じてみよう • Tips: 光ファイバー中の光速の60-70%程度 • 東京から直線距離で光速だと • 大阪 : 500km – 1.7ms • 那覇 : 1600km – 5.3ms • サンノゼ : 8200km – 27.4ms • ニューヨーク:10800km – 36.0ms • ロンドン : 9600km – 32.0ms (実際にはアメリカ経由になりがち) アメリカが遠い

42.

アメリカが遠いなら近くに置けばいい • エッジ (ここでの定義) • ユーザーに近いデータセンターで処理させたい

43.

Cloudflare • 世界中に300カ所以上のエッジロケーションがある • 東京 • 大阪 • 福岡 • 那覇 Cloudflare Global Network | Data Center Locations

44.

でも全部にサーバ置いたら高くね? • サーバレスにしたいよね • Cloudflare Workers がある

45.

Cloudflare Workers • エッジロケーションで動くJavaScriptランタイム • Node.js ではなく V8が動く • ChromeのJavaScript処理系 • 0ms Cold Startsができる • (無料枠がデカイ)

46.

V8 JavaScript engine ってなに? Node.jsは内部でV8を使っている Node.jsのアーキテクチャ Event Loop and the Big Picture — NodeJS Event Loop Part 1

47.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Run第2世代で置き換えてください • マイクロVM Firecracker • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる A closer look at AWS Lambda

48.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Run第2世代で置き換えてください • マイクロVM Firecracker • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる EC2 ベアメタルインスタンス A closer look at AWS Lambda

49.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Run第2世代で置き換えてください • マイクロVM Firecracker 顧客ごとに分離 • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる EC2 ベアメタルインスタンス A closer look at AWS Lambda

50.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Run第2世代で置き換えてください • マイクロVM Firecracker 顧客ごとに分離 • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる 実行環境 一度に1つの呼び出しにつき実行環境1つ 実行後もしばらく生き残り再利用される EC2 ベアメタルインスタンス A closer look at AWS Lambda

51.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Run第2世代で置き換えてください • マイクロVM Firecracker 顧客ごとに分離 • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる 実行環境 一度に1つの呼び出しにつき実行環境1つ 実行後もしばらく生き残り再利用される EC2 ベアメタルインスタンス A closer look at AWS Lambda

52.

Cold Startsとはなにか? • よく知ってるAWS Lambdaの場合を見てみよう • Google Cloud派の人はCloud Runで置き換えてください • マイクロVM Firecracker • 125msで起動できる • そこからNode.jsを起動する • リクエストを受け付けられるようになるまで 時間がかかる サーバありなら0msなのに… A closer look at AWS Lambda

53.

V8 isolation • Cloudflare WorkersはV8 isolationを使っている • 実行コンテキストを分離するための機能 • マルチスレッドで動くので起動も速いし、メモリ消費量も少ない • 5msで起動できる Cloud Computing without Containers

54.

でもまだ起動に5msかかる… • TLSハンドシェイク中に起動しちゃえ Eliminating cold starts with Cloudflare Workers

55.

でもまだ起動に5msかかる… • TLSハンドシェイク中に起動しちゃえ リクエスト Eliminating cold starts with Cloudflare Workers

56.

でもまだ起動に5msかかる… • TLSハンドシェイク中に起動しちゃえ TLS ハンドシェイク リクエスト Eliminating cold starts with Cloudflare Workers

57.

でもまだ起動に5msかかる… • TLSハンドシェイク中に起動しちゃえ TLS ハンドシェイク どうせこのあとリクエスト来るし 先に起動しちゃえ! リクエスト Eliminating cold starts with Cloudflare Workers

58.

でもデータベースが遠ければ意味がなくない? • それはそう 長距離の往復が発生 Smart Placement · Cloudflare Workers docs

59.

Cloudflare Smart Placement • 自動でバックエンドに近い場所のWorkerを呼び出す 地理的に近い場所 Smart Placement · Cloudflare Workers docs

60.

エッジでもデータベースを使いたい • Cloudflare D1 • エッジで動くSQLite • ユーザーに近いところにリードレプリカを配置する • 不思議な力でリードレプリカが同期される • テーブルサイズなどの制限が強い • (RDBが無料で使える) Building D1: a Global Database

61.

データは置けないけどキャッシュなら • Cloudflare Workers KV • エッジで動くKey-Valueストレージ • データーベースをエッジに持ってこれなくても近いところでキャッシュ

62.

どうしても遅いデータがある • SSRはすべてをレンダリングしてからでないとレスポンスできない • Streaming SSR • できたところから配信して段階的にレンダリングする • 1つのHTTP通信の中に詰め込むので無駄がない • でもどう分割する? → <Suspense>

63.

React <Suspense> • 子要素が読み込み完了するまでフォールバックを表示する <div> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </div>

64.

<Suspense> • 子要素が読み込み完了するまでフォールバックを表示する <div> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </div> 読み込み中表示

65.

<Suspense> • 子要素が読み込み完了するまでフォールバックを表示する <div> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </div> 読み込み中表示 重い処理

66.

<Suspense> • 子要素が読み込み完了するまでフォールバックを表示する <div> <h1>{artist.name}</h1> <Suspense fallback={<Loading />}> <Albums artistId={artist.id} /> </Suspense> </div> 外側の要素を先に描画できる 読み込み中表示 重い処理

67.

ごめんエッジSSR速くなかったわ • Vercel役員、突然の懺悔

68.

ごめんエッジSSR速くなかったわ • エッジでレンダリングすると速くならない • データベースの近くで実行しないと速くならない それはそう • ほとんどのデータはグローバルに複製されない それはそう • v8じゃなくてNode.jsにしたら速くなった 根拠がよくわからない • セキュリティ • 多くの顧客は他サービスへのプライベート接続が必要 それはそう • データをグローバルに分散させることに懸念がある それはそう • v0では • Node.jsでStreaming SSRが高速だった • そこでPartial Prerenderingを実験してる

69.

Pertial Prerendering: PPR • Next.js v14で実装 • まだExperimental Partial Prerendering

70.

レンダリングはコンポーネント単位 • レンダリングは「ページ単位」から「コンポーネント単位」へ • コンポーネント単位でレンダリングが静的か動的かが決まる • 1ページの中に静的/動的レンダリングが混ざるようになる

71.

まとめ • Server Side Renderingはサーバ上でレンダリングして返す技術 • 早く表示できるのでユーザー体験が向上する • 動的なOGPに対応できる • 事前に用意しておけるならSSG、ISR • エッジコンピューティング • ユーザーになるべく近いところで実行する • Node.jsじゃないので起動が速くて、詰め込めるので安い • ページ単位で静的/動的レンダリングが変わっていたが、 今後はコンポーネント単位で切り替わるようになる

72.

質疑応答用資料

73.

React.lazy

74.

103 Early Hints Early Hints: How Cloudflare Can Improve Website Load Times by 30%