Angular Standalone Components: Міграція та найкращі практики у 2026 році
Повний посібник з міграції Angular-додатків з NgModules на standalone-компоненти. Охоплює офіційний 3-кроковий процес CLI, lazy loading, маршрутизацію та найкращі практики для Angular 21.

Standalone-компоненти Angular усувають потребу в NgModules, зменшуючи обсяг шаблонного коду та відкриваючи можливості для детального lazy loading по всьому додатку. Відтоді як Angular 19 зробив standalone режимом за замовчуванням, а Angular 21 закріпив безвідповідальне виявлення змін без зон, міграція застарілих модульних кодових баз стала водночас простою та високоефективною.
Офіційна схема Angular CLI автоматично обробляє більшу частину міграції за три проходи. Типовий корпоративний додаток може завершити конверсію за один спринт, а розміри бандлів зменшуються на 30-50% завдяки lazy loading на рівні компонентів.
NgModules проти standalone-компонентів: що змінилося
NgModules виконували роль контексту компіляції для компонентів починаючи з Angular 2. Кожен компонент, директива та pipe мали бути оголошені рівно в одному модулі, а спільна функціональність вимагала ретельно організованих імпортів та експортів модулів. Це створювало тісний зв'язок між непов'язаними функціями та ускладнювало tree-shaking.
Standalone-компоненти перевертають цю модель. Кожен компонент оголошує власні залежності безпосередньо в масиві imports декоратора @Component. Без реєстрації в модулі, без спільних модулів, без barrel-експортів половини додатку.
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. Бандлер бачить точно, які компоненти, pipe та директиви потрібні кожному файлу, що уможливлює точний tree-shaking.
3-кроковий процес міграції CLI
Angular надає автоматизовану схему, яка обробляє міграцію в трьох послідовних проходах. Кожен крок базується на попередньому, і проєкт повинен компілюватися чисто між кожним проходом.
Крок 1: Конвертація оголошень у standalone
Перший прохід сканує кожен компонент, директиву та pipe в проєкті, додає standalone: true і переносить необхідні імпорти з батьківського NgModule у власний масив imports кожного компонента.
# Крок 1: Конвертація всіх оголошень у standalone
ng g @angular/core:standalone --path=src/appОберіть "Convert all components, directives and pipes to standalone" при запиті. Схема використовує статичний аналіз для вирішення залежностей, тому будь-який компонент з метаданими, які неможливо проаналізувати під час збірки, буде пропущений з попередженням.
Крок 2: Видалення непотрібних NgModules
Коли всі оголошення вже standalone, багато NgModules стають порожніми оболонками. Цей прохід ідентифікує модулі, які лише реекспортували standalone-оголошення, і видаляє їх.
# Крок 2: Видалення порожніх NgModules
ng g @angular/core:standalone --path=src/appОберіть "Remove unnecessary NgModule classes". Модулі, які все ще містять провайдери, конфігурації маршрутів або імпортуються кількома іншими модулями, будуть збережені з коментарем TODO для ручного перегляду.
Крок 3: Перехід на standalone bootstrap
Останній прохід замінює кореневий NgModule на API bootstrapApplication Angular і конвертує кореневий компонент у 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 } 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 кореневого модуля. Всі функції-провайдери (provideRouter, provideHttpClient, provideAnimations) працюють безпосередньо без модульних обгорток.
Міграція маршрутизації: від модулів до loadComponent
Модулі маршрутизації потребують ручної уваги, оскільки схема не конвертує автоматично імпорти loadChildren з модулів на loadComponent або loadChildren на рівні маршрутів зі standalone-маршрутами.
Старий патерн завантажував цілі функціональні модулі:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];Standalone-еквівалент завантажує окремі компоненти або файли маршрутів безпосередньо:
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 ліниво завантажує окремий компонент. loadChildren з файлом маршрутів ліниво завантажує цілу функціональну область. Обидва варіанти створюють окремі chunk-и, які браузер отримує за запитом.
Готовий до співбесід з Angular?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Робота з SharedModules та спільними залежностями
SharedModules — універсальні модулі, що експортують широко використовувані компоненти, директиви та pipe — є найпоширенішою перешкодою під час міграції. Схема не може їх автоматично видалити, оскільки їх імпортують кілька модулів.
Рішення: конвертувати спільні оголошення в standalone по одному, а потім видалити SharedModule, коли нічого більше його не імпортує.
// До: SharedModule реекспортує все
@NgModule({
declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
imports: [CommonModule]
})
export class SharedModule {}
// Після: Кожне оголошення standalone, імпортуйте безпосередньо
// loading-spinner.component.ts
@Component({
selector: 'app-loading-spinner',
standalone: true,
template: `<div class="spinner" role="status"></div>`
})
export class LoadingSpinner {}Споживачі тепер імпортують LoadingSpinner безпосередньо замість цілого SharedModule. Бандлер включає лише ті компоненти, які потрібні кожному маршруту.
Примусове використання виключно standalone
Після міграції запобігання появі нових NgModules у кодовій базі є критично важливим. Angular надає опцію компілятора TypeScript для цього.
{
"angularCompilerOptions": {
"strictStandalone": true
}
}З увімкненим strictStandalone будь-яка спроба створити не-standalone компонент, директиву або pipe призводить до помилки компіляції. Це забезпечує дотримання нової архітектури у всій команді.
Приріст продуктивності: розмір бандлів та lazy loading
Основна перевага standalone-компонентів у продуктивності полягає в детальному lazy loading. З NgModules lazy loading працював на рівні модуля — імпорт одного компонента з модуля тягнув за собою кожне оголошення, яке цей модуль експортував. Standalone-компоненти розривають цей зв'язок.
Реальний бенчмарк на середньому корпоративному додатку (200+ компонентів) показав:
| Метрика | На базі NgModule | Standalone | Покращення | |---------|-----------------|------------|------------| | Початковий бандл | 485 КБ | 218 КБ | -55% | | Найбільший лінивий chunk | 142 КБ | 38 КБ | -73% | | Час до інтерактивності | 3,2с | 1,8с | -44% | | Час збірки (esbuild) | 12,4с | 8,1с | -35% |
Ці показники є результатом усунення накладних витрат на розв'язання модулів та надання бандлеру можливості елімінувати невикористані експорти на рівні компонентів, а не модулів.
Тестування standalone-компонентів
Юніт-тести значно спрощуються зі standalone-компонентами. Конфігурація TestBed більше не вимагає імпорту цілих модулів для задоволення залежностей компонента.
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);
});
});Компонент потрапляє безпосередньо в масив imports TestBed.configureTestingModule. Усі оголошені залежності вже розв'язані через власні imports компонента, тому додаткові імпорти модулів не потрібні.
Типові пастки міграції
Циклічні імпорти між standalone-компонентами. Коли компонент A імпортує компонент B, а B імпортує A, компілятор TypeScript видає помилку циклічної залежності. Рішення: виділити спільний інтерфейс в окремий файл або використати forwardRef() як тимчасове рішення під час рефакторингу ланцюга залежностей.
Сторонні бібліотеки, що все ще використовують NgModules. Багато бібліотек перейшли на standalone, але деякі застарілі пакети все ще експортують NgModules. Імпортуйте ці модулі безпосередньо в масив imports standalone-компонента — Angular підтримує змішування standalone та модульних імпортів.
Відсутні провайдери після видалення AppModule. Сервіси, що раніше надавалися в масиві providers кореневого модуля, повинні бути перенесені до ApplicationConfig в app.config.ts або використовувати providedIn: 'root' в декораторі @Injectable. Сервіси з областю видимості маршруту повинні використовувати масив providers у конфігураціях маршрутів.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Підсумки
- Офіційна схема Angular CLI автоматизує 80-90% міграції у трьох послідовних проходах: конвертація оголошень, видалення модулів, зміна bootstrap
- Модулі маршрутизації потребують ручної конвертації з
loadChildrenз NgModules наloadComponentабо standalone файли маршрутів - SharedModules є основною перешкодою — конвертуйте кожне спільне оголошення в standalone окремо, потім видаліть модуль
- Увімкніть
strictStandaloneв tsconfig для запобігання появі нових NgModules після міграції - Розміри бандлів зменшуються на 30-55% завдяки tree-shaking на рівні компонентів та детальному lazy loading з
loadComponent - Юніт-тести значно спрощуються — standalone-компонент імпортується безпосередньо в
TestBedбез конфігурації модулів - Беззонне виявлення змін Angular 21 та реактивність на основі сигналів природно поєднуються зі standalone-архітектурою для максимальної продуктивності
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
