Skip to content
    All posts
    Angular

    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.

    3 min readby

    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 TestBed module 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:

    1. 01Leaf components first — buttons, badges, empty states, anything with no child components of its own. These convert cleanly and merge in a day.
    2. 02Directives and pipes next — same reason, and everything above them depends on them.
    3. 03Feature components — one feature folder per PR, converting the components and adding their explicit imports.
    4. 04Routes — swap loadChildren for loadComponent, deleting the routing module as you go.
    5. 05Bootstrap last — bootstrapApplication with providers, then delete AppModule.

    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:

    ts
    @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.

    AngularArchitectureMigration

    Keep reading

    PAPulapa Arun Kumar

    Full-stack developer building performant, clean, and user-friendly web & mobile applications. Also written as Arun Kumar Pulapa — same person, surname first.

    Built with

    ReactTypeScriptTailwind CSSVite

    © 2026 Pulapa Arun Kumar. All rights reserved.