1. ホーム
  2. React

【React】react-dom の preload・preconnect の使い方|リソースを先読みして表示を速くする

Share

React 19 の react-dom には、あとで必要になるリソースをブラウザに前もって知らせるための関数が用意されています。preloadpreconnect を呼ぶだけで、React が <link rel="preload"> のようなタグを <head> に挿入してくれます。この記事では preload / preinit / preconnect / prefetchDNS の4つについて、それぞれの役割の違いと具体的な書き方、HTML に手書きする場合との使い分けを解説します。

ブラウザに「これから使うもの」を先に伝える4つの関数

ブラウザは HTML を上から解析しながら、必要になった順にリソースを取りに行きます。そのため、たとえばフォントの URL が CSS の中に書かれていると、CSS をダウンロードして解析し終えるまでフォントの取得が始まりません。あらかじめ「このファイルはあとで使う」と伝えておけば、ブラウザは待たずに先へ進めます。この「伝える」処理を React のコンポーネント側から書けるようにしたのが、リソースプリロード用の API です。

いずれも react-dom からインポートして使います。名前が似ていますが、やることの深さがはっきり違います。

App.tsx
import { preload, preinit, preconnect, prefetchDNS } from 'react-dom';
関数ブラウザに与えるヒント
prefetchDNS(href)これから接続するドメイン名に対応する IP アドレスをプリフェッチする(DNS 解決だけ)
preconnect(href)具体的なリソースが分からなくても、リクエスト先のサーバへの接続を事前に確立する
preload(href, options)使用予定のスタイルシート・フォント・画像・外部スクリプトをフェッチする
preinit(href, options)外部スクリプトをフェッチして実行する、またはスタイルシートをフェッチして挿入する

上から順に、DNS 解決だけ → 接続の確立まで → ファイルのダウンロードまで → 実行・適用まで、と踏み込む度合いが深くなっていくと考えると整理しやすくなります。なお react-dom には ESM モジュール用の preloadModulepreinitModule もありますが、この記事では扱いません。

preload でリソースを先にダウンロードさせる

preload(href, options) は、指定したリソースのダウンロードを開始するようブラウザにヒントを与える関数です。第1引数にダウンロードしたい URL、第2引数にオプションのオブジェクトを渡します。options は必須で、その中の as も必須です。as はリソースの種類をブラウザに伝えるもので、これがないとブラウザは適切な優先度を決められません。

AppRoot.tsx
import { preload } from 'react-dom';

function AppRoot() {
  // スクリプトを先読みする
  preload('https://example.com/script.js', { as: 'script' });

  // スタイルシートを先読みする
  preload('https://example.com/style.css', { as: 'style' });

  return <div>...</div>;
}

preload は何も返しません。あくまで「ダウンロードしておいて」というヒントなので、ダウンロードしたものを受け取って使うための関数ではない、という点は押さえておいてください。実際に使うのは、いつもどおり <img><link>、動的 import() の側です。

as に指定できる値

as に指定できるのは次の値です。先読みしたいファイルが何なのかに合わせて選びます。

as の値対象となるリソース
'style'スタイルシート
'script'スクリプト
'font'フォントファイル
'image'画像
'fetch'fetch や XHR で取得するデータ
'audio' / 'video' / 'track'音声・動画・字幕トラック
'document' / 'embed' / 'object'フレームや埋め込みコンテンツ
'worker'Web Worker のスクリプト

as 以外の主なオプション

options には as のほかにもいくつかの指定ができます。よく使うものを挙げます。

オプション指定できる値と意味
crossOrigin'anonymous''use-credentials'。使用する CORS ポリシー。as: 'fetch' のときは必須
fetchPriority'auto'(デフォルト)/ 'high' / 'low'。フェッチの相対的な優先度
typeリソースの MIME タイプ
integrityリソースの真正性を検証するための暗号学的ハッシュ
nonce厳格な Content Security Policy を使うときの暗号学的 nonce
referrerPolicy送信する Referrer ヘッダの内容。'no-referrer-when-downgrade'(デフォルト)など
imageSrcSetas: 'image' のときだけ使う。画像のソースセット
imageSizesas: 'image' のときだけ使う。画像のサイズ

fetchPriority: 'high' は、ファーストビューに出る主役の画像のように「まっさきに欲しい」ものに使います。逆に、あとで表示される可能性はあるが急がないものには 'low' を指定して、重要なリソースの取得を邪魔しないようにできます。

