Building Angular with open-source tools
This guide outlines the technical process for migrating a legacy enterprise Angular application from an NgModule-based architecture to a modern Standalone and Signal-based architecture. This transition reduces bundle size through better tree-shaking and simplifies the dependency graph for large-scale maintenance.
Execute Automated Standalone Migration
Use the Angular CLI's built-in migration tool to convert components, directives, and pipes to standalone. This command updates the 'standalone: true' flag and populates the 'imports' array within each class decorator.
ng generate @angular/core:standalone⚠ Common Pitfalls
- •The schematic may fail on complex templates with deep nesting; verify manual imports if template errors occur after migration.
- •Internal shared modules might be converted into multiple circular dependencies if not handled carefully.
Refactor Root Bootstrapping
Replace the platformBrowserDynamic bootstrap method in main.ts with bootstrapApplication. This removes the requirement for a root AppModule and allows for functional provider configurations.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes';
import { provideHttpClient } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient()
]
}).catch(err => console.error(err));⚠ Common Pitfalls
- •Ensure all global singleton services previously provided in AppModule are added to the providers array in bootstrapApplication.
- •Third-party libraries that still rely on NgModules must be imported using the importProvidersFrom() utility.
Transition Component State to Signals
Replace standard class properties and RxJS-heavy template bindings with Angular Signals. This enables fine-grained change detection and reduces the overhead of Zone.js.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `<div>{{ fullName() }}</div>`
})
export class UserProfileComponent {
firstName = signal('John');
lastName = signal('Doe');
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
updateName(newFirst: string) {
this.firstName.set(newFirst);
}
}⚠ Common Pitfalls
- •Avoid calling signal setters inside computed() functions as this will trigger an error.
- •Signals are not a direct replacement for RxJS streams that handle asynchronous events (like WebSockets); use toSignal for interop.
Integrate RxJS Interop for Data Fetching
Use the @angular/core/rxjs-interop package to bridge existing RxJS-based services with Signal-based components. This allows you to maintain complex observable logic in services while benefiting from Signal performance in the UI.
import { toSignal } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { UserService } from './user.service';
export class UserListComponent {
private userService = inject(UserService);
users = toSignal(this.userService.getUsers(), { initialValue: [] });
}⚠ Common Pitfalls
- •toSignal must be called in an injection context (constructor or field initializer) unless an Injector is provided.
- •Ensure the source observable is properly handled if it emits errors, as toSignal will re-throw them.
Implement Deferrable Views for Bundle Optimization
Wrap non-critical components, especially heavy third-party charts or editors, in @defer blocks. This automatically splits the code and only loads it when the specified condition (e.g., visibility or idle) is met.
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div>Loading chart...</div>
} @loading (after 100ms; minimum 500ms) {
<app-spinner />
}⚠ Common Pitfalls
- •The component inside @defer must be standalone; if it is part of an NgModule, it will not be lazy-loaded correctly.
- •Over-using @defer on very small components can increase the number of HTTP requests and negatively impact performance.
What you built
By moving to a Standalone and Signal-based architecture, enterprise Angular applications achieve better maintainability through explicit dependency declarations and improved runtime performance. This refactor sets the foundation for removing Zone.js in the future and adopting more performant rendering strategies.