メインコンテンツまでスキップ

パフォーマンス最適化

Angularは大規模アプリケーションでのパフォーマンス最適化手段を豊富に備えています。Change Detection戦略からバンドルサイズ管理まで、体系的にパフォーマンスを改善できます。

1. Change Detection 戦略​

Change Detection(変更検知)は、コンポーネントの状態が変わったときに DOM を更新すべきかどうかを判定する仕組みです。最適化の第一歩は、この判定の対象範囲を絞ることです。

Default 戦略​

既定の戦略では、Zone.js がブラウザの非同期イベント(クリック、タイマー、HTTP 完了など)をフックし、イベントのたびにアプリケーションのコンポーネントツリー全体を上から順にチェックします。小規模なうちは問題になりませんが、コンポーネント数やバインディング数が増えると、無関係なイベントでも全体が再評価され、コストが無視できなくなります。

OnPush 戦略​

ChangeDetectionStrategy.OnPush を指定したコンポーネントは、次のいずれかの場合にのみチェックされます。

  • @Input(input())の参照が変化した。
  • そのコンポーネント(または子)のテンプレートでイベントが発火した。
  • テンプレートで async パイプにより購読している Observable が値を発行した。
  • テンプレートで読み取っている Signal の値が変化した。
  • markForCheck() が明示的に呼ばれた。
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

@Component({
selector: 'app-user-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
`,
})
export class UserCardComponent {
user = input.required<{ name: string; email: string }>();
}

OnPush の適用方法と注意点​

最大の注意点は、オブジェクトを破壊的に変更しても検知されないことです。OnPush は参照の比較で判定するため、不変(イミュータブル)なデータの更新が前提になります。

// NG: 参照が変わらないため、OnPush の子は更新されない
this.user.name = '新しい名前';

// OK: 新しいオブジェクトを作る
this.user = { ...this.user, name: '新しい名前' };

その他の注意点は次のとおりです。

  • 配列の push ではなく、[...items, newItem] のように新しい配列を作る。
  • 状態を Signal で持てば、OnPush との相性が良く、変更の追跡も自動になる(セクション 2 を参照)。
  • 新規プロジェクトでは、最初から全コンポーネントを OnPush にする運用が推奨される。ESLint の Angular ルールや、スキャフォールドの changeDetection 設定で徹底できる。

markForCheck() の使いどころ​

OnPush のコンポーネント内で、Angular が検知できない変更(setTimeout 内での直接代入、外部ライブラリのコールバック、Signal 化されていない内部状態など)を行った場合に、ChangeDetectorRef.markForCheck() でこのコンポーネントを「次回チェック対象」にマークします。

import { ChangeDetectorRef, inject } from '@angular/core';

private cdr = inject(ChangeDetectorRef);

onExternalLibCallback(value: string): void {
this.label = value;
this.cdr.markForCheck();
}

markForCheck() は即時に検知を走らせるのではなく、祖先方向のコンポーネントをダーティとしてマークするだけです。detectChanges() は即時に実行するため、使いすぎは避けます。基本は Signal か async パイプで済ませ、markForCheck() は例外的に使います。

React との対比​

AngularReact
Default(全体チェック)親が再レンダーされると子も再レンダー(既定)
OnPushReact.memo で props の浅い比較をして再レンダーを抑制
Signal の computeduseMemo(ただし依存配列は手動、Signal は自動追跡)
不変データの更新が必要state の不変更新が必要(同じ)
markForCheck()setState による再レンダーのスケジュール

React は仮想 DOM を再計算して差分を適用する方式で、再レンダーのたびに関数コンポーネントが再実行されます。Angular はテンプレートのバインディング単位で値の変化を検査し、変化した DOM のみを更新します。どちらも「不要な再評価をどう減らすか」が課題である点は共通で、OnPush は React.memo に近い役割ですが、Angular の Signal は依存関係を自動追跡するため、useMemo や useCallback のような手動の依存管理が不要です。

2. Zoneless Angular(Zone.js の廃止)​

Zone.js は非同期 API をモンキーパッチして変更検知のきっかけを得る仕組みですが、バンドルサイズ、スタックトレースの複雑化、async/await のダウンレベル変換といったコストがありました。Zoneless は、Zone.js なしで Angular を動かすモードです。

有効化​

Angular 20 では provideZonelessChangeDetection() として提供されます(Angular 18 〜 19 では provideExperimentalZonelessChangeDetection())。現在の安定度は使用バージョンの公式ドキュメントで確認してください。

app.config.ts
import type { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection(), provideRouter(routes)],
};

あわせて、angular.json の polyfills から zone.js を削除します。

{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": []
}
}
}
}
}
}

Signals ベースの変更検知​

Zoneless では、変更検知は次のきっかけでスケジュールされます。

  • テンプレートで読み取っている Signal の更新
  • async パイプによる Observable の値の発行
  • テンプレート内のイベントリスナーの実行
  • markForCheck() の呼び出し
  • コンポーネントの作成・破棄、ComponentRef.setInput など
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>{{ count() }}</p>
<button (click)="increment()">+1</button>
`,
})
export class CounterComponent {
count = signal(0);

increment(): void {
this.count.update((v) => v + 1);
}
}

