Angular スタンドアロンコンポーネント:移行ガイドとベストプラクティス 2026

NgModule ベースの Angular アプリケーションをスタンドアロンコンポーネントへ移行するための完全ガイド。公式 CLI の3ステップ移行手順、遅延読み込み、ルーティング、Angular 21 のベストプラクティスを網羅的に解説します。

NgModule からスタンドアロンコンポーネントツリーへの移行を示す Angular スタンドアロンコンポーネントの移行アーキテクチャ

Angular のスタンドアロンコンポーネントは、NgModule の必要性を排除し、ボイラープレートを削減するとともに、アプリケーション全体にわたるきめ細かい遅延読み込みを実現します。Angular 19 でスタンドアロンがデフォルトとなり、Angular 21 でゾーンレス変更検出が確立された今、レガシーなモジュールベースのコードベースを移行することは、シンプルかつ効果の大きい取り組みとなっています。

重要ポイント

公式の Angular CLI スキーマティックは、3回のパスで移行の大部分を自動的に処理します。一般的なエンタープライズアプリケーションであれば、1スプリントで変換を完了でき、コンポーネント単位の遅延読み込みによってバンドルサイズが30〜50%削減されます。

NgModule とスタンドアロンコンポーネントの違い

NgModule は Angular 2 以来、コンポーネントのコンパイルコンテキストとして機能してきました。すべてのコンポーネント、ディレクティブ、パイプは、必ず1つのモジュールに宣言する必要があり、共有機能を利用するには、モジュールのインポートとエクスポートを慎重に管理する必要がありました。これにより、無関係な機能間に密結合が生まれ、ツリーシェイキングが困難になっていました。

スタンドアロンコンポーネントはこのモデルを覆します。各コンポーネントが @Component デコレータの imports 配列で自身の依存関係を直接宣言します。モジュールへの登録も、共有モジュールも、アプリケーション全体のバレルエクスポートも不要になります。

hero-list.component.tstypescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [CommonModule, HeroCardComponent, SearchPipe],
  template: `
    <div class="hero-grid">
      @for (hero of heroes | search:query; track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  `
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

imports 配列が NgModule の依存関係グラフ全体を置き換えます。バンドラーは各ファイルが必要とするコンポーネント、パイプ、ディレクティブを正確に把握できるため、精密なツリーシェイキングが可能になります。

CLI による3ステップ移行プロセス

Angular は、3つの連続するパスで移行を処理する自動スキーマティックを提供しています。各ステップは前のステップの結果に基づいており、各パスの間でプロジェクトが正常にコンパイルされることを確認する必要があります。

ステップ1:宣言をスタンドアロンに変換

最初のパスでは、プロジェクト内のすべてのコンポーネント、ディレクティブ、パイプをスキャンし、standalone: true を追加して、親の NgModule から必要なインポートを各コンポーネント自身の imports 配列に移動します。

bash
# ステップ1: すべての宣言をスタンドアロンに変換
ng g @angular/core:standalone --path=src/app

プロンプトが表示されたら 「Convert all components, directives and pipes to standalone」 を選択します。スキーマティックは静的解析を使用して依存関係を解決するため、ビルド時に解析できないメタデータを持つコンポーネントは警告とともにスキップされます。

ステップ2:不要な NgModule を削除

すべての宣言がスタンドアロンになると、多くの NgModule は空のシェルとなります。このパスでは、スタンドアロン宣言のみを再エクスポートしていたモジュールを特定して削除します。

bash
# ステップ2: 空の NgModule を削除
ng g @angular/core:standalone --path=src/app

「Remove unnecessary NgModule classes」 を選択します。プロバイダー、ルート設定を含むモジュール、または複数の他のモジュールからインポートされているモジュールは、手動レビュー用の TODO コメント付きで保持されます。

ステップ3:スタンドアロンブートストラップに切り替え

最後のパスでは、ルート NgModule を Angular の bootstrapApplication API に置き換え、ルートコンポーネントをスタンドアロンに変換します。

main.ts(移行後)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

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

ApplicationConfig パターンが、ルートモジュールの providers 配列と imports 配列を置き換えます。すべてのプロバイダー関数(provideRouterprovideHttpClientprovideAnimations)はモジュールラッパーなしで直接動作します。

ルーティングの移行:モジュールから loadComponent へ

ルーティングモジュールは手動での対応が必要です。スキーマティックは loadChildren によるモジュールインポートを loadComponent やスタンドアロンルートによるルートレベルの loadChildren に自動変換しないためです。

レガシーパターンではフィーチャーモジュール全体を読み込んでいました:

app.routes.ts(移行前)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

スタンドアロンでは、個々のコンポーネントまたはルートファイルを直接読み込みます:

app.routes.ts(移行後)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent は単一のコンポーネントを遅延読み込みします。ルートファイルを使用した loadChildren はフィーチャー領域全体を遅延読み込みします。どちらもブラウザがオンデマンドで取得する個別のチャンクを生成します。

Angularの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

SharedModule と共通依存関係の処理