レスポンシブ画像を先読みする

画面幅によって読み込む画像を切り替えている場合、URL をひとつ指定するだけでは足りません。imageSrcSetimageSizes<img> 側と同じ内容で渡すことで、ブラウザは実際に使う1枚だけを先読みしてくれます。

Hero.tsx
import { preload } from 'react-dom';

const SRC_SET = '/banner512.png 512w, /banner1024.png 1024w';
const SIZES = '(max-width: 512px) 512px, 1024px';

function Hero() {
  preload('/banner.png', {
    as: 'image',
    imageSrcSet: SRC_SET,
    imageSizes: SIZES,
    fetchPriority: 'high',
  });

  return (
    <img
      src="/banner.png"
      srcSet={SRC_SET}
      sizes={SIZES}
      alt="バナー"
    />
  );
}

画像の場合、同じ呼び出しかどうかの判定は href だけでなく imageSrcSetimageSizes も含めて行われます。<img> 側とこれらの値がずれていると、ブラウザが別のリソースとみなして二重にダウンロードしてしまう可能性があるため、上の例のように定数へ切り出して共有しておくと安全です。

preinit はダウンロードして実行・適用まで行う

preinit(href, options) は、外部スクリプトをフェッチして実行する、またはスタイルシートをフェッチして挿入するところまでやってくれる関数です。preload がダウンロードして手元に置いておくだけなのに対し、preinit はそのリソースを実際にページへ効かせます。この違いから、as に指定できるのは 'script''style' の2つだけです。

AppRoot.tsx
import { preinit } from 'react-dom';

function AppRoot() {
  // スクリプトをダウンロードして実行する
  preinit('https://example.com/script.js', { as: 'script' });

  // スタイルシートをダウンロードして適用する
  preinit('https://example.com/style.css', {
    as: 'style',
    precedence: 'medium',
  });

  return <div>...</div>;
}

as: 'style' のときは precedence が必須です。値は 'reset' / 'low' / 'medium' / 'high' のいずれかで、他のスタイルシートに対する相対的な挿入位置を指定します。優先度が高いスタイルシートは、低いものをオーバーライドできます。このほか crossOrigin / integrity / nonce / fetchPrioritypreload と同じように指定できます。

どちらを使うかの判断はシンプルで、そのリソースを今すぐ効かせたいなら preinit、あとで使うから取得だけしておきたいなら preload です。たとえばモーダルを開くボタンにマウスが乗った時点でモーダル用の CSS を適用してしまってよいなら preinit、実際に開いたときに初めて当てたいなら preload でファイルだけ温めておく、という具合です。

preconnect と prefetchDNS で接続そのものを前倒しする

preloadpreinit は「どのファイルを取りに行くか」が分かっている場合の関数です。一方で、外部の API やドメインを使うことは分かっているけれど URL までは決まっていない、という場面もあります。そのときに使うのが preconnectprefetchDNS です。どちらも引数は URL 文字列ひとつだけで、オプションはありません。

AppRoot.tsx
import { preconnect, prefetchDNS } from 'react-dom';

function AppRoot() {
  // 接続の確立まで済ませておく
  preconnect('https://api.example.com');

  // DNS 解決だけ済ませておく
  prefetchDNS('https://cdn.example.com');

  return <div>...</div>;
}

preconnect は DNS 解決に加えて TCP 接続や TLS ハンドシェイクまで先に済ませるため効果は大きいのですが、そのぶんコストもかかります。React の公式ドキュメントでは、多数のドメインに対して投機的に接続する場合は preconnect のオーバーヘッドが利益を上回る可能性があるため prefetchDNS が適している、と説明されています。ほぼ確実に使うと分かっている少数のドメインには preconnect、使うかどうか五分五分のドメインが多数あるなら prefetchDNS と考えるとよいでしょう。

なお、ページ自体と同じホストに対して preconnectprefetchDNS を呼んでも意味はありません。すでに接続済み・解決済みだからです。これらが効くのは外部ドメインに対してだけです。

どこで呼べるのか、何度呼んでよいのか

レンダー中でもイベントハンドラ内でも呼べる

これら4つの関数は、ブラウザ上ではコンポーネントのレンダー中・エフェクト内・イベントハンドラ内など、どんな状況でも呼び出せます。上の例のようにコンポーネント本体で直接呼んでいるのが違和感があるかもしれませんが、これは公式ドキュメントに載っている使い方です。フックではないので、条件分岐の中で呼んでも問題ありません。