状態をすべて Signal で管理し、全コンポーネントを OnPush にしておくと、Zoneless への移行は容易です。逆に、プレーンなプロパティを setTimeout 内で書き換えるだけのコードは更新されなくなるため、Signal へ書き換える必要があります。

パフォーマンスへのインパクトとメリット​

観点効果
バンドルサイズZone.js(約 30KB 前後、非圧縮)が不要になる
起動性能パッチ処理がなくなり、初期化が軽くなる
予測可能性変更検知が起きる条件が明確で、不要なチェックが減る
デバッグスタックトレースが読みやすくなる
互換性Zone.js 前提のライブラリは動作しない場合があるため要確認

React は元から Zone のような仕組みを持たず、setState による明示的な通知で動くため、Zoneless は Angular が React に近い「明示的な更新通知」モデルへ寄ってきたものと捉えると理解しやすくなります。

3. 遅延ロードとプリロード戦略​

初期ロード時に読み込む JavaScript を減らすことは、体感速度に直結する最も効果的な施策の一つです。

ルートレベルの遅延ロード​

loadComponent はスタンドアロンコンポーネント単位、loadChildren はルート定義の集合単位で遅延ロードします。

app.routes.ts
import type { Routes } from '@angular/router';

export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component').then((m) => m.DashboardComponent),
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
},
];

ビルド時に、動的 import() の境界ごとにチャンクが分割されます。

@defer による遅延ロード(Angular 17+)​

@defer ブロックは、テンプレートの一部(コンポーネント、ディレクティブ、パイプ)を、条件を満たしたタイミングで遅延ロードします。ルートに依存せず、画面内の重い部品を分離できます。

@defer (on viewport) {
<app-heavy-chart [data]="data()" />
} @placeholder {
<div class="chart-skeleton">チャート領域</div>
} @loading (minimum 300ms) {
<app-spinner />
} @error {
<p>読み込みに失敗しました</p>
}

主なトリガーは次のとおりです。

トリガー説明
on idleブラウザがアイドルになったとき(既定)
on viewportプレースホルダーが画面内に入ったとき
on interactionクリックなどの操作があったとき
on hoverホバーしたとき
on timer(2s)指定時間が経過したとき
when 条件式条件が真になったとき
prefetch on idle表示はしないが、先にコードを取得しておく

@defer 内で使うコンポーネントは、スタンドアロンで、同じファイル内の他の箇所から直接参照されていないことが必要です。参照されていると、通常の静的インポートとなり、チャンク分割されません。

プリロード戦略​

遅延ロードしたルートを、ユーザーが遷移する前に裏で読み込んでおく設定です。

app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withPreloading(PreloadAllModules))],
};

PreloadAllModules は、初期表示後にすべての遅延ルートを先読みします。ルート数が多い場合は帯域を無駄にするため、カスタム戦略で選択的に先読みします。

import { Injectable } from '@angular/core';
import type { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class SelectivePreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] ? load() : of(null);
}
}
{
path: 'reports',
data: { preload: true },
loadChildren: () => import('./reports/reports.routes').then((m) => m.REPORT_ROUTES),
}

withPreloading(SelectivePreloadStrategy) で登録します。ネットワーク状況やユーザーのロールに応じた先読み判定も実装できます。

