Gatsbyは単なるReactベースの静的サイトジェネレータではなかった話

はじめてGatsbyを使ってページをビルドしたとき大量のJSファイルが同時に生成されているのを見つけました。 単純にHTMLだけを生成すると思っていたので少し予想と異なり混乱しましたが、勉強を進めていくとGatsbyのより便利な側面が見えてきました。
2021.08.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

この記事の概要

  • Gatsbyはプリレンダリングをしている
  • クライアント側でページを生成することも可能

用語の整理

静的レンダリング

静的レンダリングは 「全てのページ分の HTML ファイルをあらかじめ生成しておき、それを配信するという方法」 です。 先にファイルを生成しておくことでサーバーの計算量を減らすことができ、素早くコンテンツを提供することが可能です。 また、静的ファイルをホスティングできれば良いので S3 や Netlify などにデプロイすることも可能です。

静的レンダリングの問題点はリクエストが予測できない場合にあります。 検索エンジンのようなサイトはクエリによってページが異なります。 しかし、全てのクエリ分のページを予めレンダリングしておくことは現実的ではありません。

また、コンテンツが頻繁に更新される場合にはその都度生成するのに時間がかかってしまいます。 SNS などのユーザーが素早く更新されたコンテンツにアクセスしたい場合には不向きでしょう。

Hugo などがこれに当てはまります。 今回取り扱う Gatsby も部分的に当てはまります。

クライアントサイドレンダリング

ここでのクライアントサイドレンダリングは 「JavaScript を利用してブラウザ側で HTML を生成すること」 です。 こちらも、静的レンダリングと同様に静的なファイルさえ配信できればひとまずは動きます。

ですがクライアントサイドレンダリングでは JS を用いてブラウザで API などを使用し動的にページを生成することができます。 先ほどのリクエストが予測できないような場合についても、その都度ページを生成することで解決できます。 また、API ベースでデータを非同期に取得することで使い心地が良くなるかもしれません。

ただ、デメリットもあります。

1 つ目はクライアント側に負荷がかかるということです。 ページのレンダリングを Web ブラウザが行うのでその計算のコストがかかります。

2 つ目はアプリケーションが複雑になるに従って、スクリプトファイルが大きくなることです。 スクリプトファイルのサイズが多いくなると初回のロード時間が長くなってしまい、ユーザー体験が低下する可能性があります。

React などがこれに当てはまります。 実は今回取り扱う Gatsby もまたこれに当てはまります。

プリレンダリング

プリレンダリングは静的とクライアントサイドの中間に位置するような手法です。 あらかじめ部分的に HTML を生成しておき JS のスクリプトと合わせて配布します。 こうすることでクライアントサイドよりも最初に一部のコンテンツが表示される時間を早めることができます。 さらに、静的レンダリングと異なりクライアントサイドで HTML を生成するので柔軟さもあります。

例えば、通販サイトなどで商品に関する部分は先にレンダリングしておき、顧客ごとのおすすめをクラアントサイドでレンダリングするということができるようになります。

ただデメリットもあり静的レンダリングのようにリクエストの予測ができない場合には工夫が必要です。 また、クライアントサイドレンダリングのようにスクリプトをロードする必要があります。

Gatsbyのメリット

個人的なGatsbyを使うメリットは以下の通りです。

  • プリレンダリングが比較的低コストで実装できる
  • Reactを用いて開発ができる

GatsbyがReactベースで作られているため、Reactのコンポーネントをテンプレートのように用いて開発ができます。 また、Reactのコンポーネントがそのままクライアントサイドでも再利用されるため、ビルド時とクライアントでのレンダリングの整合性を保ちやすいです。 ビルドするためのコードとクライアントサイドでのコードを別で開発するよりはミスが減るかもしれません。

ためしてみる

Gatsbyで簡単なページを作成してプリレンダリングが行われていることを確認してみます。

以下のコードからページを生成します。

index.js

import * as React from "react"


const IndexPage = () => {
  const [count, setCount] = React.useState(0);

  return (
    <main>
      <div>
        これはカウンターアプリです
      </div>
      <div>
        現在のカウント: {count}
        <br/>
        <button onClick={()=> setCount(count + 1)} >+</button>        
      </div>
    </main>
  )
}

export default IndexPage

