純SPAではNext.jsとは異なり、基本的にひとつのindex.htmlを用いるため、ページによってmetaタグを切り替えるのが難しい。このスライドでは、CloudFrontやLambda@Edgeなどを使って純SPAでもページによってmetaタグを切り替える方法を4つ紹介する。
純SPAでNext.jsに対抗する〜ページによってmetaタグを切り替える編〜清川 航⼀
View Slide
© DMM.com自己紹介 2名前 清川航一 (𝕏 @kiyoshiro944) おしごと おもにフロントエンド(React/TypeScript) バックエンドも書く(Golang) 所属 ポイントクラブチーム 新卒2年目
© DMM.com前提・背景 3
© DMM.com前提:用語の定義 • SPA • ※人によって定義が違う😇 • CSRしかしていないアプリ • CSR / SSR / SGを1つ以上組み合わせたアプリ • 純SPA • CSRしかしていないアプリ • 例:Create React AppやViteで作ったアプリ
© DMM.com• 2年くらい純SPAでフロントをつくっていた • パフォーマンス・SEOが必要になったため、今年の7月に2人で1ヶ月 かけてNext.jsへ移行した • 「純SPAのままNext.jsに対抗する」方法も考えていた • そのアイデアを少し一般化してこの発表で供養 背景:ポイントクラブでは
© DMM.com純SPAの課題 6
© DMM.com(色々あるけど、今回取り上げるのは) 「ページによってmetaタグを切り替える」 metaタグの例: • Next.jsではページごとにHTMLを生成するのが簡単 /users → users.html /posts → posts.html • 純SPAだと、基本的にページによらずひとつのindex.htmlを使う /users → index.html /posts → index.html 純SPAだとムズいこと
© DMM.com純SPAの課題に直面する例 Webアプリケーションのフロントエンドを開発することになりました とくにSEOや初回表示速度が重要なわけでもないので、純SPAでつくることに ✅開発も終盤にさしかかってそろそろリリースできるかと思った矢先、 「SNSで共有されたときのために、 ページによってmetaタグ変えたい」 という要件がでてきました。。。 ※最初からNext.jsを採用しておけというツッコミはNG
© DMM.com純SPAはブラウザでしか実行されないが、Next.jsだと、Node.jsとブラウザの両方で実行される。 例えば、useEffectの外でWeb APIにアクセスしている場合エラーがでる (ポイントクラブで実際にNext.jsへの移行に1ヶ月くらいかかった原因のひとつ😇) 純SPAからNext.jsへの移行は案外大変
© DMM.com純SPAのまま この課題を解決する方法を見ていきましょう 10
© DMM.com⚠注意点 • 時間が足りなそうなので、細かい実装の話はしません • 気になる人はhttps://github.com/KoichiKiyokawa/spa-meta • AWSを使っていますがCloudflareとかのほうが簡単にできるかも • この発表は純SPAの使用を推奨するものではありません • あくまで回避策のお話
© DMM.com「ページによってmetaタグを変えたい」の解像度を上げる • 動的か静的かの2種類に分類できる • 静的のほうがインフラ構成などが楽 動的 静的 パターン数が多すぎて事前生成ができないのでリクエスト時に実行する 例:/posts/1, /posts/2, …/posts/9999 でmetaタグを変える パターン数が少なく事前生成できる 例:/aboutだけmetaタグを変える
© DMM.com4種類の方法一覧 動的(リクエスト時に実行) 静的(ビルド時に実行) 方法 dynamic rendering head-only SSR head-only SG static prerendering 概要 ? ? ? ? インフラ 構築難度 ? ? ? ? 保守難度 ? ? ? ? その他 ※dynamic-rendering以外の名称は自分が適当に考えました
© DMM.com• デモ:https://dynamic-rendering.kiyoshiro.me/posts/123 方法1:dynamic rendering(概要)
© DMM.comLambda@Edge + CloudFront + S3の場合 方法1:dynamic rendering(インフラ例)
© DMM.comLambda@Edge + CloudFront + S3の場合 方法1:dynamic rendering(インフラ例) キャッシュの有無に関わらず、 リクエストのたびに必ず実行される • キャッシュヒットしなかったときだけ実行される • ここで返したレスポンスはキャッシュされる
© DMM.com方法1:dynamic rendering(アプリコード)
© DMM.com😄GOOD • SEO:ページコンテンツも描画できる • 保守性:一度インフラを整えたら手を加える必要なし • アプリコードだけに集中できる 方法1:dynamic rendering
© DMM.com方法1:dynamic rendering 😩BAD • 最初にインフラ構築するのが大変 bot判定・ユーザーとbotでキャッシュの分離・ヘッドレスブラウザの実行etc… • Google曰く、インフラが複雑になるので「回避策」 • 回避策としてのダイナミック レンダリング | Google 検索セントラル • Next.jsとかのSSRと比べてレスポンスが遅くなりがち • Google Botへのレスポンスが遅くなると SEOに悪影響が(CoreWebVitals) • CloudFrontなどCDNのstale-while-revalidate機能 を使うと軽減できそう ※キャッシュヒットしなかったとき
© DMM.com補足:CDNのstale-while-revalidate機能 キャッシュヒットしなかったときの比較(矢印の長さに注目)
© DMM.com4種類の方法一覧 動的(リクエスト時に実行) 静的(ビルド時に実行) 方法 dynamic rendering head-only SSR head-only SG static prerendering 概要 botからアクセスされたときだけヘッドレスブラウザでレンダリングしたHTMLを返却 ? ? ? インフラ 構築難度 ✕難 ? ? ? 保守難度 ◎必要なし ? ? ? その他 😄ページコンテンツもレンダリング 😩不安定で遅い
© DMM.com方法2:head-only SSR(概要) デモ:https://head-only-ssr.kiyoshiro.me/posts/123 dist/index.html
© DMM.com方法2:head-only SSR(コード) esbuildなどを使ってビルド時に埋め込むと S3へのアクセスが省略できて良い
© DMM.com方法2:head-only SSR(インフラ) • キャッシュヒットしなかったときだけ実行。レスポンスはキャッシュされる
© DMM.com方法2:head-only SSR 😄GOOD • dynamic renderingよりもインフラ構成が楽&レスポンスも早い 😩BAD • 保守性:ページが増えるたびにエッジ関数に追記してデプロイする必要あり 例:/posts/:idに加えて、/users/:idパスが増えたらエッジ関数に追記 • SEO:ページのコンテンツはレンダリングしない
© DMM.com4種類の方法一覧 動的(リクエスト時に実行) 静的(ビルド時に実行) 方法 dynamic rendering head-only SSR head-only SG static prerendering 概要 botからアクセスされたときだけヘッドレスブラウザでレンダリングしたHTMLを返却 エッジ関数でmetaタグを注入したHTMLを返却 ? ? インフラ 構築難度 ✕難 △微難 ? ? 保守難度 ◎必要なし ✕大変 ? ? その他 😄ページコンテンツもレンダリング 😩不安定で遅い
© DMM.com方法3:head-only SG ↑index.html ↓そこから生成したabout.html 例:/aboutページだけmetaタグを変えたい場合 インフラ /aboutでアクセスされたらabout.htmlを参照するように設定 ビルド時 /aboutページ用のabout.htmlを作る (headタグだけ注入)
© DMM.com方法3:head-only SGdist ├ index.html ├ … dist ├ index.html ├ about.html ├ … ビルドスクリプト実行metaタグ注入
© DMM.com方法3:head-only SG
© DMM.com方法3:head-only SG 😄GOOD • head-only SSRよりもインフラ構成が楽 • /aboutを/about.htmlにルーティングするだけ 😩BAD • 保守性:アプリコードとは別に、head注入するスクリプトを保持する必要あり • SEO:ページのコンテンツはレンダリングしない
© DMM.com4種類の方法一覧 動的(リクエスト時に実行) 静的(ビルド時に実行) 方法 dynamic rendering head-only SSR head-only SG static prerendering 概要 botからアクセスされたときだけヘッドレスブラウザでレンダリングしたHTMLを返却 エッジ関数でmetaタグを注入したHTMLを返却 ビルド時にmetaタグを注入したHTMLをつくる ? インフラ 構築難度 ✕難 △微難 ◯簡単 ? 保守難度 ◎必要なし ✕大変 △普通 ? その他 😄ページコンテンツもレンダリング 😩不安定で遅い
© DMM.comビルド時に、Bot用のレンダリングされたHTMLをヘッドレスブラウザで作成 方法4:static prerendering リクエスト時ではなく ビルド時に行う 👆(再掲)dynamic renderingの図
© DMM.com方法4:static prerendering ※細かい処理は省略
© DMM.com方法4:static prerendering
© DMM.com方法4:static prerendering 😄GOOD • head-only SGと同様、インフラ構成は楽 • Botからのアクセス時に/aboutへのリクエストを/about.htmlにルーティングするだけ • SEO:ページコンテンツもレンダリングできる 😩BAD • 保守性:アプリコードとは別で、ヘッドレスブラウザを実行するスクリプトを保守する必要あり • ビルド時間:ヘッドレスブラウザを動かす分、時間がかかる
© DMM.com4種類の方法一覧 動的(リクエスト時に実行) 静的(ビルド時に実行) 方法 dynamic rendering head-only SSR head-only SG static prerendering 概要 botからアクセスされたときだけヘッドレスブラウザでレンダリングしたHTMLを返却 エッジ関数でmetaタグを注入したHTMLを返却 ビルド時にmetaタグを注入したHTMLをつくる ビルド時にヘッドレスブラウザでHTMLを生成する インフラ 構築難度 ✕難 △微難 ◯簡単 ◯簡単 保守難度 ◎必要なし ✕大変 △普通 △普通 その他 😄ページコンテンツもレンダリング 😩不安定で遅い 😄ページコンテンツもレンダリング
© DMM.comまとめ 40
© DMM.com• 純SPAでもLambda@Edgeなどを使えばページごとにmetaタグを切り替えることができる まとめ
© DMM.com• 面倒なので、最初からNext.jsを採用しよう • ページによってmetaタグを切り替えられる以外にもメリット多い • zero config • viewportに入ったリンクのprefetch • 画像/フォント/外部scriptの読み込み最適化 • etc… とはいえ・・・
© DMM.comご清聴ありがとうございました 43