初期バンドルサイズの削減​

  • 管理画面など利用者が限られる機能は、必ずルート単位で分離する。
  • 重いライブラリ(チャート、エディタ、PDF 生成)は、使う場面で動的インポートまたは @defer を使う。
  • 遅延ルートで共有されるサービスは、providedIn: 'root' か、ルートの providers で提供する範囲を意識する。

Next.js との比較​

Next.js(App Router)は、ファイルシステムのルーティングに基づき、ページ単位でコード分割が自動で行われます。next/dynamic や React.lazy で明示的な分割もできます。Angular は分割をルート定義(loadComponent / loadChildren)で明示する点が異なりますが、そのぶん分割の境界を設定ファイルで一覧でき、プリロード戦略まで細かく制御できます。@defer は React の lazy + Suspense に相当しますが、ビューポートや操作といったトリガーが宣言的に書ける点が特徴です。

4. バンドルサイズの管理​

angular.json の budgets​

budgets は、ビルド成果物がサイズ閾値を超えたとき、警告またはビルドエラーにする Angular 独自の機能です。CI でバンドルの肥大化を自動検知できます。

angular.json
{
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
]
}
}
}
type対象
initial初期ロードで必要な JS/CSS の合計
anyScript個々のスクリプトファイル
anyComponentStyle個々のコンポーネントのスタイル
bundle名前付きバンドル
allScript / all全スクリプト/全体

ng build が budgets に違反するとエラーで終了するため、そのまま CI のゲートとして機能します。React / Next.js には標準で同等の機能がなく、Next.js では @next/bundle-analyzer や外部の CI ツール(bundlesize など)で補うのが一般的です。

バンドルの分析​

# stats.json を出力
ng build --stats-json

