【Next.js11】next/scriptで外部スクリプトの実行順序を制御する

## はじめに
WEBサイトでは外部スクリプト、例えば広告計測用のGoogle/Yahoo/Facebookタグを入れていることがほとんどだと思います。今回広告計測用ではありませんが、ある外部スクリプトを必ず読み込んでからでないと、実行できない処理があり、Next.js11のnext/scriptで解決できたのでご紹介します。

また、外部スクリプトは読み込みパフォーマンスがLCP低下の要因となり、ユーザーエクスペリエンスを損ねてしまう可能性があります。そんなときに、Next.js11のnext/scriptを使えば、スクリプトの実行順序を制御でき、問題を改善できるのでオススメです。

## next/script とは
2021/6/16に発表されたNext.js11の新機能です。

> With `next/script`, you can define the `strategy` property and Next.js will automatically prioritize them to improve loading performance:

> - `beforeInteractive`: For critical scripts that need to be fetched and executed <b>before</b> the page is interactive, such as bot detection and consent management. These scripts are injected into the initial HTML from the server and run before self-bundled JavaScript is executed.
- `afterInteractive` (<b>default</b>): For scripts that can fetch and execute <b>after</b> the page is interactive, such as tag managers and analytics. These scripts are injected on the client-side and will run after hydration.
- `lazyOnload` For scripts that can wait to load during idle time, such as chat support and social media widgets.

