# Angular Standalone Components: Міграція та найкращі практики у 2026 році
> Повний посібник з міграції Angular-додатків з NgModules на standalone-компоненти. Охоплює офіційний 3-кроковий процес CLI, lazy loading, маршрутизацію та найкращі практики для Angular 21.
- Published: 2026-04-21
- Updated: 2026-04-21
- Author: SharpSkill
- Tags: angular, standalone components, міграція, angular 21, ngmodule, lazy loading
- Reading time: 9 min
---
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-експортів половини додатку.
```typescript
// hero-list.component.ts
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: `
@for (hero of heroes | search:query; track hero.id) {
}
`
})
export class HeroListComponent {
heroes = signal([]);
query = signal('');
}
```
Масив `imports` замінює весь граф залежностей NgModule. Бандлер бачить точно, які компоненти, pipe та директиви потрібні кожному файлу, що уможливлює точний tree-shaking.
## 3-кроковий процес міграції CLI
Angular надає автоматизовану схему, яка обробляє міграцію в трьох послідовних проходах. Кожен крок базується на попередньому, і проєкт повинен компілюватися чисто між кожним проходом.
### Крок 1: Конвертація оголошень у standalone
Перший прохід сканує кожен компонент, директиву та pipe в проєкті, додає `standalone: true` і переносить необхідні імпорти з батьківського NgModule у власний масив `imports` кожного компонента.
```bash
# Крок 1: Конвертація всіх оголошень у standalone
ng g @angular/core:standalone --path=src/app
```
Оберіть **"Convert all components, directives and pipes to standalone"** при запиті. Схема використовує статичний аналіз для вирішення залежностей, тому будь-який компонент з метаданими, які неможливо проаналізувати під час збірки, буде пропущений з попередженням.
### Крок 2: Видалення непотрібних NgModules
Коли всі оголошення вже standalone, багато NgModules стають порожніми оболонками. Цей прохід ідентифікує модулі, які лише реекспортували standalone-оголошення, і видаляє їх.
```bash
# Крок 2: Видалення порожніх NgModules
ng g @angular/core:standalone --path=src/app
```
Оберіть **"Remove unnecessary NgModule classes"**. Модулі, які все ще містять провайдери, конфігурації маршрутів або імпортуються кількома іншими модулями, будуть збережені з коментарем TODO для ручного перегляду.
### Крок 3: Перехід на standalone bootstrap
Останній прохід замінює кореневий NgModule на API `bootstrapApplication` Angular і конвертує кореневий компонент у standalone.
```typescript
// main.ts (після міграції)
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));
```
```typescript
// app.config.ts
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-маршрутами.
Старий патерн завантажував цілі функціональні модулі:
```typescript
// app.routes.ts (до)
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];
```
Standalone-еквівалент завантажує окремі компоненти або файли маршрутів безпосередньо:
```typescript
// app.routes.ts (після)
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)
}
];
```
```typescript
// settings/settings.routes.ts
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-и, які браузер отримує за запитом.
## Робота з SharedModules та спільними залежностями
SharedModules — універсальні модулі, що експортують широко використовувані компоненти, директиви та pipe — є найпоширенішою перешкодою під час міграції. Схема не може їх автоматично видалити, оскільки їх імпортують кілька модулів.
Рішення: конвертувати спільні оголошення в standalone по одному, а потім видалити SharedModule, коли нічого більше його не імпортує.
```typescript
// До: 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: ``
})
export class LoadingSpinner {}
```
Споживачі тепер імпортують `LoadingSpinner` безпосередньо замість цілого SharedModule. Бандлер включає лише ті компоненти, які потрібні кожному маршруту.
## Примусове використання виключно standalone
Після міграції запобігання появі нових NgModules у кодовій базі є критично важливим. Angular надає опцію компілятора TypeScript для цього.
```json
// tsconfig.json
{
"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` більше не вимагає імпорту цілих модулів для задоволення залежностей компонента.
```typescript
// hero-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
describe('HeroListComponent', () => {
let fixture: ComponentFixture;
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 та [реактивність на основі сигналів](/technologies/angular/interview-questions/angular-signals) природно поєднуються зі standalone-архітектурою для максимальної продуктивності
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/uk/blog/angular/angular-standalone-components-migration-best-practices