# source-map-explorer を使う場合(source map が必要)
npx source-map-explorer dist/my-app/browser/*.js

# webpack-bundle-analyzer を使う場合
npx webpack-bundle-analyzer dist/my-app/browser/stats.json

ビルダーによって出力形式が異なる場合があるため、使用バージョンのドキュメントも確認してください。Angular の esbuild ベースのビルダーでは、stats.json を esbuild Bundle Size Analyzer などの Web ツールにアップロードして調べる方法もあります。

Tree Shaking の仕組み​

Tree Shaking は、どこからも参照されないコードをバンドルから除去する最適化です。ES Modules の静的な import / export 構造を前提に、ビルド時に未使用のエクスポートを判定します。Angular では、次の点が効果を高めます。

  • AOT コンパイルでテンプレートが静的に解析され、使われないディレクティブやパイプが特定できる。
  • providedIn: 'root' のサービスは、注入されない場合にバンドルから除外される(Tree-Shakable Providers)。
  • スタンドアロンコンポーネントは、imports に列挙したものだけがバンドルに含まれる。

不要な imports の除去​

スタンドアロンコンポーネントでは、使っていない import がそのまま残りがちです。Angular の拡張診断は、テンプレートで使用していない import を警告します(unusedStandaloneImports として提供されている場合があります)。

// NG: DatePipe を imports に入れたが、テンプレートで使っていない
@Component({
standalone: true,
imports: [DatePipe, NgClass],
template: `<p>{{ name }}</p>`,
})

ほかにも、次の習慣が有効です。

  • lodash 全体ではなく lodash-es の関数単位でインポートする。
  • moment.js のようなサイズの大きいライブラリは、date-fns やネイティブの Intl に置き換える。
  • RxJS は rxjs からの名前付き import(RxJS 7 以降)で、Tree Shaking が効く。

スタンドアロンコンポーネントによるバンドルサイズ削減​

NgModule では、モジュール単位で宣言と依存がまとめられ、1 つのコンポーネントのために使わない部品まで含まれることがありました。スタンドアロンでは、コンポーネントごとに必要な依存を明示するため、含まれるコードの粒度が細かくなり、Tree Shaking の精度が上がります。

5. AOT コンパイルと Ivy​

AOT(Ahead-of-Time)コンパイル​

AOT は、アプリのビルド時にテンプレートと TypeScript を JavaScript へ変換する方式です。現在の Angular では、ng build と ng serve のどちらも既定で AOT が使われます。

  • テンプレートがビルド時に命令コードへコンパイルされるため、ブラウザ上でのコンパイルが不要になる。
  • テンプレート内の型エラーや、存在しないプロパティの参照をビルド時に検出できる(strictTemplates)。
  • コンパイラ自体をブラウザへ配信する必要がなく、バンドルが小さくなる。

JIT(Just-in-Time)との違い​

項目AOTJIT
コンパイルのタイミングビルド時ブラウザでの実行時
初期表示速いコンパイル分だけ遅い
バンドルコンパイラを含まないコンパイラを含む
エラー検出ビルド時実行時
主な用途本番・開発の既定一部のテスト環境など

Ivy レンダリングエンジン​

Ivy は、Angular 9 以降の既定のコンパイラ兼レンダリングエンジンで、旧 ViewEngine を置き換えました。主な特徴は次のとおりです。

  • 局所性(Locality): コンポーネントごとに独立してコンパイルでき、増分ビルドが速い。
  • Tree Shaking しやすい生成コード: 使われる機能だけが命令として生成される。
  • 生成コードが小さく、デバッグしやすい。
  • 動的なコンポーネント生成や遅延ロードが簡素になる。

Tree-Shakable Providers​

サービスを providedIn: 'root' で定義すると、NgModule の providers 配列にサービスを登録する代わりに、サービス側から自分の提供先を宣言できます。この方式では、どこからも inject されないサービスはバンドルから取り除かれます。

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
// どのコンポーネントからも注入されなければ、バンドルされない
}

InjectionToken でも、factory を指定することで同様の Tree-Shakable な定義ができます。

export const API_BASE = new InjectionToken<string>('API_BASE', {
providedIn: 'root',
factory: () => '/api',
});

6. テンプレートの最適化​

track(@for での使用)​

新しい制御フロー @for では、track 式が必須です。リスト更新時に、Angular が各要素を識別して DOM を再利用するために使います。

<!-- 推奨: 一意な ID で追跡する -->
@for (item of items(); track item.id) {
<app-row [item]="item" />
}

<!-- プリミティブの配列では $index も利用可能 -->
@for (tag of tags(); track $index) {
<span>{{ tag }}</span>
}

track に一意なキーを使うと、並べ替えや追加・削除の際に、既存の DOM を移動するだけで済みます。track $index やオブジェクト自体を使うと、データを再取得して新しいオブジェクトが返るたびに DOM が作り直される可能性があるため、IDが利用できる場合は ID を使ってください。旧 *ngFor の trackBy 関数に相当し、React の key と同じ役割です。

純粋パイプの活用​

パイプは既定で「純粋(pure)」で、入力の参照が変化したときだけ再計算され、結果がキャッシュされます。テンプレートの式で関数を呼ぶ代わりに、純粋パイプを使うことで再計算を抑えられます。

@Pipe({ name: 'fullName', standalone: true })
export class FullNamePipe implements PipeTransform {
transform(user: { first: string; last: string }): string {
return `${user.last} ${user.first}`;
}
}
<p>{{ user() | fullName }}</p>

pure: false の非純粋パイプは、変更検知のたびに実行されるため、特別な理由がない限り避けます。

テンプレート内での関数呼び出しを避ける​

<!-- NG: 変更検知のたびに毎回呼ばれる -->
<p>{{ calcTotal(items) }}</p>

<!-- OK: Signal の computed でキャッシュする -->
<p>{{ total() }}</p>
total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));

メソッド呼び出しは変更検知のたびに実行されます。computed なら依存する Signal が変わったときだけ再計算され、結果もキャッシュされます。React の useMemo に相当しますが、依存配列を書く必要はありません。ゲッターや、重いロジックを含むメソッドは特に注意が必要です。

cdkVirtualScrollViewport(仮想スクロール)​

数千行以上のリストでは、DOM ノード数そのものがボトルネックになります。CDK の仮想スクロールを使うと、可視範囲の要素だけを描画します。

<cdk-virtual-scroll-viewport itemSize="48" style="height: 480px">
<div *cdkVirtualFor="let item of items()">{{ item.name }}</div>
</cdk-virtual-scroll-viewport>

詳細は Angular Material & CDK を参照してください。

7. 画像とリソースの最適化​

NgOptimizedImage ディレクティブ​

画像は LCP(Largest Contentful Paint)に直結する代表的なリソースです。NgOptimizedImage は、ベストプラクティスを強制・自動化するディレクティブです。

import { NgOptimizedImage } from '@angular/common';

@Component({
standalone: true,
imports: [NgOptimizedImage],
template: `
<img ngSrc="/assets/hero.jpg" width="1200" height="600" priority />
<img ngSrc="/assets/thumb.jpg" width="200" height="200" />
`,
})
export class HeroComponent {}

src の代わりに ngSrc を使います。主な機能は次のとおりです。

  • width と height を必須にし、レイアウトシフト(CLS)を防ぐ。
  • LCP 画像に priority を付けると、fetchpriority="high" を設定し、preload リンクを自動生成する。
  • priority のない画像には、loading="lazy" を既定で付与する。
  • fill モードで親要素いっぱいに画像を表示できる。
  • 開発モードで、LCP 画像の priority 漏れや、元画像が過大などの問題を警告する。

画像ローダーとレスポンシブ画像​

画像 CDN(Cloudflare、Cloudinary、imgix など)用のローダーを提供すると、srcset を自動生成できます。

import { provideCloudflareLoader } from '@angular/common';

providers: [provideCloudflareLoader('https://example.com')];
<img ngSrc="hero.jpg" width="1200" height="600" sizes="(max-width: 768px) 100vw, 50vw" />

遅延読み込みとプリコネクト​

画面外の画像・iframe には、ネイティブの loading="lazy" 属性を使えます。NgOptimizedImage は、priority のない画像に対してこれを自動設定します。

外部ドメインから画像やフォントを読み込む場合は、index.html で事前接続やプリロードを設定します。

src/index.html
<link rel="preconnect" href="https://images.example.com" />
<link rel="preload" as="image" href="/assets/hero.jpg" />

NgOptimizedImage の開発モードの警告は、preconnect の追加を提案してくれます。

next/image との比較​

Next.js の next/image は、画像の最適化(リサイズ、WebP/AVIF への変換)をフレームワーク(または Vercel)のサーバー側で行います。NgOptimizedImage は、変換そのものは行わず、srcset の生成、priority、遅延読み込み、警告といった「正しいマークアップの強制」に集中し、実際の変換は画像 CDN に任せる設計です。そのため、画像 CDN の選定と設定が別途必要になります。

8. SSR / SSG によるパフォーマンス最適化​

@angular/ssr によるサーバーサイドレンダリング​

SSR は、サーバー側で HTML を生成して返すことで、初期表示(FCP / LCP)の高速化、SEO、SNS 向けのメタ情報の対応を実現します。

# 新規プロジェクト
ng new my-app --ssr

# 既存プロジェクトへ追加
ng add @angular/ssr

プリレンダリング(ビルド時の静的生成)​

リクエストごとにサーバーで描画するのではなく、ビルド時に HTML を生成する方式(SSG)です。更新頻度が低いページに向いており、CDN から静的ファイルとして配信できます。

angular.json
{
"build": {
"options": {
"outputMode": "static",
"ssr": { "entry": "src/server.ts" }
}
}
}

outputMode に static(全ページをプリレンダリング)または server(SSR)を指定します。動的パラメータを持つルートは、app.routes.server.ts の getPrerenderParams で対象のパラメータを列挙します。ページごとに RenderMode.Prerender、RenderMode.Server、RenderMode.Client を使い分けられます(Angular 19 以降)。

app.routes.server.ts
import { RenderMode, type ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
{ path: '', renderMode: RenderMode.Prerender },
{ path: 'products/:id', renderMode: RenderMode.Server },
{ path: 'mypage', renderMode: RenderMode.Client },
];

Hydration の仕組みと最適化​

SSR で生成された HTML をブラウザが受け取った後、Angular が既存の DOM を再利用してイベントリスナーなどを接続するのが Hydration です。DOM を破棄して作り直す従来の方式に比べ、チラつきや LCP の悪化を避けられます。

app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};
  • withEventReplay(): Hydration 完了前に発生したユーザー操作を記録し、完了後に再生する。
  • @defer と組み合わせると、hydrate on viewport などのトリガーで、ブロックごとの Hydration を遅延できる(Incremental Hydration)。
  • DOM を直接操作するコードがあると、Hydration の不一致エラーになる。回避できない部分は ngSkipHydration 属性でスキップできる。
@defer (hydrate on viewport) {
<app-comments />
}

TransferState による二重フェッチの防止​

SSR 時にサーバーで実行した HTTP リクエストを、クライアントでもう一度実行してしまうと無駄が生じます。Angular の HttpClient は、provideClientHydration() を使うと、サーバーで行ったリクエストの結果を自動で TransferState にシリアライズし、クライアントでは再取得せずに再利用します(既定で GET / HEAD)。

// 手動で TransferState を使う場合
import { makeStateKey, TransferState, inject } from '@angular/core';

const KEY = makeStateKey<Product[]>('products');
private state = inject(TransferState);

load(): Product[] | null {
return this.state.get(KEY, null);
}

HttpClient を使っているなら、通常は自動で動作するため、手動での利用が必要になる場面は限られます。

Next.js の SSR / SSG / ISR との比較​

方式Next.jsAngular
SSRServer Components / dynamic レンダリングRenderMode.Server
SSGgenerateStaticParams で静的生成RenderMode.Prerender + getPrerenderParams
ISRrevalidate による再生成組み込みなし(CDN のキャッシュや再デプロイで代替)
CSRuse clientRenderMode.Client
部分的な HydrationServer Components + SuspenseIncremental Hydration(hydrate on ...)

Next.js の Server Components は、サーバーでのみ実行されクライアントへ JS を送らないコンポーネントという概念を持ちますが、Angular にはこれに相当するものはなく、すべてのコンポーネントがクライアントで Hydration の対象になります。ISR も標準では提供されていないため、更新頻度の高い静的コンテンツは、CDN のキャッシュ戦略(stale-while-revalidate)や再ビルドで補います。

9. Web Workers​

メインスレッドで重い計算(大量データの集計、暗号処理、画像処理など)を実行すると、UI が固まります。Web Worker にオフロードすることで、メインスレッドの負荷を軽減できます。

ng generate web-worker​

ng generate web-worker heavy-calc

実行すると、Worker のファイルが生成され、tsconfig.worker.json が構成されます。

src/app/heavy-calc.worker.ts
/// <reference lib="webworker" />

addEventListener('message', ({ data }) => {
const result = fibonacci(data as number);
postMessage(result);
});

function fibonacci(n: number): number {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
calc.component.ts
import { Component, signal } from '@angular/core';

@Component({
selector: 'app-calc',
standalone: true,
template: `
<button (click)="run()">計算</button>
<p>結果: {{ result() }}</p>
`,
})
export class CalcComponent {
result = signal<number | null>(null);

run(): void {
if (typeof Worker === 'undefined') {
return; // SSR など Worker が使えない環境
}
const worker = new Worker(new URL('./heavy-calc.worker', import.meta.url), {
type: 'module',
});
worker.onmessage = ({ data }) => {
this.result.set(data);
worker.terminate();
};
worker.postMessage(40);
}
}

使いどころと注意点​

  • Worker から DOM やコンポーネントには直接アクセスできない。通信は postMessage によるデータのコピー(構造化複製)になるため、大きなデータには Transferable を使う。
  • Worker の起動にもコストがあるため、短い処理には向かない。
  • SSR 環境では Worker が存在しないため、ブラウザ判定が必要。
  • RxJS で Worker を扱いたい場合は、fromEvent でメッセージを Observable 化する方法もある。

React でも同様に Web Worker を使えますが、Angular では CLI が Worker 用のビルド構成まで自動で整えてくれます。

パフォーマンス改善の進め方​

最適化は、計測してから行います。Chrome DevTools の Performance と Lighthouse、Angular DevTools のプロファイラーで、ボトルネックを特定したうえで、効果の大きい順に次の施策を適用します。

  1. 遅延ロードで初期バンドルを削減する
  2. OnPush + Signal(可能なら Zoneless)で変更検知を最小化する
  3. 画像の最適化(NgOptimizedImage)
  4. SSR / プリレンダリングによる初期表示の高速化
  5. 重い処理の Worker へのオフロード、仮想スクロールの導入