ぐるなびでフロントエンドを担当している原田です。
今回は、コーポレートサイトをリニューアルしたお話です。
リニューアルとはいえ、ページの見た目は全く変わっていないので、気づく人はほとんどいないと思いますが……裏側でどんなことが行われ、どう変わったのかをお伝えしようと思います。
目次
リニューアル前のコーポレートサイト
システム構成
既存のシステムは、Movable Type をベースに拡張された PowerCMS を使用していました。
サーバーに PowerCMS のシステムをインストールし、全てをその中で完結させる、オールインワンな構成です。
セキュリティ上の懸念から、静的パブリッシングで生成された HTML ファイルを別サーバーに配置して公開していました。
問題点
管理できていないシステム
社内に PowerCMS が導入されたのは2012年。
それから10年以上が経ち、PowerCMS のプログラムを管理できる人もいなくなってしまいました。
そのため、プログラムのアップデートができない、システムトラブルが起こったとしても対応できる人がいない、という、とても危険な状態で使い続けていました。
運用上の問題
CMS 機能を使用していないページやテンプレートの管理も CMS の管理画面で行う必要がありました。
そのため、何か変更があった場合は、CMS の管理画面に変更したコードを入力して公開していました。
本番公開時は、差分を注意深く確認するのみ。コードレビューやテストコードもなく、リリース手順はコピー&ペースト。
他に良いやり方も思い浮かばず、しばしば事故を起こしながらも、ずっとその運用を続けてきてしまいました。
移行不能なデータ
PowerCMS の管理画面自体をカスタムしまくっていたため、データ抽出が難しい状態になっていました。いわゆる魔改造です。
そうなると、CMS を新しい環境に移行することも困難です。だけど、データはどんどん増えていく……。
これからのことを考えると、既存のシステムを使い続けるよりも、丸ごとリニューアルするのが良いのではないか、と判断しました。
リニューアル
リニューアルのシステム構成を考える上でベースとしたのは、Jamstack アーキテクチャです。
Jamstackとは
公式サイトには以下のように書かれています。
Jamstack is an architectural approach that decouples the web experience layer from data and business logic, improving flexibility, scalability, performance, and maintainability.
直訳すると、「Webサイトをデータやビジネスロジックから切り離し、パフォーマンス、スケーラビリティ、柔軟性、保守性を高めるアーキテクチャ」だそうです。
これだけだと分かりにくいので、図にしてみました。
ソースコードと API から静的な HTML を事前に作成して CDN から配信する、というのが、基本的な Jamstack のアーキテクチャです。
それにより、高パフォーマンス、高いセキュリティ、低コストなスケーラビリティが実現できます。
また、例えば ソースコードのフレームワークを Next.js から Nuxt.js に変更する、といった場合でも、それぞれが分離された環境にあるため、容易に変更できるのも特徴です。
既存の環境は、オールインワンの環境であることが運用上の問題を生んでいましたが、Jamstack アーキテクチャに移行することでその問題は解決できると考えました。
システム構成と技術スタック
システム構成はこのような形になりました。
CI/CD は Github Actions、 ホスティングは AWS(S3, CloudFront)を採用しています。
ソースコードの部分、フロントエンドの実装は、社内のフロントエンドの標準的な技術を採用しました。
- TypeScript
- Next.js (Static Exports)
- Jest, React Testing Library
- Storybook
- Mock Service Worker (MSW) など
特別なナレッジ不要、使い慣れた言語やフレームワークで書けるのは、実装が捗ります。
APIを担うヘッドレスCMS
API の部分は、ヘッドレス CMS を使用しました。
ヘッドレス CMS とは、HTML 生成機能を持たず、コンテンツを管理する画面と、コンテンツにアクセスできる API が用意されるサービスです。
つまり、誰でも簡単に、管理画面と API が作れてしまうサービスです。
数あるヘッドレス CMS の中から、今回は microCMS を採用しました。
microCMS の良いところ
- 日本製なので、画面が日本語。開発者以外でも理解しやすい。サポートが日本語なのもありがたい。
- 直感的に挙動が分かる UI なので、運用・更新担当の非エンジニアへの説明コストが低い。
- 画像 API が提供されれているので、簡単に画像を最適化できる。
- 新機能のリリースやアップデートが頻繁に行われている。
ポチポチするだけであっという間に API が作れてしまうので、CMS の開発コストは格段に低くなりました。
また、クラウドサービスなので、プログラムを自分たちで管理する必要がなくなり、既存環境での課題もクリアすることができました。
工夫したところ
ビルド時間の短縮
Jamstack の宿命ですが、事前にビルドを行い静的な HTML を生成するため、ページが増えれば増えるほど、ビルド時間が長くなり、公開されるのが遅くなります。
CI/CD で使用する Github Actions は実行時間で課金されるため、節約のためにもビルド時間を短縮することは重要です。
今回の場合、以下のような状況にありました。
- ニュースリリース用の API、バナー登録用 API、IR情報の API のように、カテゴリーやページ単位で複数の API を作成する必要があった。
- ニュースリリースの過去記事はページ数が多く、今後も増えていく予定。
そこで、ソースコードをモノレポ構成にすることで、ビルド対象を分割、絞り込むことはできないかと考えました。
モノレポの管理は、 npm workspaces と Vercel 社製の Turborepo を使用することにしました。
ビルド対象の絞り込み
Github Actions での microCMS からの Webhook の受け取りと Turborepo の Filter 機能を連携させることで、ビルド対象を絞り込み、実行時間を短縮しました。
Github Actions のワークフローはこのような感じです。
name: Build and Deploy on: # microCMS Webhook repository_dispatch: types: [ hoge, # 【1】 fuga, piyo, ] jobs: install: # npm ci を実行するjob (略) # build build-hoge: if: contains(fromJSON('["hoge"]'), github.event.action) # 【2】 uses: ./.github/workflows/_build.yml # 【3】 with: environment: production app-name: hoge # 【4】 secrets: inherit concurrency: # 【5】 group: hoge_build cancel-in-progress: true needs: install build-profile: if: contains(fromJSON('["fuga"]'), github.event.action) # 以下略 build-ir: if: contains(fromJSON('["piyo"]'), github.event.action) # 以下略 # deploy deploy-hoge: if: success() uses: ./.github/workflows/_deploy.yml with: environment: production app-name: hoge secrets: # (略) needs: build-hoge # 以下略
【1】では、microCMS で設定した名前の Webhook をトリガーにして、ビルド・デプロイワークフローが実行されます。
【2】では、トリガーとなるイベントアクションに指定したアクション名が存在する場合に、ビルドを実行するように指定しています。
例えば、Webhook 名が hoge
だった場合、build-hoge
のみが実行されます。
【4】では、ビルド対象のアプリケーション名を指定。再利用可能ワークフローに渡して使用しています。
【5】では、例えば CMS の「公開」ボタンを連打した場合などに、ワークフローが連続で動かないようにするため、最後の Webhook のみを実行するように設定しています。
そうすることで、無駄なワークフローが実行されず、Github Actions 実行時間を節約できます。
【3】の build job は再利用可能ワークフローを使用して、使いまわせるようにしています。
name: build on: workflow_call: inputs: environment: description: staging or prod required: true type: string app-name: description: app-name required: true type: string jobs: build: name: build timeout-minutes: 15 runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: # (省略) - name: Build run: npm run export -- --filter=${{ inputs.app-name }} # ポイント! env: # (省略)
ポイントは、Turborepo の Filter 機能を使い、受け取った app-name
のワークスペースのみをビルドすることです。
以下の実行サンプルでは、ニュースリリースの記事が更新された場合、app-name
が release
と home
(トップページのニュースリリース最新情報の更新のため)のみが動いていることがわかります。
不要な job はスキップされ、実行時間を短縮することができました。
Github Actions でのキャッシュ利用
Github Actions でキャッシュを活用して、さらに時間短縮を目指しました。
- npm パッケージインストール時に
node_modules
キャッシュを活用- Github Actions 公式の例を参考に、
node_modules
をキャッシュ。キャッシュがある場合は、パッケージのインストールをスキップ。 - キャッシュがある場合、約2分短縮。
- Github Actions 公式の例を参考に、
- Next.js のビルドキャッシュを活用
- Next.js 公式の例を参考に、
.next/cache
に保存されているビルドキャッシュを活用して、ビルドのパフォーマンスを向上。
- Next.js 公式の例を参考に、
- Turborepo のキャッシュを活用
- リモートキャッシュは使用せず、
.cache
フォルダに保存されるキャッシュを利用。変更が加えられてないワークスペースは実行をスキップする。 - デフォルトでは
node_modules/.cache
配下に保存されるので、--cache-dir
オプションを指定して、.cache
ディレクトリをnode_modules
の外に設定。node_modules
のキャッシュとは分けて保存するようにした。 - Pull Request 作成・更新時の Lint , テストの実行に活用。平均3分の時間短縮。
- リモートキャッシュは使用せず、
- Pull Request がクローズされたらキャッシュを削除するワークフローを追加して、キャッシュがいっぱいにならないようにした。
その他
せっかくのモノレポ なので、Next.js アプリケーション で使用する共通コンポーネント、関数、設定などは、packages に配置して一括管理できるようにしました。
モノレポのディレクトリ構成はこんな感じです。
corporate ├── apps // アプリケーション │ ├── ir // Next.js │ ├── profile // Next.js │ ... │ └── release // Next.js ├── packages // アプリケーションで共通利用するもの │ ├── storybook │ ├── eslint-config-custom │ ├── tsconfig │ ├── ui // 共通 React コンポーネント │ └── utils // 共通の関数, Hooks, 定義等 ├── package.json └── turbo.json
packages/ui
配下には各アプリで共通で利用する React コンポーネントを作成、Storybook でカタログ管理することで、効率よく実装することができました。
リニューアル後の変化
このように、既存のシステムが抱えていた問題は、リニューアルによって解消することができました。
障害が起きにくい環境、起きても発見しやすい環境へ
CMS 機能を使用しないページやテンプレートは Github でコード管理されているため、コードレビューやテストをパスしたコードを、決められた手順に従って Github Actions を使ってリリースできるようになりました。
当たり前のことではあるのですが……これまでのような危険なリリースを行わずに済むようになったのは、本当に良かったと思います。
また、Slack チャンネルにビルド・デプロイ結果の通知を行うようにしたので、もしビルドに失敗することがあれば、すぐ気付けるようになりました。
NewRelic での監視設定や、CloudFront のログを Kibana で確認するなど、社内で整備されている可視化・監視等を使用して、今何が起きているのかを分析できるようになったことも安心材料です。
ページのパフォーマンスが大幅に改善
PC 表示の際の PageSpeed Insights の結果です。
PC表示の場合、旧サイトではパフォーマンスが「78」でしたが、新サイトではパフォーマンスが「94」と改善しました。
SP 表示の場合は、旧サイトが「28」、新サイトでは「64」と、こちらも大幅に改善しました。
そして、Core Web Vitals などの指標も「合格」でした!
パフォーマンス改善の要因
CloudFront のキャッシュ利用が一番の要因だと思います。
エッジロケーションから配信されるとやっぱり早い、というのを実感しました。
画像最適化もかなり効果が高かったと思います。
microCMS の画像 API を利用した次世代フォーマット対応を行い、 圧縮率が高い avif
(もしくは webp
)に変換を行いました。
サイズが大きな画像に対しては、サイズパラメータを付与することで、PC表示 / SP表示それぞれに最適な画像を設定することができます。
コードの実装としては、Picture タグによる画像の出し分け処理を行なっていきました。
<picture> <source media="(max-width:480px)" srcSet={`${content.image.url}?fm=avif&w=640`} type="image/avif" /> <source media="(max-width:480px)" srcSet={`${content.image.url}?fm=webp&w=640`} type="image/webp" /> <source media="(max-width:480px)" srcSet={`${content.image.url}?w=640`} /> <source srcSet={`${content.image.url}?fm=avif`} type="image/avif" /> <source srcSet={`${content.image.url}?fm=webp`} type="image/webp" /> <img src={content.image.url} alt="" width={content.image.width} height={content.image.height} decoding="async" /> </picture>
パラメータの制御に加え、decoding="async"
もしくは loading="lazy"
属性を付与することにより、画像の読み込みがメインスレッドの読み込みを邪魔しなくなるので付与しています。
microCMS で管理していない画像に関しても、上記の属性と画像の幅高さを明記することで、CLS (Cumulative Layout Shift) を削減しています。
また、以前は、全てのページに jQuery や全ての機能を網羅した common.js 、巨大な common.css など、不要な機能を読み込んでいましたが、そのページで必要なファイルや機能のみを読み込むようにしたことも良い影響を与えていると思います。
Next.js が自動でページ単位で最適化してくれているのも大きいと思います。
まとめ
コーポレートサイトのシステムリニューアルについて、さらっと書くつもりがだいぶ長くなってしまいました。
何かを決めて実装するたびに問題にぶつかって、考えて乗り越えて……を繰り返して、なんとかリリースまでたどり着いたので、他にも書きたいことはたくさんあるのですが、それはまたいつか機会があれば。
運用フェーズに入って新たな課題も見えてきたので、ひとつひとつ解決しながら、今後もより良いサイトを目指していきたいと思います。