🌠

Offersデジタル人材総研を支える技術 ー Astro、Svelte、Amplify、microCMS|Offers Tech Blog

2022/07/04に公開

Offers を運営している株式会社 overflowあほむ でございます。今回は 弊社の新規メディア の開発にご参画いただいた、どせいさんのアイコンでお馴染みの(?) leader22 さんに技術スタック解説をご寄稿いただいたテックブログです。

はじめに

こんにちは。ランチをご馳走するとそれなりに働く副業エンジニアの、りぃ (Twitter, Blog) です。

この記事では、先日リリースされた、Offers デジタル人材総研 の裏側、つまりはサイトを支える技術スタックについて紹介していきます。

要件の整理

技術選定をするにあたり、まずは要件を整理しました。そうして洗い出された要件は、だいたい次のようなものでした。

  • パブリックで静的なウェブサイトである
    • 更新頻度はそれほど高くない
  • API などを使って動的に表示する要素はない
    • ログインなどもない
  • JavaScript を使った UI は必要
    • カルーセルや、3rd パーティのスクリプト
  • 入稿には CMS を利用したい
    • 記事は Markdown で記述する
    • 予約投稿、入稿時・公開前プレビューがしたい

詳細な経緯は割愛しますが、結論として「静的サイトジェネレーターを利用して事前にビルドを行い、それを CDN にデプロイする方式」を採用することにしました。GitHub のリポジトリや CMS の更新にフックしてビルドとデプロイが行われる、いわゆる Jamstack(端的に SSG とも)と呼ばれる構成です。

動的な要素がなく、すべてのユーザーに対して等しく同じコンテンツを提供すればいいのなら静的にビルドしておくのが最速だからです。

技術スタック

さて、本題の技術スタックとしては、主に次のツールやサービスを採用しました。

それぞれ簡単に紹介していきます。

Amplify Hosting ー 静的サイトのホスティング

https://aws.amazon.com/amplify/hosting/

Amplify Hosting は、AWS が提供する静的サイトのホスティングサービスです。

GitHub などのリポジトリを連携することで、コードのビルドからデプロイまでを一気通貫で行うことができ、それらは CDN(Cloudfront)から配信されるようになります。

プルリクエストごとにプレビューサイトをデプロイしてくれたり、ベーシック認証をかけられたりと、チームでの開発にも便利です。

コードを修正してプッシュしたタイミングだけでなく、CMS のデータを更新した際にも、Webhook でサイトのビルドとデプロイを自動化しています。

microCMS ー ヘッドレスCMS

https://microcms.io/

記事データは CMS で管理したいということで、いくつかの CMS を試した結果、microCMS が採用されました。

ビルド時に API から記事データを一括取得して利用しているほか、公開前の記事プレビューでは、クライアントサイドから直接 API を呼び出しています。

個人的に microCMS を採用してよかったなと思った点としては、画像 API があります。これは、microCMS にアップロードした画像を、imgix の各種パラメータを付与した URL から利用できるというものです。

1 つの画像をアップロードすれば、それをさまざまな解像度で取得したり、フォーマットの変更ができたりと、便利に利用しています。(実際には、自社のプロキシーを経由して利用しています)

Astro ー モダンな静的サイトジェネレーター

https://astro.build/

Astro は、静的サイトジェネレーターにカテゴライズされるツールです。

比較対象としては、EleventyHugo などが挙げられます。他にも、Next.jsで export コマンド を使うこともできました。

そんな中で、Astro を採用する決め手となったのは、次の 2 つの特徴です。

  • 既存 UI フレームワークがコンポーネントに利用できる
  • ビルドで必要最低限の JavaScript だけが出力される

まず 1 つ目です。Astro を使ったコードでは、.astro という拡張子のファイルで、ページやコンポーネントを形成するのが基本スタイルです。

---
import BaseHead from "../components/base-head/index.astro";
import AboutLayout from "../layouts/about/index.astro";
import SiteHeader from "../components/site-header/index.svelte";
import SiteFooter from "../components/site-footer/index.svelte";
import AboutUs from "../components/about-us.svelte";
import { setupAboutPage } from "../data/index.js";

const { allCategories } = await setupAboutPage(Astro);
---

<!DOCTYPE html>
<html lang="ja">
  <head>
    <BaseHead />
  </head>
  <body>
    <AboutLayout>
      <SiteHeader {allCategories} />
      <AboutUs />
      <SiteFooter {allCategories} />
    </AboutLayout>
  </body>
</html>

「ビルド時にのみ利用され、クライアントサイドの JavaScript としては出力されない」ということを明示する、Frontmatter を用いたシンタックスが特徴的です。

ただ、それ以外は見慣れた JSX とほとんど同じです。そしてこの JSX の中では、React や Vue、Svelte などのコンポーネントをそのまま利用できます。

書きなれたフレームワークのコード、ライブラリが使えるのはやはり嬉しいものですし、もし将来的に懇意にしているフレームワークを鞍替えすることになっても安心です。

そして、2 つ目の特徴ですが、どの UI フレームワークを利用したとしても、デフォルトでは JavaScript のコードが一切出力されません。ビルド時に SSR された結果として、HTML と CSS は出力されるのですが、JavaScript は出力されないようになっています。JavaScript をダウンロード・実行しないということは、その分ページが速くロード・表示されるということです。

クライアントサイドで JavaScript を実行したい場合は、特別なディレクティブ(後述)を利用する必要があります。そしてその際、実行タイミングなども最適化されるようになっています。

