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

Standalone-компоненти Angular усувають потребу в NgModules, зменшуючи обсяг шаблонного коду та відкриваючи можливості для детального lazy loading по всьому додатку. Відтоді як Angular 19 зробив standalone режимом за замовчуванням, а Angular 22 представив стабільні безселекторні компоненти, міграція застарілих модульних кодових баз стала водночас простою та високоефективною.
Офіційна схема Angular CLI автоматично обробляє більшу частину міграції за три проходи. Типовий корпоративний додаток може завершити конверсію за один спринт, а розміри бандлів зменшуються на 30-50% завдяки lazy loading на рівні компонентів.
NgModules проти standalone-компонентів: що змінилося
NgModules виконували роль контексту компіляції для компонентів починаючи з Angular 2. Кожен компонент, директива та pipe мали бути оголошені рівно в одному модулі, а спільна функціональність вимагала ретельно організованих імпортів та експортів модулів. Це створювало тісний зв'язок між непов'язаними функціями та ускладнювало tree-shaking.
Standalone-компоненти перевертають цю модель. Кожен компонент оголошує власні залежності безпосередньо в масиві imports декоратора @Component. Без реєстрації в модулі, без спільних модулів, без barrel-експортів половини додатку. Починаючи з Angular 22, цей патерн ще більше спрощений завдяки безселекторним компонентам, які дозволяють прямий імпорт без рядкових селекторів.
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('');
}Масив imports замінює весь граф залежностей NgModule. Бандлер бачить точно, які компоненти, pipe та директиви потрібні кожному файлу, що уможливлює точний tree-shaking. CommonModule більше не потрібен в Angular 22, оскільки синтаксис control flow (@for, @if) вбудований в компілятор.
3-кроковий процес міграції CLI
Angular надає автоматизовану схему, яка обробляє міграцію в трьох послідовних проходах. Кожен крок базується на попередньому, і проєкт повинен компілюватися чисто між кожним проходом.
Крок 1: Конвертація оголошень у standalone
Перший прохід сканує кожен компонент, директиву та pipe в проєкті, додає standalone: true і переносить необхідні імпорти з батьківського NgModule у власний масив imports кожного компонента.
# Крок 1: Конвертація всіх оголошень у standalone
ng generate @angular/core:standalone --path=src/appОберіть "Convert all components, directives and pipes to standalone" при запиті. Схема використовує статичний аналіз для вирішення залежностей, тому будь-який компонент з метаданими, які неможливо проаналізувати під час збірки, буде пропущений з попередженням.
Крок 2: Видалення непотрібних NgModules
Коли всі оголошення вже standalone, багато NgModules стають порожніми оболонками. Цей прохід ідентифікує модулі, які лише реекспортували standalone-оголошення, і видаляє їх.
# Крок 2: Видалення порожніх NgModules
ng generate @angular/core:standalone --path=src/appОберіть "Remove unnecessary NgModule classes". Модулі, які все ще містять провайдери, конфігурації маршрутів або імпортуються кількома іншими модулями, будуть збережені з коментарем TODO для ручного перегляду.
Angular 22 вимагає TypeScript 6 та Node.js 26. Старіші версії не підтримуються. Перед початком міграції запустіть ng version, щоб перевірити сумісність.
Крок 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, 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]))
]
};Патерн ApplicationConfig замінює масиви providers та imports кореневого модуля. Всі функції-провайдери (provideRouter, provideHttpClient, provideAnimations) працюють безпосередньо без модульних обгорток.
Angular 22: OnPush за замовчуванням та безселекторні компоненти
Angular 22 представив дві зміни, що впливають на розробку standalone-компонентів: OnPush як стратегія виявлення змін за замовчуванням та стабільний статус безселекторних компонентів.
OnPush за замовчуванням
Нові компоненти, згенеровані в Angular 22, використовують ChangeDetectionStrategy.OnPush за замовчуванням. Це заохочує реактивність на основі сигналів замість перевірок, ініційованих Zone.js. Під час міграції схема додає явне changeDetection: ChangeDetectionStrategy.Default до існуючих компонентів для збереження зворотної сумісності.
// Angular 22: OnPush за замовчуванням для нових компонентів
@Component({
selector: 'app-dashboard',
standalone: true,
// changeDetection: ChangeDetectionStrategy.OnPush тепер за замовчуванням
template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
title = signal('Dashboard');
}Для мігрованих додатків розгляньте поступове впровадження OnPush шляхом заміни властивостей класу на сигнали. Це узгоджується з архітектурою signal-first, яку просуває Angular 22.
Безселекторні компоненти (стабільні)
Безселекторні компоненти дозволяють імпортувати компоненти безпосередньо в шаблонах без визначення рядкових селекторів. Це усуває конфлікти імен селекторів у великих кодових базах і забезпечує кращу типову безпеку.
// Традиційний підхід з селектором
@Component({
selector: 'app-user-card',
standalone: true,
template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
user = input.required<User>();
}
// Безселекторний підхід (Angular 22+)
@Component({
standalone: true,
template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
user = input.required<User>();
}У шаблонах безселекторні компоненти використовуються за їх ім'ям класу, а не за тегом спеціального селектора. Компілятор гарантує наявність імпорту, виявляючи помилки під час компіляції, а не під час виконання.
Міграція маршрутизації: від модулів до 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 призводить до помилки компіляції. Це забезпечує дотримання нової архітектури у всій команді. Міграція форм Angular слідує тому ж патерну, при цьому Signal Forms є стабільними в Angular 22.
Приріст продуктивності: розмір бандлів та 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-компонентів з Vitest
Юніт-тести значно спрощуються зі standalone-компонентами. Angular 22 зробив Vitest test runner за замовчуванням, замінивши Karma. Конфігурація TestBed більше не вимагає імпорту цілих модулів для задоволення залежностей компонента.
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);
});
});Компонент потрапляє безпосередньо в масив 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 запитують про standalone-компоненти, зосередьтеся на трьох пунктах: (1) управління залежностями переміщується з рівня модуля на рівень компонента, (2) tree-shaking стає гранулярним, (3) налаштування тестів спрощується, оскільки компоненти оголошують власні імпорти.
Джерела
- Angular v22 Release Notes - OnPush за замовчуванням, стабільні безселекторні компоненти, вимога TypeScript 6
- Angular Standalone Migration Guide - Офіційна документація 3-крокової схеми
- What's New in Angular 22 - Детальний changelog з інформацією про зміни стратегії виявлення
- Selectorless Components RFC - Обґрунтування дизайну та статус реалізації
Міграція до standalone: ключові висновки для Angular 22
- Офіційна схема Angular CLI автоматизує 80-90% міграції у трьох послідовних проходах: конвертація оголошень, видалення модулів, зміна bootstrap
- Модулі маршрутизації потребують ручної конвертації з
loadChildrenз NgModules наloadComponentабо standalone файли маршрутів - SharedModules є основною перешкодою, конвертуйте кожне спільне оголошення в standalone окремо, потім видаліть модуль
- Увімкніть
strictStandaloneв tsconfig для запобігання появі нових NgModules після міграції - Розміри бандлів зменшуються на 30-55% завдяки tree-shaking на рівні компонентів та детальному lazy loading з
loadComponent - Юніт-тести спрощуються з Vitest як test runner за замовчуванням, standalone-компонент імпортується безпосередньо в
TestBedбез конфігурації модулів - OnPush за замовчуванням в Angular 22 та безселекторні компоненти природно поєднуються зі standalone-архітектурою для чистішої та зручнішої в підтримці кодової бази
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Чи знайдеш ти помилку в Angular?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 21 серпня 2026 р.
Теги
Поділитися
Пов'язані статті

Просунутий Dependency Injection в Angular 2026: Providers, Tokens та Питання на Співбесідах
Комплексний посібник з просунутих механізмів Dependency Injection в Angular. Providers, InjectionToken, ієрархія інжекторів та найпоширеніші питання на технічних співбесідах.

Синтаксис Control Flow в Angular 2026: @if, @for, @switch та питання на співбесідах
Повний посібник з блокового синтаксису керування потоком в Angular. Вивчіть @if, @for, @switch з практичними прикладами коду та підготуйтесь до технічних співбесід.

NgRx Signal Store vs Класичний NgRx у 2026: Який Обрати?
Комплексне порівняння NgRx Signal Store та класичного NgRx для управління станом в Angular. Практичні приклади коду та рекомендації на 2026 рік.