引用元：[Script Optimization](https://nextjs.org/blog/next-11#script-optimization)

公式ブログから下記のように解釈しました。詳細は原文を参照ください。

`next/script`では、`strategy`プロパティを使用することで、自動的に優先順位を付けることができる。

- `beforeInteractive` ：
    - サーバー側に最初から挿入される
    - ページがインタラクティブになる前にフェッチして実行したいスクリプトに有用
    - 例）ボット検出や同意管理など

- `afterInteractive` ：
    - デフォルト
    - クライアント側に挿入され、ハイドレーション後に実行される
    - ページがインタラクティブになった後にフェッチして実行したいスクリプトに有用
    - 例）タグマネージャーやアナリティクスなど

- `lazyOnload` ：
    - アイドル時間中にロードを待機できるスクリプトに有用
    - 例）チャットサポートやソーシャルメディアウィジェットなど

また、ユーザーの同意が完了してからコードを実行する処理のように、ロード後にコードを実行することもできる。

## 使い方
### 基本的な使い方
```js
<Script
  src="https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.map"
  strategy="beforeInteractive" // lazyOnload, afterInteractive
/>
```
### ロード後にコードを実行する場合の使い方
```tsx
<Script
  src={url} // consent mangagement
  strategy="beforeInteractive"
  onLoad={() => {
    // If loaded successfully, then you can load other scripts in sequence
  }}
/>
```
上記のコードは[Script Optimization](https://nextjs.org/blog/next-11#script-optimization)から引用しています。

## 実際に使ってみた例を紹介
### やりたいこと
今回は、外部スクリプトを読み込み→外部スクリプトの機能であるフォームを表示させ→サブミットします。
① 本番環境と開発環境で変数に入れる値を変える
② ①で設定したbaseUrlを使用して外部スクリプトを読み込む
③ ②の読み込みが完了したら、そのスクリプトの機能を使用してフォームを表示する
④ サブミット処理を実行

```tsx
// ① 本番環境と開発環境で変数に入れる値を変える
if (window.location.host === '本番のURL') {
  baseUrl = '//prod.example.com'
  hogeId = '1111'
  fugaId = '1234'
} else if (window.location.host === '開発環境のURL') {
  baseUrl = '//dev.example.com'
  hogeId = '2222'
  fugaId = '5678'
}

// ② ①で設定したbaseUrlを使用して外部スクリプトを読み込む
<script src=baseUrl + '/js/hoge.min.js'></script>

// ③ ②の読み込みが完了したら、そのスクリプトの機能を使用してフォームを表示する
<script>
window.hoge.loadForm(baseUrl, hogeId, fugaId, function(form) {
  // ④ サブミット処理を実行
  form.onSubmit(function () {
    sendhogehoge;
  })
});
</script>
```

### next/scriptを使用して書いてみる

まずは.envファイルに環境変数を書きます。
環境変数の設定の詳細は[公式ドキュメント](https://nextjs.org/docs/basic-features/environment-variables)を参考にしてください。

```js
// .env
NEXT_PUBLIC_PROD_HOST='本番のURL'
NEXT_PUBLIC_PROD_BASE_URL='//prod.example.com'
NEXT_PUBLIC_PROD_HOGEID='1111'
NEXT_PUBLIC_PROD_FUGAID='1234'
NEXT_PUBLIC_DEV_HOST='開発環境のURL'
NEXT_PUBLIC_DEV_BASE_URL='//dev.example.com'
NEXT_PUBLIC_DEV_HOGEID='2222'
NEXT_PUBLIC_DEV_FUGAID='5678'
```

①〜④の処理を書きます。

```tsx
// next/scriptを読み込む
import Script from "next/script";
//省略
export default function Form(): JSX.Element {
  const [baseUrl, setBaseUrl] = useState<string>("");
  const [hogeId, setHogeId] = useState<string>("");
  const [fugaId, setFugaId] = useState<string>("");
  // ① 本番環境と開発環境で変数に入れる値を変える
  useEffect(() => {
    if (typeof window !== "undefined") {
      if (
        process.env
          .NEXT_PUBLIC_PROD_HOST!.split(",")
          .includes(window.location.host)
      ) {
        setBaseUrl(process.env.NEXT_PUBLIC_PROD_BASE_URL || "");
        setHogeId(process.env.NEXT_PUBLIC_PROD_HOGEID || "");
        setFugaId(process.env.NEXT_PUBLIC_PROD_FUGAID || "");
      } else if (
        process.env
          .NEXT_PUBLIC_DEV_HOST!.split(",")
          .includes(window.location.host)
      ) {
        setBaseUrl(process.env.NEXT_PUBLIC_DEV_BASE_URL || "");
        setHogeId(process.env.NEXT_PUBLIC_DEV_HOGEID || "");
        setFugaId(process.env.NEXT_PUBLIC_DEV_FUGAID || "");
      }
    }
  }, []);

  return (
    <>
      <form id={fugaId} />
        {typeof window === "undefined" || !baseUrl ? (
        <></>
        ) : (
        // ② ①で設定したbaseUrlを使用して外部スクリプトを読み込む
        <Script
          src={baseUrl + "/js/hoge.min.js"} // consent mangagement
          strategy="afterInteractive"

          // ③ ②の読み込みが完了したら、そのスクリプトの機能を使用してフォームを表示する
          onLoad={() => {
            window.hoge.loadForm(
              baseUrl,
              hogeId,
              fugaId,
              // ④ サブミット処理を実行
              function (form: any) {
                form.onSubmit(function () {
                  sendhogehoge;
                });
              }
            );
          }}
        />
        )}
    </>
  );
}

```

### 解説
<b> ① 本番環境と開発環境で変数に入れる値を変える変数をセットする</b>
useEffect内に記述して、変数をセットします。
第二引数を空（[ ]）にしているので、初回のみ実行されます。

<b>② ①で設定したbaseUrlを使用して外部スクリプトを読み込む</b>
ロード後にコードを実行する場合の使い方を参考にして、スクリプトを読み込みます。
`strategy`は`afterInteractive`に設定しているので、ハイドレーション後に実行されます。

<b>③ ②の読み込みが完了したら、そのスクリプトの機能を使用してフォームを表示する</b>
`onLoad=`以降に記述しているので、②のロード後に、フォームを表示するスクリプトが実行されます。

<b>④ サブミット処理を実行</b>
③のフォーム読み込み内に関数を記述しているので、最後にサブミット処理が実行されます。

## まとめ
Next.js11のnext/scriptは、

- `strategy`プロパティで自動的に優先順位を付けることができる
    - `beforeInteractive`：サーバー側で最初からHTMLに挿入され、セルフバンドルされたJavaScriptが実行される前に実行される
    - `afterInteractive`：クライアント側に挿入され、ハイドレーション後に実行される
    - `lazyOnload`：アイドル状態になったら実行される
- `onLoad`を使用すれば、ロード後にコードを実行することができる


今回は、ロード後にコードを実行する例をご紹介しましたが、`strategy`プロパティを使用して外部スクリプトの読み込みを制御することでLCP向上が見込めると思います。