- Angular
Angular遅延ロード|loadComponentとloadChildren
Angular の遅延ロードとは、ルートに loadComponent や loadChildren を書いて、その画面の JavaScript を別ファイル(lazy chunk)に分け、URL に移動したときに初めて読み込む仕組みです。 最初に読み込むファイル(initial chunk)が小さくなるので、画面数が増えても最初の表示を軽く保てます。書き方は loadComponent: () => import('./pages/products').then((m) => m.Products) のように、動的 import() を返す関数を渡すだけです。
この記事では、5画面の小さなアプリを遅延ロードなし・ありの両方でビルドし、ng build の出力をそのまま並べて初期バンドルの差を数値で示します。さらに、PreloadAllModules と独自のプリロード戦略を入れたときにどのファイルがいつ読み込まれるかをブラウザのネットワークログで記録しました。以前の文字列指定('./lazy/lazy.module#LazyModule' の形)をいまの Angular に書くと出るエラーと、書き換え例も載せています。
確認環境:Ubuntu 24.04 / Node.js 24.21.0 / Angular CLI 22.2.1・Angular 22.2.1(ng new の既定構成)/ TypeScript 6.0.3。ネットワークログは Playwright 1.56.0 の Chromium で ng build の出力を開いて記録しました。
Angular 遅延ロードの早見表
| やりたいこと | 書き方(コピペ用) | 分かれる単位 |
|---|---|---|
| 1画面を遅延ロードする | loadComponent: () => import('./pages/products').then((m) => m.Products) |
コンポーネント1つ |
.then() を省く |
コンポーネント側を export default class Report {} にして loadComponent: () => import('./pages/report') |
コンポーネント1つ |
| 配下の画面をまとめて遅延ロードする | loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) |
ルート配列と、その中で使う画面 |
ルート配列の .then() を省く |
ルート側を export default [...] satisfies Routes にして loadChildren: () => import('./admin/admin.routes') |
同上 |
| 画面の一部だけを後から読む | テンプレートで @defer (on interaction(btn)) { ... } @placeholder { ... } |
テンプレート内のブロック |
| 遅延ロードした画面を先読みする | provideRouter(routes, withPreloading(PreloadAllModules)) |
すべての遅延ルート |
| 選んだルートだけ先読みする | data: { preload: true } と独自の PreloadingStrategy |
印を付けたルート |
| 使い分け | loadComponent | loadChildren | @defer |
|---|---|---|---|
| 書く場所 | ルート定義 | ルート定義 | テンプレート |
| 読み込むきっかけ | その URL へ移動 | 配下の URL へ移動 | トリガー(表示・操作・時間など) |
| 向いている場面 | 単独の画面 | 画面がいくつもある機能(管理画面など) | 画面内の重い部品 |
| URL との関係 | 1ルート = 1画面 | 1つのパスの下に子ルート | URL と無関係 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular遅延ロード|loadComponentとloadChildren」
遅延ロード前後の ng build 出力を比べる
サンプルアプリの構成
ng new で作ったプロジェクトに、次の5画面を用意しました。
| パス | 画面 | 中身 |
|---|---|---|
/ |
ホーム | 文章だけ |
/products |
商品一覧 | @for で一覧を表示 |
/contact |
問い合わせ | リアクティブフォーム(@angular/forms を使う) |
/report |
レポート | DatePipe・DecimalPipe を使う。default export |
/admin |
管理 | 子ルート users・settings(settings は FormsModule を使う) |
npx @angular/cli@latest new app --skip-git --defaults --style=css --ssr=false
cd app
遅延ロードなし(すべて静的 import)
import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { Products } from './pages/products';
import { Contact } from './pages/contact';
import Report from './pages/report';
import { ADMIN_ROUTES } from './admin/admin.routes';
export const routes: Routes = [
{ path: '', component: Home },
{ path: 'products', component: Products },
{ path: 'contact', component: Contact },
{ path: 'report', component: Report },
{ path: 'admin', children: ADMIN_ROUTES },
];
Initial chunk files | Names | Raw size | Estimated transfer size
main-TUZ5RPHN.js | main | 290.77 kB | 77.30 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes
| Initial total | 290.77 kB | 77.30 kB
すべての画面が main の1ファイルに入り、ホームを開くだけの人も問い合わせフォームや管理画面のコードを受け取ります。
遅延ロードあり(loadComponent と loadChildren)
app.routes.ts だけを次のように書き換えます。コンポーネント側は変更していません。
import { Routes } from '@angular/router';
import { Home } from './pages/home';
export const routes: Routes = [
{ path: '', component: Home },
{
path: 'products',
loadComponent: () => import('./pages/products').then((m) => m.Products),
},
{
path: 'contact',
loadComponent: () => import('./pages/contact').then((m) => m.Contact),
},
{
// default export のコンポーネントは .then() を省略できる
path: 'report',
loadComponent: () => import('./pages/report'),
},
{
// ルート配列を返す。配下の画面がまとめて別ファイルになる
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
Initial chunk files | Names | Raw size | Estimated transfer size
chunk-DB4Yqjat.js | - | 138.47 kB | 41.56 kB
main-7IZWTCQU.js | main | 108.49 kB | 28.38 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes
| Initial total | 246.96 kB | 69.93 kB
Lazy chunk files | Names | Raw size | Estimated transfer size
chunk-CaMogLIE.js | - | 42.60 kB | 9.34 kB
chunk-C7JWlwtp.js | admin-routes | 1.83 kB | 813 bytes
chunk-DwSFzM4L.js | contact | 1.37 kB | 706 bytes
chunk-9OqahxDz.js | products | 667 bytes | 667 bytes
chunk-CLtViVLR.js | report | 581 bytes | 581 bytes
数値で見る差
| 項目 | 遅延ロードなし | 遅延ロードあり | 差 |
|---|---|---|---|
| Initial total(Raw size) | 290.77 kB | 246.96 kB | −43.81 kB(約15%減) |
| Initial total(転送サイズの見積もり) | 77.30 kB | 69.93 kB | −7.37 kB(約10%減) |
| Lazy chunk の数 | 0 | 5 | +5 |
| Lazy chunk の合計(Raw size) | 0 | 約47.05 kB | — |
| Lazy chunk | Names 列 | Raw size | 何が入っているか |
|---|---|---|---|
| chunk-CaMogLIE.js | - |
42.60 kB | @angular/forms。問い合わせと管理の設定画面の両方が使うため、共有チャンクとして自動で切り出された |
| chunk-C7JWlwtp.js | admin-routes | 1.83 kB | ADMIN_ROUTES と、そこから静的 import している3つのコンポーネント |
| chunk-DwSFzM4L.js | contact | 1.37 kB | 問い合わせ画面 |
| chunk-9OqahxDz.js | products | 667 bytes | 商品一覧画面 |
| chunk-CLtViVLR.js | report | 581 bytes | レポート画面 |
ここから分かることが3つあります。
- 減った 43.81 kB のほとんどは
@angular/forms(42.60 kB)です。 画面そのもののコードは1つ1 kB 前後で、初期バンドルを大きくしているのは「その画面だけが使うライブラリ」でした。遅延ロードの効果は、遅延させる画面が抱えているライブラリの大きさで決まります。 - 複数の遅延ルートが同じライブラリを使うと、共有チャンクが自動で作られます。 Names 列が
-のファイルがそれです。同じコードが2つのチャンクに重複して入ることはありませんでした。 - Initial と Lazy の合計は 294.01 kB で、分割前の 290.77 kB より少し増えています。 全体が小さくなるのではなく、「最初に必要な分」と「あとで必要な分」に分ける仕組みだと分かります。
ブラウザが実際に読み込むファイル
ビルド出力を配信し、ナビゲーションのリンクを順にクリックしたときに読み込まれた JavaScript ファイルの記録です。
最初の表示(/) → chunk-DB4Yqjat.js, main-7IZWTCQU.js
「商品一覧」をクリック → chunk-9OqahxDz.js
「問い合わせ」をクリック → chunk-DwSFzM4L.js, chunk-CaMogLIE.js
「レポート」をクリック → chunk-CLtViVLR.js
「管理」をクリック → chunk-C7JWlwtp.js
リンクをクリックした時点で、その画面のチャンクだけが読み込まれています。 「問い合わせ」では画面本体と一緒に共有チャンク(forms)も読み込まれ、あとで「管理」を開いたときには forms は読み込み済みなので、admin-routes だけで済んでいます。
loadComponent・loadChildren・default export の書き方
loadComponent:1画面を遅延ロードする
{
path: 'products',
loadComponent: () => import('./pages/products').then((m) => m.Products),
},
読み込まれる側は、ふつうのスタンドアロンコンポーネントです。遅延ロードのための特別な記述はありません。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-products',
template: `
<h1>商品一覧</h1>
<ul>
@for (p of products(); track p.id) {
<li>{{ p.id }} {{ p.name }}({{ p.price }}円)</li>
}
</ul>
`,
})
export class Products {
protected readonly products = signal([
{ id: 1, name: 'ノート', price: 180 },
{ id: 2, name: 'ペン', price: 120 },
{ id: 3, name: '付箋', price: 250 },
]);
}
default export:then を省略する
コンポーネントを export default にしておくと、.then((m) => m.Xxx) を書かずに済みます。
import { Component } from '@angular/core';
import { DatePipe, DecimalPipe } from '@angular/common';
@Component({
selector: 'app-report',
imports: [DatePipe, DecimalPipe],
template: `
<h1>レポート</h1>
<p>{{ today | date: 'yyyy-MM-dd' }} 時点の売上: {{ total | number }} 円</p>
`,
})
export default class Report {
protected readonly today = new Date();
protected readonly total = 1234567;
}
{
path: 'report',
loadComponent: () => import('./pages/report'),
},
default export ではないファイルに省略記法を使うと、ビルドで TS2322 が出ます。 products.ts(名前付き export)に対して試した結果です。
✘ [ERROR] TS2322: Type '() => Promise<typeof import("./pages/products")>' is not assignable to type '() => Type<unknown> | Observable<Type<unknown> | DefaultExport<Type<unknown>>> | Promise<Type<unknown> | DefaultExport<Type<unknown>>>'.
src/app/app.routes.ts:9:4:
9 │ loadComponent: () => import('./pages/products'),
╵ ~~~~~~~~~~~~~
型に DefaultExport と出ているとおり、loadComponent が受け取れるのは「コンポーネントそのもの」か「default にコンポーネントを持つモジュール」です。このエラーが出たら、.then((m) => m.Products) を付けるか、コンポーネントを export default に変えます。
loadChildren:ルート配列ごと遅延ロードする
機能ごとにルート配列を別ファイルに分け、親のルートから loadChildren で読み込みます。
import { Routes } from '@angular/router';
import { AdminShell } from './admin-shell';
import { AdminUsers } from './admin-users';
import { AdminSettings } from './admin-settings';
export const ADMIN_ROUTES: Routes = [
{
path: '',
component: AdminShell,
children: [
{ path: '', pathMatch: 'full', redirectTo: 'users' },
{ path: 'users', component: AdminUsers },
{ path: 'settings', component: AdminSettings },
],
},
];
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
admin.routes.ts の中で静的 import したコンポーネントは、同じチャンク(admin-routes)にまとまります。 上のビルド出力で admin-routes が 1.83 kB だったのは、シェル・ユーザー・設定の3画面が入っているためです。/admin の下に画面が増えても、親の app.routes.ts は1行のまま変わりません。
@angular/router 22.2.1 の型定義では、loadChildren に渡す関数の型は次のとおりです。ルート配列(Routes)のほか、default export された Routes も受け取れることが読み取れます。
type LoadChildrenCallback = () => Type<any> | NgModuleFactory<any> | Routes | Observable<Type<any> | Routes | DefaultExport<Type<any>> | DefaultExport<Routes>> | Promise<NgModuleFactory<any> | Type<any> | Routes | DefaultExport<Type<any>> | DefaultExport<Routes>>;
遅延ロードの中でさらに遅延ロードする
loadChildren で読み込むルート配列の中にも loadComponent を書けます。設定画面だけを切り出した例です。
import { Routes } from '@angular/router';
import { AdminShell } from './admin-shell';
import { AdminUsers } from './admin-users';
export const ADMIN_ROUTES: Routes = [
{
path: '',
component: AdminShell,
children: [
{ path: '', pathMatch: 'full', redirectTo: 'users' },
{ path: 'users', component: AdminUsers },
{
path: 'settings',
loadComponent: () => import('./admin-settings').then((m) => m.AdminSettings),
},
],
},
];
ビルドすると admin-settings という lazy chunk が増え、admin-routes は 1.83 kB から 1.21 kB になりました(全体の出力は次の @defer の節に載せています)。
@defer で画面の一部だけを遅延ロードする
ルート単位ではなく、画面の中の一部分だけを後から読み込みたいときは @defer を使います。ホーム画面の「お知らせ」を、ボタンを押したときに読み込む例です。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-news',
template: `
<ul>
@for (n of news(); track n) {
<li>{{ n }}</li>
}
</ul>
`,
})
export class News {
protected readonly news = signal(['新商品を追加しました', '送料を見直しました']);
}
import { Component } from '@angular/core';
import { News } from './news';
@Component({
selector: 'app-home',
imports: [News],
template: `
<h1>ホーム</h1>
<p>最初に表示されるページです。</p>
<button type="button" id="show-news" #newsButton>お知らせを表示</button>
@defer (on interaction(newsButton)) {
<app-news />
} @placeholder {
<p>ボタンを押すとお知らせを読み込みます。</p>
}
`,
})
export class Home {}
imports に News を書いていても、テンプレートで @defer の中だけで使っていれば別チャンクになります。この状態(設定画面の loadComponent 化と、次の節の独自プリロード戦略も入れた最終形)のビルド出力です。
Initial chunk files | Names | Raw size | Estimated transfer size
chunk-wLmurlds.js | - | 148.18 kB | 44.40 kB
main-PDFDVH5F.js | main | 110.85 kB | 29.00 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes
| Initial total | 259.03 kB | 73.40 kB
Lazy chunk files | Names | Raw size | Estimated transfer size
chunk-BLvZj0v5.js | - | 42.59 kB | 9.35 kB
chunk-C4d-xvlH.js | contact | 1.36 kB | 714 bytes
chunk-C4q_P2sR.js | admin-routes | 1.21 kB | 660 bytes
chunk-CqvnC51y.js | admin-settings | 810 bytes | 810 bytes
chunk-M2jIUNSG.js | products | 666 bytes | 666 bytes
chunk-Bu0Vfluo.js | report | 584 bytes | 584 bytes
chunk-9zucUgqg.js | news | 532 bytes | 532 bytes
news が lazy chunk として分かれました。一方で Initial total は、@defer と設定画面の切り出しを入れる前の 248.95 kB から 259.03 kB へ、約10 kB 増えています。 532 bytes の部品を分けるために initial 側が増えたので、この例では差し引きで大きくなりました。@defer は、グラフやエディタのように部品そのものが大きいときに使うと効果が出ます。
| 分けたいもの | 選ぶ方法 | 理由 |
|---|---|---|
| URL を持つ1画面 | loadComponent | ルート定義だけで完結する |
| URL を持つ画面のまとまり | loadChildren | 親のルート定義が1行で済み、機能ごとにファイルを分けられる |
| 画面内の大きな部品 | @defer | ルートを変えずに、表示や操作をきっかけに読み込める |
| 数百バイトの小さな部品 | 分けない | 仕組みのためのコードのほうが大きくなることがある |
プリロードで「最初は軽く、移動は速く」を両立する
遅延ロードだけだと、リンクをクリックしてからチャンクを取りに行くぶん、初回の画面切り替えに通信の待ち時間が入ります。プリロードは、最初の表示が終わったあとに、遅延ルートのチャンクを裏で先に読み込んでおく仕組みです。
withPreloading(PreloadAllModules):すべて先読みする
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { PreloadAllModules, provideRouter, withPreloading } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withPreloading(PreloadAllModules)),
],
};
最初の表示(/) → chunk-y_rAGrho.js, main-W43WW37B.js, chunk-C3fbx05s.js, chunk-3ky63ZMT.js, chunk-gWqEjO5G.js, chunk-CWWrAVMQ.js, chunk-CM9K9Kbz.js
「商品一覧」をクリック → (読み込みなし)
「問い合わせ」をクリック → (読み込みなし)
「レポート」をクリック → (読み込みなし)
「管理」をクリック → (読み込みなし)
最初の表示のあと、initial の2ファイルに続いて lazy chunk が5つとも読み込まれ、クリック時の読み込みはゼロになりました。このときのビルド出力は Initial total 248.85 kB で、プリロードなしの 246.96 kB から 1.89 kB 増えています(プリロードの仕組みのぶん)。lazy chunk の構成は変わりません。
独自のプリロード戦略:印を付けたルートだけ先読みする
画面数が多いアプリですべてを先読みすると、使われない画面のぶんまで通信します。よく使う画面だけ先読みするには、PreloadingStrategy を実装したクラスを作ります。
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { EMPTY, Observable } from 'rxjs';
// data: { preload: true } を付けたルートだけ先読みする
@Injectable({ providedIn: 'root' })
export class SelectivePreload implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] === true ? load() : EMPTY;
}
}
preload() の第2引数の関数を呼ぶと読み込みが始まり、EMPTY(何も流さない Observable)を返すと読み込みません。ルート側には data: { preload: true } を付けます。
import { Routes } from '@angular/router';
import { Home } from './pages/home';
export const routes: Routes = [
{ path: '', component: Home },
{
path: 'products',
data: { preload: true },
loadComponent: () => import('./pages/products').then((m) => m.Products),
},
{
path: 'contact',
loadComponent: () => import('./pages/contact').then((m) => m.Contact),
},
{
// default export のコンポーネントは .then() を省略できる
path: 'report',
loadComponent: () => import('./pages/report'),
},
{
// ルート配列を返す。配下の画面がまとめて別ファイルになる
path: 'admin',
data: { preload: true },
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router';
import { routes } from './app.routes';
import { SelectivePreload } from './selective-preload';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withPreloading(SelectivePreload)),
],
};
商品一覧と管理にだけ印を付けたときの記録です(設定画面を loadComponent にする前の構成で記録)。
最初の表示(/) → chunk-y_rAGrho.js, main-BGLZJODN.js, chunk-C3fbx05s.js, chunk-CJnufdU8.js, chunk-Xax2clak.js
「商品一覧」をクリック → (読み込みなし)
「問い合わせ」をクリック → chunk-BOiCTJUD.js
「レポート」をクリック → chunk-DLhuS7JY.js
「管理」をクリック → (読み込みなし)
印を付けた products と admin-routes だけが先読みされ、contact と report はクリック時に読み込まれました。 3つ目の chunk-Xax2clak.js は forms の共有チャンクです。この時点の admin-routes は設定画面(FormsModule を使う)を静的 import していたため、admin-routes を先読みすると forms も一緒に読み込まれます。
設定画面を loadComponent に変えた最終形では、先読みされるのは products と admin-routes の2つだけになりました。
最初の表示(/) → chunk-wLmurlds.js, main-PDFDVH5F.js, chunk-M2jIUNSG.js, chunk-C4q_P2sR.js
「商品一覧」をクリック → (読み込みなし)
「問い合わせ」をクリック → chunk-C4d-xvlH.js, chunk-BLvZj0v5.js
「レポート」をクリック → chunk-Bu0Vfluo.js
「管理」をクリック → (読み込みなし)
「お知らせを表示」をクリック → chunk-9zucUgqg.js
「設定」をクリック → chunk-CqvnC51y.js
| プリロードの設定 | 最初の表示で読み込む lazy chunk | クリック時の読み込み | 向いている場面 |
|---|---|---|---|
| なし(既定) | 0 個 | 毎回あり | 画面ごとの利用がまばらなとき |
PreloadAllModules |
5 個すべて | なし | 画面数が少なく、どれもよく使うとき |
独自戦略(data.preload) |
印を付けた分だけ | 印のない画面だけ | よく使う画面が決まっているとき |
以前の文字列構文と、書き換え例
Angular 7 のころの記事では、loadChildren に「モジュールのパス+#+クラス名」の文字列を渡す書き方が紹介されています。この形を Angular 22.2.1 のルートに書いてビルドすると、次のエラーで止まります。
{
path: 'admin',
loadChildren: './admin/admin.routes#ADMIN_ROUTES',
},
✘ [ERROR] TS2322: Type 'string' is not assignable to type 'LoadChildrenCallback'. [plugin angular-compiler]
src/app/app.routes.ts:24:4:
24 │ loadChildren: './admin/admin.routes#ADMIN_ROUTES',
╵ ~~~~~~~~~~~~
The expected type comes from property 'loadChildren' which is declared here on type 'Route'
node_modules/@angular/router/types/_router_module-chunk.d.ts:2705:4:
2705 │ loadChildren?: LoadChildren;
╵ ~~~~~~~~~~~~
loadChildren は関数(LoadChildrenCallback)だけを受け取るので、文字列は型の段階で弾かれます。書き換えは、# の前を import() のパスに、# の後ろを .then() の中に移すだけです。
| 以前の書き方 | いまの書き方 |
|---|---|
loadChildren: './lazy/lazy.module#LazyModule' |
loadChildren: () => import('./lazy/lazy.module').then((m) => m.LazyModule) |
モジュール内の RouterModule.forChild(routes) |
ルート配列を export して loadChildren: () => import('./lazy/lazy.routes').then((m) => m.LAZY_ROUTES) |
| 1画面だけのモジュール | loadComponent: () => import('./lazy/lazy').then((m) => m.Lazy) |
1行目は NgModule を残したまま移すときの形、2行目はルート配列に置き換える形です。どちらも Angular 22.2.1 でビルドし、ブラウザで表示されることを確かめました。
import { Component, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@Component({ selector: 'app-legacy-b', template: `<h1>NgModule の遅延ロード</h1>` })
class LegacyB {}
const routes: Routes = [{ path: '', component: LegacyB }];
@NgModule({ imports: [RouterModule.forChild(routes), LegacyB] })
export class LegacyModule {}
import { Component } from '@angular/core';
import { Routes } from '@angular/router';
@Component({ selector: 'app-legacy-a', template: `<h1>default export のルート配列</h1>` })
class LegacyA {}
export default [{ path: '', component: LegacyA }] satisfies Routes;
{ path: 'legacy-a', loadChildren: () => import('./legacy/legacy.routes') },
{
path: 'legacy-b',
loadChildren: () => import('./legacy/legacy.module').then((m) => m.LegacyModule),
},
chunk-BUHvA5Ul.js | legacy-module | 528 bytes | 528 bytes
chunk-9GW8Qf7o.js | legacy-routes | 344 bytes | 344 bytes
/legacy-a → default export のルート配列
/legacy-b → NgModule の遅延ロード
legacy.routes.ts はルート配列を export default にしているので、loadChildren 側の .then() を省略できています。なお、この2ルートを足したビルドでは main が 110.85 kB から 120.33 kB に増えました。移行の途中では、ビルド出力の Initial total も合わせて見ておくと変化に気づけます。
遅延ロードが効かないときの確認ポイント
同じコンポーネントをどこかで静的 import している
loadComponent で指定したコンポーネントを、別の場所で静的 import していると、中身は initial 側に入ります。app.routes.ts に次の2行を足してビルドした結果です。
import { Products } from './pages/products';
// ...
{ path: 'featured', component: Products },
main-4A5EVV4G.js | main | 111.37 kB | 29.22 kB
...
chunk-Dvyhe7WU.js | products | 89 bytes | 89 bytes
products のチャンクは 666 bytes から 89 bytes に減り、main が 110.85 kB から 111.37 kB に増えました。 チャンク自体は残るものの中身は main に移っており、エラーも警告も出ません。ビルド出力の Lazy chunk のサイズが極端に小さいときは、静的 import が残っていないかを確かめます。
チェックリスト
| 確認すること | 見る場所 |
|---|---|
ng build の出力に「Lazy chunk files」が出ているか |
ビルドのログ |
| 遅延させた画面の名前が Names 列にあるか | ビルドのログ |
| Lazy chunk のサイズが極端に小さくないか(数十バイトなら中身は initial 側) | ビルドのログ |
| 遅延ルートのコンポーネントを、ほかのファイルが静的 import していないか | app.routes.ts、共通コンポーネントの imports |
| 大きなライブラリを使う画面が initial 側に残っていないか | Initial chunk のサイズ |
| プリロードで読み込みすぎていないか | ブラウザの開発者ツールのネットワーク欄 |
よくある質問
Q. loadComponent と loadChildren はどちらを使えばよいですか? A. 単独の画面なら loadComponent、配下に子ルートを持つ機能なら loadChildren です。loadChildren で読み込んだルート配列の中に loadComponent を書くこともでき、今回のサンプルでは設定画面だけをさらに別チャンクにしました。
Q. すべてのルートを遅延ロードにしたほうがよいですか?
A. 最初に開かれる画面(今回のホーム)は initial に置いたままで問題ありません。今回の結果では、画面そのものは1 kB 前後で、差を生んだのは @angular/forms のような「特定の画面だけが使うライブラリ」でした。そうしたライブラリを使う画面から遅延ロードにすると効果が出やすくなります。
Q. プリロードを入れると、初期表示は遅くなりませんか?
A. 今回の記録では、プリロードの読み込みは initial の2ファイルのあとに行われていました。Initial total の増加は PreloadAllModules で 1.89 kB でした。
Q. Names 列が - のチャンクは何ですか?
A. 複数のチャンクから使われるコードを、ビルドツールが自動で切り出した共有チャンクです。今回は Angular 本体などの initial 側の共有チャンク(138.47 kB)と、forms の lazy 側の共有チャンク(42.60 kB)の2つができました。
まとめ
- 1画面は
loadComponent、画面のまとまりはloadChildren(ルート配列を返す)、画面内の部品は@defer export defaultにすると.then()を省略できる。default でないファイルに省略記法を使うと TS2322 になる- 5画面のサンプルで Initial total は 290.77 kB → 246.96 kB(約15%減)。減った分のほとんどは
@angular/forms - 複数の遅延ルートが使うライブラリは、共有チャンクに自動でまとまる
withPreloading(PreloadAllModules)でクリック時の読み込みがゼロに。独自戦略ならdata: { preload: true }のルートだけ先読みできる- 文字列指定の
loadChildrenは TS2322 になる。() => import('...').then((m) => m.Xxx)に書き換える - 遅延させた画面を別の場所で静的 import すると、中身は initial 側に入る。ビルド出力の Lazy chunk のサイズで確かめる
参考・出典
@angular/router22.2.1 の型定義(node_modules/@angular/router/types/内のLoadChildrenCallback・Route.loadComponent・PreloadingStrategy)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular遅延ロード|loadComponentとloadChildren」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。