CC 4.0 ライセンス

このセクションの内容は、以下のリンクの内容を基に作成されており、CC BY 4.0 ライセンスの対象となります。

特に明記されていない限り、以下の内容は元のコンテンツに基づいて修正および削除された結果であるとみなすことができます。

コード分割

Rspack はコード分割をサポートしており、コードを他のチャンクに分割できます。生成されるアセットのサイズと数を完全に制御できるため、読み込み時間の向上を実現できます。

ここでは、ブラウザが読み込む必要があるリソースを表す「チャンク」という概念を紹介します。

動的インポート

Rspack は、動的インポートに関する ECMAScript 提案に準拠した import()構文を使用します。

Webpack との動作の不一致

Rspack は require.ensure をサポートしていません。

index.js では、import() を使用して2つのモジュールを動的にインポートすることで、新しいチャンクに分割します。

index.js
import('./foo.js');
import('./bar.js');
foo.js
import './shared.js';
console.log('foo.js');
bar.js
import './shared.js';
console.log('bar.js');

このプロジェクトをビルドすると、src_bar_js.jssrc_foo_js.jsmain.js の3つのチャンクが生成されます。これらを確認すると、shared.jssrc_bar_js.jssrc_foo_js.js の両方に存在することがわかります。重複するモジュールは、後の章で削除します。

情報

shared.js が2つのチャンクに存在しますが、実行されるのは1回だけです。複数のインスタンスの問題を心配する必要はありません。

エントリポイント

これは、コードを分割する最もシンプルで直感的な方法です。ただし、このアプローチでは Rspack を手動で設定する必要があります。複数のエントリポイントから複数のチャンクを分割する方法を見ていきましょう。

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  stats: 'normal',
};

module.exports = config;
index.js
import './shared';
console.log('index.js');
another-module.js
import './shared';
console.log('another-module');

これにより、次のビルド結果が得られます。

... アセットサイズ チャンク チャンク名 another.js 1.07 KiB another [emitted] another index.js 1.06 KiB index [emitted] index エントリポイント another = another.js エントリポイント index = index.js [./src/index.js] 41 bytes {another} {index} [./src/shared.js] 24 bytes {another} {index}

同様に、これらを調べると、すべてに重複する shared.js が含まれていることがわかります。

SplitChunksPlugin

上記で説明したコードセグメンテーションは非常に直感的ですが、最新のブラウザのほとんどは同時ネットワークリクエストをサポートしています。SPA アプリケーションの各ページを単一のチャンクに分割し、ユーザーがページを切り替えるたびに、より大きなチャンクをリクエストする場合、明らかにブラウザの同時ネットワークリクエスト処理能力を有効活用できません。そのため、チャンクをより小さなチャンクに分割できます。このチャンクをリクエストする必要がある場合、これらの小さなチャンクを同時にリクエストするように変更することで、ブラウザのリクエストをより効率的に実行できます。

Rspack はデフォルトで node_modules ディレクトリ内のファイルと重複するモジュールを分割し、これらのモジュールを元のチャンクから別の新しいチャンクに抽出します。では、なぜ上記の例では shared.js が複数のチャンクに繰り返し表示されるのでしょうか?これは、私たちの例の shared.js のサイズが非常に小さいからです。非常に小さなモジュールを個別のチャンクに分割してブラウザに読み込ませると、実際には読み込みプロセスが遅くなる可能性があります。

最小分割サイズを 0 に設定することで、shared.js を単独で抽出できます。

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  entry: {
    index: './src/index.js',
  },
+  optimization: {
+    splitChunks: {
+      minSize: 0,
+    }
+  }
};

module.exports = config;

再ビルドすると、shared.js が個別に抽出され、shared.js を含む追加のチャンクが生成物に追加されていることがわかります。

特定のモジュールの分割を強制

特定のモジュールを強制的に単一のチャンクにグループ化できます。たとえば、次の設定です。

rspack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        test: /\/some-lib\//,
        name: 'lib',
      },
    },
  },
};

