March 2025
·2 min read
Scalable Angular Architecture: Standalone Components & Lazy Loading
Architect large Angular apps with standalone components, feature-based folder structure, and route-level lazy loading for faster bundles.
Introduction
- The problem with
NgModule-heavy apps: circular imports, barrel files gone wrong, hard-to-split bundles. - Standalone components (stable since Angular 15) remove the NgModule requirement — what that means for architecture.
- Goal: a feature-based folder structure where every feature can be independently lazy-loaded, tested, and replaced.
Why Go Module-Free with Standalone Components
@Component({ standalone: true })— the component declares its own imports directly.- No more
declarations,exports, andentryComponentsin NgModules. - Faster bootstrapping:
bootstrapApplication(AppComponent, appConfig)replacesAppModule. - IDE and tree-shaking benefits: unused components are clearly orphaned.
// Before (NgModule world)
@NgModule({ declarations: [HeroListComponent], imports: [CommonModule] })
export class HeroModule {}
// After (standalone)
@Component({
selector: 'app-hero-list',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './hero-list.component.html',
})
export class HeroListComponent {}Feature-Based Folder Structure
- Recommended structure:
src/app/features/<feature-name>/containing components, services, routes, and types local to that feature. - Shared code:
src/app/shared/for cross-feature UI components and pipes;src/app/core/for singletons (auth, HTTP interceptors, error handling). - Keep feature services
providedIn: 'root'or provide them at route level for lazy-loaded scope.
src/app/
core/ ← singletons (auth, logger)
shared/ ← reusable UI components
features/
dashboard/
dashboard.routes.ts
dashboard.component.ts
widgets/
users/
users.routes.ts
user-list.component.ts
user-detail.component.ts
user.service.tsRoute-Level Lazy Loading Without NgModules
- Use
loadComponentfor individual lazy-loaded pages andloadChildrenfor a route config file. - Each feature exports a
Routesarray — no NgModule required.
// app.routes.ts
export const APP_ROUTES: Routes = [
{ path: 'dashboard', loadComponent: () =>
import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent) },
{ path: 'users', loadChildren: () =>
import('./features/users/users.routes').then(m => m.USERS_ROUTES) },
];- Result: Angular generates separate JS chunks per route automatically.
- Verify chunk sizes with
ng build --stats-json+webpack-bundle-analyzer.
Providing Services at Route Level (Scoped DI)
- Services provided at a route's
providersarray are created per lazy-loaded route tree and destroyed when the user navigates away. - Use case: feature-specific state that should not bleed between features.
// users.routes.ts
export const USERS_ROUTES: Routes = [
{
path: '',
providers: [UserService, UserStore],
children: [
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent },
],
},
];Shared UI Library Strategy
- Extract truly shared components to a local library using
ng generate libraryor a monorepo (Nx). - Barrel export pattern:
shared/index.tsre-exports all public components and directives. - Avoid over-abstracting: only extract components used in 3+ features.
Dependency Injection Best Practices at Scale
- Prefer
inject()function over constructor injection in standalone components for better tree-shakability. - Use
InjectionTokenfor configuration objects instead of@Injectableservices. HttpClientis tree-shakable when provided withprovideHttpClient(withInterceptors([...])).
Testing Standalone Components
TestBed.configureTestingModule({ imports: [MyStandaloneComponent] })— nodeclarations.- Override imports at test level to swap real HTTP for
provideHttpClientTesting(). - Faster unit tests: standalone components with no global module overhead.
Performance Checklist
ChangeDetectionStrategy.OnPushon every component.- Signals for local state (see: Angular Signals post).
@deferblocks for non-critical UI (Angular 17+): defers rendering until interaction or viewport visibility.trackBy/@for trackin lists to avoid full DOM re-renders.- Preloading strategy:
PreloadAllModulesor customQuicklinkStrategy.
Conclusion
- Standalone components + feature-based routing is Angular's modern baseline — no new projects should use NgModules.
- Lazy load at the route level from day one; retrofitting later is painful.
- The folder structure mirrors the router: if it's a route, it's a feature folder.
- Next steps: add Nx to scale to a monorepo, or explore
@ngrx/storefor cross-feature shared state.