Oteto Blogのロゴ

【Next.js】unstable_runtimejsでhydrationを無効化し静的(MPA)化する

やりたいこと

Next.jsでSSG(Static Export)なブログを作成していると、サイト自体を完全に静的化したい時がある。

「完全な静的化」とは

どいうことかというと、Next.jsではSSGをしてもhydration処理のためにruntime-jsが実行される。例えHooksや<Link>タグを利用したclient-side routingを利用していなくとも。

hydration処理のために読み込まれるJavaScript

そのため上のようなjsファイルが読み込まれるわけだが、asyncが指定されているとはいえサイズ的にはかなり膨大。

そこでNext.jsのJavaScriptを全て抜いて完全にMPAな(ピュアなHTMLのみの)サイトにしたい。

そもそもMPA化したい理由

  • 初回アクセス時の表示速度を上げたい
  • ブログの性質上、検索結果への直帰率が高いためclient-side routingが不要(SPAにする恩恵が少ない)

Next.jsを使わない手もある

ちなみに別のフレームワークやテンプレートエンジンを利用するという方法もあったが、下記の理由から断念した。

  • tsxで書きたいのでEJSやPUGは候補外
  • Reactを利用できるMPAフレームワークのAstroにも興味はあったが、単純に乗り換える手間がかかる

解決法

Next.js 12以下の場合

import Layout from "../layout";

export const config = {
  unstable_runtimeJS: false,
};

const ExamplePage = () => {
  return (
    <Layout>
      <h1>Hello world</h1>
    </Layout>
  );
};

export default ExamplePage;

/pagesを利用している場合、静的化したいページのunstable_runtimeJSfalseにするだけで実現できる。

Next.js 13以上(App Directory)の場合

App Directoryの場合unstable_runtimeJSは使用できず、なおかつNext.js側でそういったオプションは用意されていない

そこで、static exportで生成したHTMLから直接scriptタグを削除する、という無理やりな方法で実現することにした。

1. npm-scriptsを実装

#! /usr/bin/env node
const path = require("path");
const fs = require("fs");
const { JSDOM } = require("jsdom");

const HTML_FILE_PATH = "out";

const FileType = {
  File: "file",
  Directory: "directory",
  Unknown: "unknown",
};

/**
 * ファイルの種類を取得する
 * @param {string} path パス
 * @return {FileType} ファイルの種類
 */
const getFileType = (path) => {
  try {
    const stat = fs.statSync(path);

    switch (true) {
      case stat.isFile():
        return FileType.File;

      case stat.isDirectory():
        return FileType.Directory;

      default:
        return FileType.Unknown;
    }
  } catch (e) {
    return FileType.Unknown;
  }
};

/**
 * 指定したディレクトリ配下のすべてのファイルをリストアップする
 * @param {string} dirPath 検索するディレクトリのパス
 * @return {string[]}
 */
const getHtmlFilePaths = (dirPath) => {
  const result = [];
  const paths = fs.readdirSync(dirPath);

  paths.forEach((a) => {
    const path = `${dirPath}/${a}`;

    switch (getFileType(path)) {
      case FileType.File:
        if (/^.+.html$/.test(path)) result.push(path);
        break;

      case FileType.Directory:
        result.push(...getHtmlFilePaths(path));
        break;

      default:
    }
  });

  return result;
};

const dirPath = path.join(process.cwd(), HTML_FILE_PATH);
const htmlFilePaths = getHtmlFilePaths(dirPath);

htmlFilePaths.forEach((htmlFilePath) => {
  const html = fs.readFileSync(htmlFilePath, "utf-8");
  const document = new JSDOM(html).window.document;

  // scriptタグの削除
  document.querySelectorAll("script").forEach((script) => {
    script.remove();
  });

  fs.writeFileSync(htmlFilePath, document.documentElement.outerHTML);
});

/bin/optimizeHtmlFile/index.jsのようなファイルを作る。

  1. /out下に生成されたHTMLファイルを全て取得
  2. DOM操作ができるjsdomライブラリを使いscriptタグを削除
  3. HTMLファイルとして再書き出し

2. build後に実行するよう設定

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "build": "next build && npm run oh",
  "oh": "bin/optimizeHtmlFile/index.js",

あとはbuild後にそのnpm-scriptsを実行するようにpackage.jsonに追記する。これでruntime-jsを全て無効化できた 🎉