Angular lets you move fast—and that is exactly why it is easy to create a huge mess without noticing. If your app has already grown beyond 30 or so screens, you have probably felt it: change detection running for no reason, a forgotten subscribe holding on to memory, a form with a disguised any. The best practices below are not style rules; they are what separates a project that scales from one that gets stuck with every new feature.

Standalone is the standard now—stop dragging NgModule along
Standalone components have been stable since Angular 15, and they have been the CLI default since Angular 17. If you still create an @NgModule for every feature, you are paying an unnecessary boilerplate tax. Standalone components declare their dependencies directly in imports: [], eliminating the back-and-forth of declaring and exporting items in a module.
The real benefit shows up in lazy loading and tree-shaking: without intermediate modules, the bundler has a clearer view of what can be removed. Migrate gradually—the Angular team provides a schematic (ng generate @angular/core:standalone) to convert your app in stages. There is no need to rewrite everything in a single weekend.
Dumb component, smart component: split them or pay later
This idea is old, but it is still widely ignored. A smart component (container) knows where the data comes from: it injects services, communicates with the store, and triggers side effects. A dumb component (presentational component) only receives data through @Input and emits events through @Output—it does not know whether the data came from an API, cache, or mock.
Why does this matter in practice? A dumb component is trivial to test (input goes in, output comes out) and reusable by default. When you mix data fetching with rendering in the same place, every test becomes an HttpClient mock, and every reuse becomes copy-and-paste. The simple rule is this: if a component has a constructor packed with services and a large template, it wants to be two components.
OnPush + signals: change detection that does not betray you
By default, Angular checks the entire tree again after every event. In a small app, nobody notices; in a large app, typing starts to lag. ChangeDetectionStrategy.OnPush tells Angular to re-render only when an @Input changes by reference, an event is triggered in the component, or an observable emits through the async pipe.
Adopt OnPush by default, not as an optimization of last resort. Signals fit perfectly here (they have been stable since Angular 17): a signal() notifies change detection granularly, without requiring you to manage immutability manually. computed() derives state without recalculating unnecessarily. The OnPush + signals combination is currently the most predictable path—fewer markForCheck() calls scattered throughout the code and fewer bugs involving screens that fail to update.
RxJS without leaks: async pipe first, unsubscribe consciously
Manual subscriptions are the biggest source of memory leaks in Angular. Rule number one: let the async pipe handle it. It subscribes in the template and cancels automatically when the component is destroyed. *ngIf="data$ | async as data" solves 80% of cases without a single line of unsubscribe logic.
When you do need to subscribe in TypeScript—a side effect or a tap that opens a modal, for example—do not leave it unmanaged. Use takeUntilDestroyed() (from @angular/core/rxjs-interop, stable since Angular 16), which ties the subscription to the component lifecycle without ngOnDestroy boilerplate. Avoid a bare .subscribe() with no cleanup: it appears to work, then leaks silently for weeks until the app becomes sluggish for no obvious reason.
Typed forms and lazy routes: what actually changes
Reactive forms have been typed since Angular 14. If you still read form.value and get any, you are throwing away one of the best DX improvements of recent years. Declare forms with FormGroup<{ nome: FormControl<string> }> or let FormBuilder infer the types—and the compiler will warn you when you access a field that does not exist or pass the wrong type. Forms are where type bugs hurt the most; typing them pays off quickly.
For routes, load everything that is not the initial screen with loadComponent / loadChildren. With standalone components, loadComponent: () => import('./pagina').then(m => m.Pagina) splits the bundle without ceremony. The user downloads only what the current route needs, and the first load benefits from it.
Organize folders by feature, not by type
A components/, services/, models/ structure with fifty files in each folder is the classic approach that does not scale. You end up jumping from folder to folder just to understand a single feature. Organize by domain instead: features/orders/ with the component, service, types, and test together. Use shared/ only for things that are genuinely reusable, and core/ for app-wide singletons such as guards and interceptors.
The test is simple: to delete a feature, you should be able to remove one folder. If deleting it requires hunting for files in six different places, your structure is organized by type—and every change will cost more than it should.
None of these practices is revolutionary; they are decisions you make once and benefit from for years. Start with what hurts most in your project today—OnPush if you have lag, the async pipe if you have leaks, feature folders if you struggle to find things—and adopt the rest as defaults over time. Sustainable Angular code does not come from a new framework; it comes from stopping the postponement of these decisions.

