Standalone Components: How I Restructured a Large Angular App Without Stopping Feature Work
NgModules weren't the problem — the dependency graph they hid was. A file-by-file migration path that never needed a code freeze.
Our app had 31 NgModules. Not one of them was there because someone designed a boundary — they were there because that's what ng generate produced in 2021. SharedModule imported and exported 40 things, so importing it anywhere pulled in everything, and nobody could tell you what a given component actually depended on.
Why standalone is a real improvement, not a syntax change
With NgModules, a component's dependencies live in a different file. With standalone components, the imports array sits right on the component. That sounds cosmetic until you look at what it enables:
- You can read one file and know everything the component uses.
- Tree-shaking gets accurate, because the graph is explicit instead of module-wide.
- Lazy loading works at the component level via
loadComponent, not just the route-module level. - Tests stop needing a
TestBedmodule declaration ritual — you import the component itself.
The migration order that worked
The official schematic (ng generate @angular/core:standalone) does the mechanical work well, but running it across the whole repo at once produces a diff no one can review. I ran it in slices, bottom-up:
- 01Leaf components first — buttons, badges, empty states, anything with no child components of its own. These convert cleanly and merge in a day.
- 02Directives and pipes next — same reason, and everything above them depends on them.
- 03Feature components — one feature folder per PR, converting the components and adding their explicit
imports. - 04Routes — swap
loadChildrenforloadComponent, deleting the routing module as you go. - 05Bootstrap last —
bootstrapApplicationwithproviders, then deleteAppModule.
The critical property of that order: at every point, the app builds and the two styles coexist. A standalone component can be imported by an NgModule, and an NgModule-declared component can be used inside a standalone one via imports: [SomeModule]. There was never a moment where feature work had to pause.
Killing SharedModule
This was the highest-value part and the one people resist. SharedModule feels convenient — one import and you have everything. That convenience is exactly the cost: your bundle contains everything too, and every change to it invalidates the build cache for every consumer.
What replaced it was nothing at all. Each component imports the three or four things it actually uses:
@Component({
selector: 'app-payroll-summary',
standalone: true,
imports: [DatePipe, CurrencyPipe, StatusBadgeComponent, RouterLink],
templateUrl: './payroll-summary.component.html',
})
export class PayrollSummaryComponent { }Yes, that's more lines across the codebase. It is also the first time the dependency graph has been true. When we later removed a deprecated badge component, the compiler told us exactly which eleven files used it. Under SharedModule, that was a grep and a prayer.
Providers: the one genuine gotcha
NgModules were doing double duty — declaring components *and* providing services. When you delete the module, the providers need a new home. Three options, in the order I prefer them:
providedIn: 'root'on the service — for anything genuinely app-wide. Tree-shakeable, no config.- Route-level
providers— for a service scoped to a feature. It's instantiated when the route loads and destroyed when it unloads, which is usually what the old feature-module scoping meant anyway. - Component-level
providers— for per-instance state, like a form coordinator shared between a parent and its children.
Results
Initial bundle dropped 22% — mostly dead code that SharedModule had been keeping alive. Cold build time improved noticeably. But the change I actually feel day to day is smaller and harder to measure: I can open any component file and understand it without opening a second one.
If your app is still on NgModules, you don't need permission or a project plan. Convert the next leaf component you touch. In four months you'll be mostly done.