Web サイトには、アクセス解析や広告、チャットウィジェットといった外部サービスのスクリプトを埋め込む場面がよくあります。素の <script> タグをそのまま置くこともできますが、読み込むタイミング次第ではページの表示速度を落としてしまいます。Next.js には、こうした外部スクリプトを最適なタイミングで読み込むための next/script という仕組みが用意されています。この記事では、App Router を前提に、Script コンポーネントの基本的な書き方、読み込みタイミングを決める strategy プロパティの使い分け、インラインスクリプトやイベントハンドラの扱い方、そして実際の Google Analytics 風の読み込み例まで解説します。
目次
next/script の Script コンポーネントとは
next/script が提供する Script コンポーネントは、外部スクリプトを最適化して読み込むための Next.js の仕組みです。アナリティクスや広告タグ、SNS の埋め込み、チャットウィジェットなど、自分のアプリの外部から読み込むスクリプトを扱うときに、素の <script> の代わりに使います。
なぜわざわざ専用のコンポーネントを使うのかというと、大きく2つの理由があります。1つは読み込みタイミングを制御できることです。後述する strategy プロパティで「いつスクリプトを読み込むか」を指定でき、ページの表示(First Paint やインタラクティブになるまでの時間)を妨げないようにできます。もう1つは重複読み込みを防げることです。同じ src のスクリプトが複数の場所で使われても、Next.js が同一スクリプトを一度しか読み込まないように管理してくれます。
基本の書き方
まずは最小の例です。next/script から Script をインポートし、読み込みたいスクリプトの URL を src に渡すだけです。素の <script src="..."> を書く感覚とほとんど変わりません。
import Script from 'next/script';
export default function Page() {
return (
<>
<h1>トップページ</h1>
{/* 外部スクリプトを読み込む */}
<Script src="https://example.com/widget.js" />
</>
);
}
Script は Server Component の中でも使えます。src を指定しただけの場合、読み込みタイミングは既定の afterInteractive(ページがインタラクティブになった直後)になります。どのページに置くかによって読み込まれる範囲が変わり、特定のページの page.tsx に置けばそのページだけ、後述する root layout に置けば全ページで読み込まれます。
strategy プロパティで読み込みタイミングを制御する
next/script の中心となるのが strategy プロパティです。スクリプトを「いつ」読み込むかを指定するもので、次の4つの値から選びます。スクリプトの役割に応じて適切な値を選ぶことで、ページの表示パフォーマンスへの影響を抑えられます。
| strategy | 読み込みタイミング | 向いている用途 |
|---|---|---|
beforeInteractive | ページがインタラクティブになる前(Next.js のコードより先)に読み込む | 同意管理(Cookie 同意)、ボット検知など、ページ描画前に動く必要があるもの。root layout で使う |
afterInteractive(デフォルト) | ページがインタラクティブになった直後に読み込む | タグマネージャ、アクセス解析など。指定しない場合はこれになる |
lazyOnload | ブラウザのアイドル時間(他の処理が落ち着いてから)に読み込む | チャットウィジェット、SNS 埋め込みなど優先度の低いもの |
worker(実験的) | メインスレッドではなく Web Worker で読み込む | メインスレッドを空けたい重いスクリプト。experimental な機能 |
基本的な考え方はシンプルです。ページ描画より前に絶対動かす必要があるものだけ beforeInteractive、すぐに動いてほしい解析系は既定の afterInteractive、後回しでよいものは lazyOnload、と役割で振り分けます。worker は現時点で実験的な位置づけのため、有効化には next.config での設定が必要で、本番運用では慎重に検討する必要があります。
strategy の指定例
読み込みタイミングを変えたいときは、strategy に値を渡すだけです。優先度の低いチャットウィジェットを、ブラウザのアイドル時間に読み込む例を見てみます。
import Script from 'next/script';
export default function Page() {
return (
<>
<h1>トップページ</h1>
{/* アイドル時間に読み込むので、初期表示を邪魔しない */}
<Script
src="https://example.com/chat-widget.js"
strategy="lazyOnload"
/>
</>
);
}
インラインスクリプトを書くときは id が必須
外部ファイルではなく、その場に直接コードを書く「インラインスクリプト」も Script で扱えます。書き方は2通りあり、いずれの場合もid 属性が必須です。Next.js は指定された id を使ってスクリプトを識別し、重複実行を防いだり読み込みを追跡したりするため、これを付けないと正しく動作しません。
1つ目は、Script の子要素としてコードを書く方法です。
import Script from 'next/script';
export default function Page() {
return (
<>
<h1>トップページ</h1>
{/* id は必須 */}
<Script id="show-banner">
{`document.getElementById('banner').classList.remove('hidden');`}
</Script>
</>
);
}
2つ目は、dangerouslySetInnerHTML を使う方法です。こちらも id は必須です。
<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').classList.remove('hidden');`,
}}
/>
どちらの書き方でも結果は同じです。テンプレートリテラル(バッククォート)で囲むと改行を含むコードを素直に書けるので、子要素として渡す前者のほうが読みやすいことが多いでしょう。
onLoad・onReady・onError で読み込み後の処理を書く
スクリプトの読み込み状況に応じて何か処理をしたいときは、イベントハンドラを使います。Script には次の3つのハンドラが用意されています。それぞれ呼ばれるタイミングが異なります。
| ハンドラ | 呼ばれるタイミング |
|---|---|
onLoad | スクリプトの読み込みが完了した直後に一度だけ呼ばれる |
onReady | 読み込み完了後、およびそのコンポーネントがマウントされるたびに呼ばれる |
onError | スクリプトの読み込みに失敗したときに呼ばれる |
これらのイベントハンドラはClient Component の中でのみ使えます。イベントハンドラは関数であり、Server Component から Client 側へ渡すことができないためです。ファイルの先頭に 'use client' を付けたコンポーネントで利用します。
'use client';
import Script from 'next/script';
export default function Widget() {
return (
<Script
src="https://example.com/widget.js"
onLoad={() => {
// 読み込み完了後に初期化処理を実行する
console.log('スクリプトの読み込みが完了しました');
}}
onError={(e) => {
console.error('読み込みに失敗しました', e);
}}
/>
);
}
実用例:Google Analytics を読み込む
実際によくあるユースケースとして、アクセス解析(Google Analytics 風)の読み込み例を見てみます。解析タグは「外部スクリプトの読み込み」と「初期化のためのインラインスクリプト」の2つをセットで使うのが定番です。前者は素直に src で、後者はインラインスクリプトとして id を付けて書きます。解析系はページがインタラクティブになった直後で十分なので、既定の afterInteractive が適しています。
import Script from 'next/script';
const GA_ID = 'G-XXXXXXXXXX';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
{children}
{/* 1. 解析ライブラリ本体を読み込む */}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
strategy="afterInteractive"
/>
{/* 2. 初期化用のインラインスクリプト(id は必須) */}
<Script id="ga-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}');
`}
</Script>
</body>
</html>
);
}
全ページで計測したいので、この例では root layout(app/layout.tsx)に置いています。Script が重複読み込みを防いでくれるため、layout に置いてもページ遷移のたびに二重に読み込まれる心配はありません。
beforeInteractive が効かない・使える場所
strategy="beforeInteractive" を指定したのに思ったタイミングで読み込まれない、というのはつまずきやすいポイントです。原因の多くは「置く場所」にあります。
beforeInteractive は「ページがインタラクティブになる前」、つまり Next.js 自身のコードよりも先に読み込むための指定です。この特性を成立させるため、root layout(app/layout.tsx)に置く必要があります。個別のページ(page.tsx)や、下の階層のネストされた layout に beforeInteractive の Script を置いても、意図したとおりには機能しません。全ページ共通で、かつ描画前に確実に動かす必要があるスクリプトのための指定だと考えると、置き場所が root layout に限られる理由が分かりやすいでしょう。
逆に言えば、同意管理やボット検知のように「本当にページ描画前でなければ困る」もの以外に beforeInteractive を使う必要はありません。ほとんどの外部スクリプトは既定の afterInteractive か lazyOnload で十分で、むやみに beforeInteractive を使うと、かえって初期表示を遅らせてしまいます。
まとめ
next/script の Script コンポーネントは、アナリティクスや広告、チャットウィジェットといった外部スクリプトを最適化して読み込むための仕組みです。src を渡すだけの基本形は素の <script> とほぼ同じですが、strategy プロパティで読み込みタイミングを制御でき、重複読み込みも防げます。strategy は、描画前に動かす beforeInteractive、既定でインタラクティブ直後の afterInteractive、アイドル時間の lazyOnload、実験的な worker の4つがあり、スクリプトの役割で使い分けます。インラインスクリプトを書くときは id が必須で、onLoad・onReady・onError のイベントハンドラは Client Component 内でのみ使えます。beforeInteractive は root layout に置く必要がある点も押さえておきましょう。