質素なカウンターアプリです。 ボタンを押すとカウントが増えます。

生成物をみてみる

今回は難読化オプションをオフにしてビルドします。 ビルドすると以下のようなファイルが生成されます。

public/
├── app-19657a8b5f1e8b5f9df4.js
├── app-19657a8b5f1e8b5f9df4.js.map
├── chunk-map.json
├── component---src-pages-index-js-204f2269a8e93769b1ae.js
├── component---src-pages-index-js-204f2269a8e93769b1ae.js.map
├── framework-7749635b6746d7da00de.js
├── framework-7749635b6746d7da00de.js.map
├── index.html
├── page-data
│   ├── app-data.json
│   ├── dev-404-page
│   │   └── page-data.json
│   └── index
│       └── page-data.json
├── polyfill-9536a471372b2d608b7b.js
├── polyfill-9536a471372b2d608b7b.js.map
├── render-page.js
├── render-page.js.map
├── static
├── webpack-runtime-b6ae6751b12938453ad8.js
├── webpack-runtime-b6ae6751b12938453ad8.js.map
└── webpack.stats.json

長いので一部だけですが、実際にプリレンダリングされた部分です。

index.html

<div style="outline: none" tabindex="-1" id="gatsby-focus-wrapper">
<main>
    <div>これはカウンターアプリです</div>
    <div>
    現在のカウント:
    <!-- -->0<br /><button>+</button>
    </div>
</main>
</div>

現在のカウント: 0 となっています。 これはReactのステートの初期値が0であるためです。

もう少し、index.htmlを見てみます。

index.html

<link
    as="script"
    rel="preload"
    href="/webpack-runtime-b6ae6751b12938453ad8.js"
/>
<link as="script" rel="preload" href="/framework-7749635b6746d7da00de.js" />
<link as="script" rel="preload" href="/app-19657a8b5f1e8b5f9df4.js" />
<link
    as="script"
    rel="preload"
    href="/component---src-pages-index-js-204f2269a8e93769b1ae.js"
/>
<link
    as="fetch"
    rel="preload"
    href="/page-data/index/page-data.json"
    crossorigin="anonymous"
/>

いろいろとJSのファイルが読み込まれています。 今回はcomponent---src-pages-index-js-204f2269a8e93769b1ae.jsを見てみます。

component---src-pages-index-js-204f2269a8e93769b1ae.js

"use strict";
(self["webpackChunkrehydrate"] = self["webpackChunkrehydrate"] || []).push([
  [678],
  {
    /***/ 704: /***/ function (
      __unused_webpack_module,
      __webpack_exports__,
      __webpack_require__
    ) {
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(294);
      var IndexPage = function IndexPage() {
        var _React$useState = react__WEBPACK_IMPORTED_MODULE_0__.useState(0),
          count = _React$useState[0],
          setCount = _React$useState[1];
        return /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.createElement(
          "main",
          null,
          /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.createElement(
            "div",
            null,
            "\u3053\u308C\u306F\u30AB\u30A6\u30F3\u30BF\u30FC\u30A2\u30D7\u30EA\u3067\u3059"
          ),
          /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.createElement(
            "div",
            null,
            "\u73FE\u5728\u306E\u30AB\u30A6\u30F3\u30C8: ",
            count,
            /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.createElement(
              "br",
              null
            ),
            /*#__PURE__*/ react__WEBPACK_IMPORTED_MODULE_0__.createElement(
              "button",
              {
                onClick: function onClick() {
                  return setCount(count + 1);
                },
              },
              "+"
            )
          )
        );
      };
      /* harmony default export */ __webpack_exports__["default"] = IndexPage;

      /***/
    },
  },
]);
//# sourceMappingURL=component---src-pages-index-js-204f2269a8e93769b1ae.js.map

webpackを通しているのでかなり読みづらいですがよく見ると元のコンポーネントの片鱗が見えます。

つまりGatsbyは先に部分的にレンダリングをしておいて、あとからReactコンポーネントで再度レンダリングしているということです。 今回の例ではブラウザ側でJSをブロックするとカウントが増えないようになります。

まとめ

Gatsbyを使用することでプリレンダリングが比較的簡単にできることがわかりました。 Next.jsでも同様のことができるみたいです。 Reactのコンポーネントを用いて開発できる点も良いと思います。

参考URL