上記の構成により、パスに some-lib ディレクトリが含まれているすべてのファイルは、lib という名前の単一のチャンクに抽出できます。some-lib 内のモジュールがほとんど変更されない場合、このチャンクはユーザーのブラウザキャッシュに確実にヒットするため、このようなよく考えられた構成はキャッシュヒット率を高めることができます。

ただし、some-lib を独立したチャンクに分離することには欠点もあります。チャンクが some-lib 内の非常に小さなファイルのみに依存している場合、some-lib のすべてのファイルが単一のチャンクに分割されているため、このチャンクは some-lib チャンク全体に依存する必要があり、ロードボリュームが大きくなります。したがって、cacheGroups.{cacheGroup}.name を使用する場合、慎重な検討が必要です。

cacheGroup の name 設定の効果を示す例を次に示します。

モジュールのプリフェッチ/プリロード

インポートを宣言する際にこれらのインラインディレクティブを使用すると、Rspack はブラウザに次のことを伝える「リソースヒント」を出力します。

  • プリフェッチ: リソースは将来のナビゲーションに必要な可能性が高いです。
  • プリロード: リソースは現在のナビゲーションでも必要になります。

例として、HomePage コンポーネントがあり、LoginButton コンポーネントをレンダリングし、クリック後にLoginModal コンポーネントをオンデマンドで読み込むとします。

LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

これにより、<link rel="prefetch" href="login-modal-chunk.js"> がページの head に追加され、ブラウザにアイドル時間に login-modal-chunk.js ファイルをプリフェッチするように指示します。

情報

親チャンクがロードされると、Rspack はプリフェッチヒントを追加します。

プリロードディレクティブは、プリフェッチと比較して多くの違いがあります。

  • プリロードされたチャンクは、親チャンクと並行して読み込みを開始します。プリフェッチされたチャンクは、親チャンクの読み込みが完了した後に開始します。
  • プリロードされたチャンクは優先度が中程度で、すぐにダウンロードされます。プリフェッチされたチャンクは、ブラウザがアイドル状態のときにダウンロードされます。
  • プリロードされたチャンクは、親チャンクによってすぐにリクエストされる必要があります。プリフェッチされたチャンクは、将来いつでも使用できます。
  • ブラウザのサポートは異なります。

例として、常に個別のチャンクにある必要がある大きなライブラリに依存する Component を挙げることができます。

巨大な ChartingLibrary を必要とする ChartComponent コンポーネントを考えてみましょう。レンダリング時に LoadingIndicator を表示し、ChartingLibrary をオンデマンドでインポートします。

ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');

ChartComponentを使用するページがリクエストされると、<link rel="preload">を介してcharting-library-chunkもリクエストされます。ページチャンクの方が小さく、より早く完了すると仮定すると、既にリクエストされたcharting-library-chunkが完了するまで、ページはLoadingIndicatorを表示します。これにより、往復回数が2回から1回になるため、ロード時間のわずかな向上につながります。特にレイテンシの高い環境では効果的です。

情報

webpackPreloadを正しく使用しないと、パフォーマンスが低下する可能性があるため、注意が必要です。

プリロードを独自に制御する必要がある場合があります。例えば、動的インポートのプリロードは非同期スクリプトを介して行うことができます。これは、ストリーミングサーバーサイドレンダリングの場合に役立ちます。

const lazyComp = () =>
  import('DynamicComponent').catch(error => {
    // Do something with the error.
    // For example, we can retry the request in case of any net error
  });

Rspackがスクリプトの読み込みを開始する前にスクリプトの読み込みが失敗した場合(Rspackは、スクリプトがページにない場合、そのコードを読み込むためのスクリプトタグを作成します)、chunkLoadTimeoutが経過するまで、catchハンドラは開始されません。この動作は予期せぬものになる可能性があります。しかし、これは説明可能です。Rspackは、スクリプトが失敗したことを知らないため、エラーをスローできません。Rspackは、エラーが発生した直後に、スクリプトにonerrorハンドラを追加します。

このような問題を防ぐために、エラーが発生した場合にスクリプトを削除する独自のonerrorハンドラを追加できます。

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

その場合、エラーが発生したスクリプトは削除されます。Rspackは独自のスクリプトを作成し、タイムアウトなしでエラーが処理されます。