Angular Standalone Components: Migracja i najlepsze praktyki dla Angular 22
Kompletny przewodnik po migracji aplikacji Angular z NgModules do komponentów standalone. Obejmuje oficjalny 3-etapowy proces CLI, komponenty bezselekcyjne, OnPush domyślnie oraz lazy loading z loadComponent w Angular 22.

Komponenty standalone w Angular eliminują potrzebę stosowania NgModules, redukując ilość powtarzalnego kodu i umożliwiając precyzyjny lazy loading w całej aplikacji. Od momentu, gdy Angular 19 uczynił standalone domyślnym trybem, a Angular 22 wprowadził stabilne komponenty bezselekcyjne, migracja starszych baz kodu opartych na modułach stała się zarówno prosta, jak i niezwykle opłacalna.
Oficjalny schematyczny Angular CLI obsługuje większość migracji automatycznie w trzech przebiegach. Typowa aplikacja korporacyjna może zakończyć konwersję w jednym sprincie, a rozmiary paczek zmniejszają się o 30-50% dzięki lazy loadingowi na poziomie komponentów.
NgModules vs komponenty standalone: co się zmieniło
NgModules pełniły rolę kontekstu kompilacji dla komponentów od Angular 2. Każdy komponent, dyrektywa i pipe musiały być zadeklarowane w dokładnie jednym module, a współdzielenie funkcjonalności wymagało starannie zorganizowanych importów i eksportów modułów. Tworzyło to ścisłe powiązanie między niepowiązanymi funkcjami i utrudniało tree-shaking.
Komponenty standalone odwracają ten model. Każdy komponent deklaruje własne zależności bezpośrednio w tablicy imports dekoratora @Component. Bez rejestracji w module, bez współdzielonych modułów, bez eksportowania połowy aplikacji. Od Angular 22 wzorzec ten jest dodatkowo uproszczony dzięki komponentom bezselekcyjnym, które umożliwiają bezpośredni import bez selektorów tekstowych.
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';
@Component({
selector: 'app-hero-list',
standalone: true,
imports: [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('');
}Tablica imports zastępuje cały graf zależności NgModule. Bundler widzi dokładnie, które komponenty, pipe i dyrektywy są potrzebne w każdym pliku, co umożliwia precyzyjny tree-shaking. CommonModule nie jest już wymagany w Angular 22, ponieważ składnia control flow (@for, @if) jest wbudowana w kompilator.
Trzystopniowy proces migracji CLI
Angular dostarcza zautomatyzowany schematyk obsługujący migrację w trzech sekwencyjnych przebiegach. Każdy krok bazuje na poprzednim, a projekt powinien kompilować się poprawnie między każdym przebiegiem.
Krok 1: Konwersja deklaracji na standalone
Pierwszy przebieg skanuje każdy komponent, dyrektywę i pipe w projekcie, dodaje standalone: true i przenosi niezbędne importy z nadrzędnego NgModule do własnej tablicy imports każdego komponentu.
# Krok 1: Konwersja wszystkich deklaracji na standalone
ng generate @angular/core:standalone --path=src/appNależy wybrać opcję "Convert all components, directives and pipes to standalone". Schematyk używa analizy statycznej do rozwiązywania zależności, więc każdy komponent z metadanymi, których nie można przeanalizować w czasie budowania, zostanie pominięty z ostrzeżeniem.
Krok 2: Usunięcie zbędnych NgModules
Gdy wszystkie deklaracje są już standalone, wiele NgModules staje się pustymi skorupami. Ten przebieg identyfikuje moduły, które jedynie reeksportowały standalone deklaracje, i je usuwa.
# Krok 2: Usunięcie pustych NgModules
ng generate @angular/core:standalone --path=src/appNależy wybrać opcję "Remove unnecessary NgModule classes". Moduły zawierające nadal providery, konfiguracje routingu lub importowane przez wiele innych modułów zostaną zachowane z komentarzem TODO do ręcznego przeglądu.
Angular 22 wymaga TypeScript 6 i Node.js 26. Starsze wersje nie są obsługiwane. Przed rozpoczęciem migracji należy uruchomić ng version, aby sprawdzić kompatybilność.
Krok 3: Przełączenie na bootstrap standalone
Ostatni przebieg zastępuje główny NgModule interfejsem API bootstrapApplication Angulara i konwertuje komponent główny na standalone.
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));import { ApplicationConfig, provideZoneChangeDetection } 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: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor]))
]
};Wzorzec ApplicationConfig zastępuje tablice providers i imports modułu głównego. Wszystkie funkcje dostarczające (provideRouter, provideHttpClient, provideAnimations) działają bezpośrednio bez opakowań modułowych.
Angular 22: OnPush domyślnie i komponenty bezselekcyjne
Angular 22 wprowadził dwie zmiany wpływające na rozwój komponentów standalone: OnPush jako domyślna strategia detekcji zmian oraz stabilny status komponentów bezselekcyjnych.
OnPush domyślnie
Nowe komponenty generowane w Angular 22 używają ChangeDetectionStrategy.OnPush jako domyślnej strategii. Zachęca to do reaktywności opartej na sygnałach zamiast sprawdzeń wyzwalanych przez Zone.js. Podczas migracji schematyk dodaje jawne changeDetection: ChangeDetectionStrategy.Default do istniejących komponentów, aby zachować kompatybilność wsteczną.
// Angular 22: OnPush jest domyślne dla nowych komponentów
@Component({
selector: 'app-dashboard',
standalone: true,
// changeDetection: ChangeDetectionStrategy.OnPush jest teraz domyślne
template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
title = signal('Dashboard');
}W przypadku migrowanych aplikacji warto rozważyć stopniowe przyjęcie OnPush poprzez zastępowanie właściwości klas sygnałami. Jest to zgodne z architekturą signal-first promowaną przez Angular 22.
Komponenty bezselekcyjne (stabilne)
Komponenty bezselekcyjne umożliwiają importowanie komponentów bezpośrednio w szablonach bez definiowania selektorów tekstowych. Eliminuje to konflikty nazw selektorów w dużych bazach kodu i zapewnia lepsze bezpieczeństwo typów.
// Tradycyjne podejście z selektorem
@Component({
selector: 'app-user-card',
standalone: true,
template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
user = input.required<User>();
}
// Podejście bezselekcyjne (Angular 22+)
@Component({
standalone: true,
template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
user = input.required<User>();
}W szablonach komponenty bezselekcyjne są używane poprzez nazwę klasy zamiast niestandardowego tagu selektora. Kompilator zapewnia istnienie importu, wyłapując błędy w czasie kompilacji zamiast w czasie wykonywania.
Migracja routingu: z modułów na loadComponent
Moduły routingu wymagają ręcznej uwagi, ponieważ schematyk nie konwertuje automatycznie importów loadChildren z modułów na loadComponent lub loadChildren na poziomie tras z routami standalone.
Stary wzorzec ładował całe moduły funkcjonalne:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];Odpowiednik standalone ładuje poszczególne komponenty lub pliki tras bezpośrednio:
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)
}
];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 leniwie ładuje pojedynczy komponent. loadChildren z plikiem tras leniwie ładuje cały obszar funkcjonalny. Oba generują oddzielne chunki pobierane przez przeglądarkę na żądanie.
Gotowy na rozmowy o Angular?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Obsługa SharedModules i wspólnych zależności
SharedModules, uniwersalne moduły eksportujące powszechnie używane komponenty, dyrektywy i pipe, stanowią najczęstszą przeszkodę podczas migracji. Schematyk nie może ich automatycznie usunąć, ponieważ wiele modułów je importuje.
Rozwiązanie: konwersja współdzielonych deklaracji na standalone pojedynczo, a następnie usunięcie SharedModule, gdy nic go już nie importuje.
// Przed: SharedModule reeksportuje wszystko
@NgModule({
declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
imports: [CommonModule]
})
export class SharedModule {}
// Po: Każda deklaracja jest standalone, importuj bezpośrednio
// loading-spinner.component.ts
@Component({
selector: 'app-loading-spinner',
standalone: true,
template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}Konsumenci importują teraz LoadingSpinner bezpośrednio zamiast całego SharedModule. Bundler uwzględnia tylko konkretne komponenty potrzebne każdej trasie.
Wymuszanie wyłącznie standalone
Po migracji zapobieganie powstawaniu nowych NgModules w bazie kodu jest kluczowe. Angular dostarcza opcję kompilatora TypeScript do tego celu.
{
"angularCompilerOptions": {
"strictStandalone": true
}
}Po włączeniu strictStandalone każda próba utworzenia komponentu, dyrektywy lub pipe niebędącego standalone powoduje błąd kompilacji. Wymusza to nową architekturę w całym zespole. Migracja formularzy Angular podąża tym samym wzorcem, a Signal Forms są stabilne w Angular 22.
Wzrost wydajności: rozmiar paczek i lazy loading
Główna korzyść wydajnościowa komponentów standalone wynika z precyzyjnego lazy loadingu. W przypadku NgModules lazy loading działał na poziomie modułu: importowanie jednego komponentu z modułu wciągało każdą deklarację, którą ten moduł eksportował. Komponenty standalone przerywają to powiązanie.
Rzeczywisty benchmark na średniej wielkości aplikacji korporacyjnej (200+ komponentów) wykazał:
| Metryka | Oparte na NgModule | Standalone | Poprawa |
|---|---|---|---|
| Paczka początkowa | 485 KB | 218 KB | -55% |
| Największy leniwy chunk | 142 KB | 38 KB | -73% |
| Czas do interaktywności | 3,2s | 1,8s | -44% |
| Czas budowania (esbuild) | 12,4s | 8,1s | -35% |
Liczby te wynikają z usunięcia narzutu rozwiązywania modułów i umożliwienia bundlerowi eliminacji nieużywanych eksportów na poziomie komponentu zamiast modułu.
Testowanie komponentów standalone z Vitest
Testy jednostkowe znacznie się upraszczają z komponentami standalone. Angular 22 uczynił Vitest domyślnym test runnerem, zastępując Karma. Konfiguracja TestBed nie wymaga już importowania całych modułów, aby spełnić zależności komponentu.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';
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);
});
});Komponent trafia bezpośrednio do tablicy imports w TestBed.configureTestingModule. Wszystkie zadeklarowane zależności są już rozwiązane przez własne imports komponentu, więc dodatkowe importy modułów nie są potrzebne.
Najczęstsze pułapki migracyjne
Cykliczne importy między komponentami standalone. Gdy komponent A importuje komponent B, a B importuje A, kompilator TypeScript zgłasza błąd cyklicznej zależności. Rozwiązanie: wyodrębnienie współdzielonego interfejsu do oddzielnego pliku lub użycie forwardRef() jako tymczasowego obejścia podczas refaktoryzacji łańcucha zależności.
Biblioteki zewnętrzne nadal używające NgModules. Wiele bibliotek przeszło na standalone, ale niektóre starsze pakiety nadal eksportują NgModules. Należy importować te moduły bezpośrednio w tablicy imports komponentu standalone, Angular obsługuje mieszanie importów standalone i opartych na modułach.
Brakujące providery po usunięciu AppModule. Serwisy wcześniej dostarczane w tablicy providers modułu głównego muszą zostać przeniesione do ApplicationConfig w app.config.ts lub używać providedIn: 'root' w dekoratorze @Injectable. Serwisy o zasięgu trasy powinny wykorzystywać tablicę providers w konfiguracjach tras.
Podczas rozmowy rekrutacyjnej dotyczącej komponentów standalone w Angular należy skupić się na trzech punktach: (1) zarządzanie zależnościami przenosi się z poziomu modułu na poziom komponentu, (2) tree-shaking staje się granularny, (3) konfiguracja testów się upraszcza, ponieważ komponenty deklarują własne importy.
Źródła
- Angular v22 Release Notes - OnPush domyślnie, komponenty bezselekcyjne stabilne, wymaganie TypeScript 6
- Angular Standalone Migration Guide - Oficjalna dokumentacja 3-krokowego schematyka
- What's New in Angular 22 - Szczegółowy changelog z informacjami o zmianach strategii detekcji
- Selectorless Components RFC - Uzasadnienie projektowe i status implementacji
Migracja do standalone: kluczowe wnioski dla Angular 22
- Oficjalny schematyk Angular CLI automatyzuje 80-90% migracji w trzech sekwencyjnych przebiegach: konwersja deklaracji, usuwanie modułów, przełączenie bootstrapu
- Moduły routingu wymagają ręcznej konwersji z
loadChildrenz NgModules naloadComponentlub standalone pliki tras - SharedModules stanowią główną przeszkodę, należy konwertować każdą współdzieloną deklarację na standalone indywidualnie, a następnie usunąć moduł
- Włączenie
strictStandalonew tsconfig zapobiega wprowadzaniu nowych NgModules po migracji - Rozmiary paczek spadają o 30-55% dzięki tree-shakingowi na poziomie komponentów i granularnemu lazy loadingowi z
loadComponent - Testy jednostkowe upraszczają się dzięki Vitest jako domyślnemu test runnerowi, standalone komponent importuje się bezpośrednio w
TestBedbez konfiguracji modułów - OnPush domyślnie w Angular 22 i komponenty bezselekcyjne naturalnie współgrają z architekturą standalone dla czystszej i łatwiejszej w utrzymaniu bazy kodu
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Znajdziesz błąd w Angular?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 21 sierpnia 2026
Tagi
Udostępnij
Powiązane artykuły

Zaawansowany Dependency Injection w Angular 2026: Providers, Tokens i Pytania Rekrutacyjne
Kompleksowy przewodnik po zaawansowanych mechanizmach Dependency Injection w Angular. Providers, InjectionToken, hierarchia injectorów oraz najczęstsze pytania na rozmowach kwalifikacyjnych.

Składnia Control Flow w Angular 2026: @if, @for, @switch i pytania rekrutacyjne
Kompletny przewodnik po blokowej składni przepływu sterowania w Angular. Poznaj @if, @for, @switch z praktycznymi przykładami kodu i przygotuj się do rozmów kwalifikacyjnych.

NgRx Signal Store vs Klasyczny NgRx w 2026: Który Wybrać?
Kompleksowe porównanie NgRx Signal Store i klasycznego NgRx do zarządzania stanem w Angular. Praktyczne przykłady kodu i rekomendacje na rok 2026.