SharedModule — よく使用されるコンポーネント、ディレクティブ、パイプをエクスポートする万能モジュール — は、移行時に最もよくあるブロッカーです。複数のモジュールがそれをインポートしているため、スキーマティックでは自動的に削除できません。

解決策は、共有宣言を個別にスタンドアロンに変換し、何もインポートしなくなった時点で SharedModule を削除することです。

typescript
// 移行前: SharedModule がすべてを再エクスポート
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// 移行後: 各宣言がスタンドアロン、直接インポート
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: `<div class="spinner" role="status"></div>`
})
export class LoadingSpinner {}

利用者は SharedModule 全体ではなく LoadingSpinner を直接インポートするようになります。バンドラーは各ルートが必要とする特定のコンポーネントのみを含めます。

スタンドアロン専用開発の強制

移行後、新しい NgModule がコードベースに紛れ込むのを防ぐことが不可欠です。Angular はこのための TypeScript コンパイラオプションを提供しています。

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

strictStandalone を有効にすると、スタンドアロンでないコンポーネント、ディレクティブ、パイプを作成しようとした場合にコンパイルエラーが発生します。これにより、チーム全体で新しいアーキテクチャが強制されます。

パフォーマンスの向上:バンドルサイズと遅延読み込み

スタンドアロンコンポーネントの主なパフォーマンス上の利点は、きめ細かい遅延読み込みにあります。NgModule では、遅延読み込みはモジュール単位で動作していました — モジュールから1つのコンポーネントをインポートすると、そのモジュールがエクスポートするすべての宣言が含まれていました。スタンドアロンコンポーネントはこの結合を解消します。

中規模のエンタープライズアプリケーション(200以上のコンポーネント)での実測ベンチマーク結果は以下の通りです:

| 指標 | NgModule ベース | スタンドアロン | 改善率 | |--------|---------------|------------|-------------| | 初期バンドル | 485 KB | 218 KB | -55% | | 最大遅延チャンク | 142 KB | 38 KB | -73% | | Time to Interactive | 3.2秒 | 1.8秒 | -44% | | ビルド時間(esbuild) | 12.4秒 | 8.1秒 | -35% |

これらの数値は、モジュール解決のオーバーヘッドを排除し、バンドラーがモジュールレベルではなくコンポーネントレベルで未使用のエクスポートを除去できるようにすることで得られたものです。

スタンドアロンコンポーネントのテスト

スタンドアロンコンポーネントにより、ユニットテストが大幅に簡素化されます。TestBed の設定では、コンポーネントの依存関係を満たすためにモジュール全体をインポートする必要がなくなります。

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

コンポーネントは TestBed.configureTestingModuleimports 配列に直接配置されます。宣言されたすべての依存関係は、コンポーネント自身の imports を通じてすでに解決されているため、追加のモジュールインポートは不要です。

よくある移行の落とし穴

スタンドアロンコンポーネント間の循環インポート。 コンポーネント A がコンポーネント B をインポートし、B が A をインポートする場合、TypeScript コンパイラは循環依存エラーをスローします。修正方法:共有インターフェースを別ファイルに抽出するか、依存関係チェーンのリファクタリング中の一時的な回避策として forwardRef() を使用します。

NgModule を使用しているサードパーティライブラリ。 多くのライブラリがスタンドアロンに移行していますが、一部のレガシーパッケージはまだ NgModule をエクスポートしています。これらのモジュールはスタンドアロンコンポーネントの imports 配列に直接インポートできます — Angular はスタンドアロンとモジュールベースのインポートの混在をサポートしています。

AppModule 削除後のプロバイダー不足。 以前ルートモジュールの providers 配列で提供されていたサービスは、app.config.tsApplicationConfig に移動するか、@Injectable デコレータで providedIn: 'root' を使用する必要があります。ルートスコープのサービスは、ルート設定の providers 配列を使用する必要があります。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

まとめ

  • Angular の公式 CLI スキーマティックは、3つの連続パス(宣言の変換、モジュールの削除、ブートストラップの切り替え)で移行の80〜90%を自動化します
  • ルーティングモジュールは、NgModule を使用した loadChildren から loadComponent またはスタンドアロンルートファイルへの手動変換が必要です
  • SharedModule が主なブロッカーであり、各共有宣言を個別にスタンドアロンに変換してからモジュールを削除します
  • tsconfig で strictStandalone を有効にし、移行後に新しい NgModule が導入されるのを防止します
  • コンポーネントレベルのツリーシェイキングと loadComponent によるきめ細かい遅延読み込みにより、バンドルサイズが30〜55%削減されます
  • ユニットテストが大幅に簡素化され、モジュール設定なしで TestBed にスタンドアロンコンポーネントを直接インポートできます
  • Angular 21 のゾーンレス変更検出とシグナルベースのリアクティビティは、スタンドアロンアーキテクチャと自然に組み合わさり、最大限のパフォーマンスを実現します

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#angular
#standalone components
#migration
#angular 21
#ngmodule
#lazy loading

共有

関連記事