次のコードは、メディアクエリーによって「画面幅が狭くなったときだけ」表示されるメニューを内包した Astro コンポーネントです。

---
import DialogMenu from "./dialog-menu/index.svelte";

const { allCategories } = Astro.props;
---

<header>
  <div class="inner">

    <!-- ... -->

    <!-- 画面幅に余裕があるときに表示、さもなくば非表示 -->
    <nav class="nav">
      <ul>
        {allCategories.map((category) => (
          <li>
            <a href={`/categories/${category.slug}/`}>{category.title}</a>
          </li>
        ))}
      </ul>
    </nav>

    <!-- 画面幅が狭いときに表示、さもなくば非表示 -->
    <div class="menu">
      <DialogMenu {allCategories} client:visible />
    </div>
  </div>
</header>

<style>/* ... */</style>

ここでいう DialogMenu コンポーネントに対する client:visible という記述が、先ほど紹介していた特別なディレクティブです。

この指定で、この Svelte コンポーネントに関する JavaScript がランタイムで利用できるようになります。そしてそのコードのダウンロード・実行タイミングは visible、つまりメディアクエリーで画面に表示されたときです。

つまり、画面幅に余裕がある状態では、Svelte の JavaScript のコードの一切をダウンロード・実行しないということです。

この挙動は、Partial Hydration と呼ばれています。Next.js などの既存フレームワークでは実現できない挙動であり、Astro の最大の特徴です。

クラシックな静的サイトジェネレーターでは、コンポーネント指向でコードを書きつつバンドルまでを最適化するという開発体験の確保までは難しいことが多いです。

一方で Next.js などを利用すると、開発体験に問題はないものの出力される JavaScript のファイルサイズが大きく、その実行タイミングも制御できないため、ほとんどが静的なサイトにも関わらずパフォーマンスを犠牲にする必要がありました。

その点、Astro を採用することで、開発体験とパフォーマンスを両立させることができました。

現時点ではまだβの扱いですが、近いうちに メジャーバージョンのリリースが予定 されています。

Svelte ー UI コンポーネントフレームワーク

https://svelte.dev/

Astro と組み合わせて利用するコンポーネントの UI フレームワークには、Svelte を採用しました。

  • Scoped に CSS を記述する方法が備わっている
  • バンドルサイズが小さい

この 2 つの要件を満たすものは、Astro が現在サポートしている各種 UI フレームワークの中でも、Svelte だけです。

単純なコンポーネントの場合には、ただの HTML と CSS を書く感覚で記述できるのもポイントでした。Astro コンポーネントでも単一ファイル内で style 要素を使っていたので、ほとんど同様の書き味で利用できます。

Partytown ー 3rdパーティスクリプトのワーカー化

https://partytown.builder.io/

サイトのパフォーマンスにこだわるからには、3rd パーティのスクリプトに対してもなにか打ち手が欲しい。というところで、そういったサイト本体に関係のないトラッキング処理などを、メインスレッドからワーカースレッドに移すことができる Partytown というライブラリを導入しました。

またβのライブラリではあるものの、Astro とも簡単に連携でき、その経過も良好のようです。

その他、パフォーマンスへの取り組み

技術選定の段階で、クライアントサイドに出力される JavaScript のベースラインを減らすことには成功しましたが、その他にもパフォーマンスのためにできることは多々あります。

たとえば、適切な HTML 要素や、その属性を利用するといったものです。


右上のコントロールをクリックすると目次コンテンツがトグルで開閉する <details>

一見すると、このような開閉 UI には JavaScript が必要では?と思うかもしれませんが、そんなことはありません。HTML には details という便利な要素があり、これを使うだけで開閉 UI を実装でき、CSS でスタイリングできます。


モバイル相当の環境で右上のコントロールをクリックするとメニューが開閉する <dialog>

ハンバーガーメニューから開ダイアログも、dialog 要素を使うことにより、ほとんどの JavaScript 実装を不要にできます。アクセシビリティなどへの配慮もブラウザが自動的にやってくれます。

他にも、転送量が問題になりがちな画像についても最適化しています。img 要素の srcset 属性や、picture 要素を利用することで、閲覧環境に応じた画像を出し分けできます。loading 属性に対応した環境であれば、遅延読み込みも簡単に実装できます。

また、基本の画像フォーマットを WebP にしたことで、ファイルサイズを大きく減らすこともできました。SVG フォーマットで用意できる画像であれば、1 ファイルで拡縮にも耐えられるようにできます。

おわりに

元々のサイト要件がシンプルなことも大きな要因ですが、ある程度の記事数になったとしても、それなりのパフォーマンスを担保できる技術スタックに仕上がったのではないでしょうか。


Lighthouse の Perf スコアも良好[1]

ただ、これからサイトがメンテナンスされていくにつれ、UI 変更やコンポーネントへの機能追加なども行われていくと思います。そういう意味では、本当の勝負はこれからです。

現状でも、大人の事情[2]によりパフォーマンスが犠牲になっている部分もありますし、運用フローと協調することによりパフォーマンス改善できる部分もまだまだあると思っているので、今後の運用に期待したいところです。

関連記事

https://zenn.dev/offers/articles/20220613-component-props-design
https://zenn.dev/offers/articles/20220523-component-design-best-practice

脚注
  1. 歩注: 99 点止まりなのは弊社都合で画像周りに余地が残っております🙏 ↩︎

  2. 歩注: HubSpot の埋め込みフォームとかございまして...📝 ↩︎

Offers Tech Blog

Discussion