特に相性がよいのがイベントハンドラです。次のページや次の状態がレンダーされる時点で読み込むのに比べて、ユーザーが操作を始めた瞬間から準備を始められます。

WizardButton.tsx
import { preinit, preconnect } from 'react-dom';

function WizardButton({ onStart }: { onStart: () => void }) {
  return (
    <button
      // マウスが乗った時点で接続だけ先に確立しておく
      onMouseEnter={() => {
        preconnect('https://api.example.com');
      }}
      onClick={() => {
        // クリック時にウィザード用の CSS を適用してから開始する
        preinit('https://example.com/wizardStyles.css', { as: 'style' });
        onStart();
      }}
    >
      設定を始める
    </button>
  );
}

サーバサイドレンダリングの場合は事情が少し違い、レンダー中またはそのレンダーから派生した非同期処理の中で呼んだときにだけ効果があります。それ以外のタイミングで呼んでも無視されるため、SSR で効かせたいものはコンポーネントのレンダー中に呼ぶようにしてください。

同じ href で何度呼んでも1回として扱われる

4つの関数はいずれも、同等の呼び出しを複数回行っても効果は1回だけです。同等かどうかは href が同じかどうかで判定されます(preload で画像を扱う場合のみ hrefimageSrcSetimageSizes がすべて同じかどうか)。

この性質のおかげで、重複を気にせずコンポーネント側に書けます。同じフォントを使うコンポーネントがページ内に10個あって、それぞれが同じ preload を呼んでも、生成される <link> は1つだけです。レンダーのたびに呼ばれることになりますが、useMemo などで呼び出し回数を抑える必要もありません。

link rel=”preload” を手書きする場合との違い

リソースの先読み自体は、もともと HTML の <link rel="preload"> でできることです。React のこれらの関数を呼ぶと、React が対応するタグをドキュメントの <head> に挿入します。React 公式ブログでは、4つの関数を呼んだ結果として次のような DOM が生成されると説明されています。

生成される HTML(React 公式ブログの例)
<html>
  <head>
    <link rel="prefetch-dns" href="https://...">
    <link rel="preconnect" href="https://...">
    <link rel="preload" as="font" href="https://.../path/to/font.woff">
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
    <script async="" src="https://.../path/to/some/script.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

公式ブログには、これらのタグは呼び出し順ではなく初期ロードにとっての有用性の順に並べ替えられるという注記も添えられています。書いた順番どおりに出力されるわけではない、ということです。

では、HTML に直接書くのと何が違うのでしょうか。最大の違いは宣言できる場所です。<link rel="preload"> を手書きする場合、その記述はドキュメントの <head> に置くことになります。しかし「このフォントが必要かどうか」を知っているのは、実際にそれを使うコンポーネントです。head 側にまとめて書くと、コンポーネントを消したのに preload の記述だけ残る、といったズレが起きやすくなります。

React の関数を使えば、リソースを必要とするコンポーネントのすぐそばに先読みの宣言を置けます。そのコンポーネントが描画されないページでは呼び出しも起きないので、不要な先読みが残りません。条件付きで表示されるコンポーネントや、ルーティングによって出し分けられる画面と特に相性がよい書き方です。

ただし、フレームワークがこれらを自動で行っている場合もあります。公式ドキュメントでも、フレームワークが自動で処理してくれることが多いため直接呼ぶ必要がないケースがある、と触れられています。Next.js のようにフォントや画像の最適化機構を持つフレームワークを使っている場合は、まずその仕組みに任せて、それでも足りない部分に手を入れるのがよいでしょう。

まとめ

react-dom のリソースプリロード用 API は、あとで必要になるリソースをブラウザへ先に伝えるための関数群です。prefetchDNS(href) は DNS 解決だけ、preconnect(href) は接続の確立まで、preload(href, options) はファイルのダウンロードまで、preinit(href, options) は実行・適用まで行います。preloadoptions.as が必須で 'style' / 'script' / 'font' / 'image' などを指定し、preinitas'script''style' のみ、スタイルシートでは precedence も必須です。

いずれもブラウザ上ならレンダー中・エフェクト内・イベントハンドラ内のどこからでも呼べ、同じ href での重複呼び出しは1回として扱われます。React が対応する <link> タグなどを <head> に挿入してくれるので、head に手書きするのではなく、リソースを使うコンポーネントのそばで宣言できるのがこの API の利点です。

参考ページ