DEV Community

Cover image for Angular Modules and Lazy Loading: Complete Guide | Feature Modules, Shared Modules & Performance
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Modules and Lazy Loading: Complete Guide | Feature Modules, Shared Modules & Performance

When I first started building Angular applications, I put everything in the root AppModule. It worked fine for small apps, but as the application grew, the initial bundle size became huge, and the app took forever to load. That's when I learned about Angular modules and lazy loading, and it completely changed how I structure applications.

Angular modules are containers that organize related components, directives, pipes, and services. They help manage dependencies, organize code into logical units, and most importantly, enable lazy loading. Lazy loading means feature modules are only loaded when their routes are accessed, dramatically reducing the initial bundle size and improving startup time.

📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What are Angular Modules?

Angular Modules provide:

  • Code Organization - Group related components, services, and functionality
  • Dependency Management - Control what's available to other modules
  • Lazy Loading - Load modules on-demand for better performance
  • Encapsulation - Keep feature code isolated and maintainable
  • Reusability - Share components and services across modules

Root Module (AppModule)

The main application module that bootstraps your app:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from 'src/core/core.module';
import { SharedModule } from './shared/shared.module';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ReactiveFormsModule,
    FormsModule,
    CoreModule.forRoot(),
    SharedModule.forRoot()
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Only declare the root AppComponent
  • Import core modules (BrowserModule, HttpClientModule)
  • Import routing module
  • Import shared and core modules using forRoot()
  • Keep AppModule minimal

Feature Module

Organize features into separate modules:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { BusinessRoutingModule } from './business-routing.module';
import { BusinessListComponent } from './business-list/business-list.component';
import { BusinessDetailsComponent } from './business-details/business-details.component';
import { BusinessSettingsComponent } from './business-settings/business-settings.component';
import { BusinessService } from 'src/services/business.service';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  declarations: [
    BusinessListComponent,
    BusinessDetailsComponent,
    BusinessSettingsComponent
  ],
  imports: [
    CommonModule, // Use CommonModule instead of BrowserModule
    ReactiveFormsModule,
    BusinessRoutingModule,
    SharedModule
  ],
  providers: [BusinessService]
})
export class BusinessModule { }
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Use CommonModule instead of BrowserModule in feature modules
  • Declare all components, directives, and pipes used in the feature
  • Import only what the feature needs
  • Provide feature-specific services

Lazy Loading

Implement lazy loading for feature modules to reduce initial bundle size:

// app-routing.module.ts
import { Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'business',
    loadChildren: () => import('./business/business.module')
      .then(m => m.BusinessModule)
  },
  {
    path: 'users',
    loadChildren: () => import('./users/users.module')
      .then(m => m.UsersModule)
  },
  {
    path: 'sites',
    loadChildren: () => import('./site/site.module')
      .then(m => m.SiteModule)
  },
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Feature Module Routing

Each lazy-loaded module has its own routing:

// business-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BusinessListComponent } from './business-list/business-list.component';
import { BusinessDetailsComponent } from './business-details/business-details.component';
import { BusinessSettingsComponent } from './business-settings/business-settings.component';

const businessRoutes: Routes = [
  { path: '', component: BusinessListComponent },
  { path: ':id', component: BusinessDetailsComponent },
  { path: ':id/settings', component: BusinessSettingsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(businessRoutes)], // Use forChild, not forRoot
  exports: [RouterModule]
})
export class BusinessRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Use loadChildren with dynamic import syntax
  • Use RouterModule.forChild() in feature modules
  • Routes are relative to the feature module path
  • Modules load only when their routes are accessed

Shared Module

Create reusable shared components, directives, and pipes:

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserInfoCardComponent } from './user-info-card/user-info-card.component';
import { DocumentViewerComponent } from './document-viewer/document-viewer.component';
import { ImageViewUploadComponent } from './image-view-upload/image-view-upload.component';

@NgModule({
  declarations: [
    UserInfoCardComponent,
    DocumentViewerComponent,
    ImageViewUploadComponent
  ],
  imports: [CommonModule],
  exports: [
    UserInfoCardComponent,
    DocumentViewerComponent,
    ImageViewUploadComponent
  ]
})
export class SharedModule {
  static forRoot(): ModuleWithProviders<SharedModule> {
    return {
      ngModule: SharedModule,
      providers: []
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Export components, directives, and pipes that other modules need
  • Import CommonModule for common directives (ngIf, ngFor)
  • Use forRoot() pattern if the module has providers
  • Import SharedModule in feature modules that need shared components

Core Module

Organize singleton services in a Core module:

import { NgModule, ModuleWithProviders, Optional, SkipSelf } from '@angular/core';
import { AuthService } from './auth/auth.service';
import { ToastService } from './controls/toast-global/toast.service';

@NgModule({
  declarations: [],
  imports: [],
  exports: []
})
export class CoreModule {
  static forRoot(): ModuleWithProviders<CoreModule> {
    return {
      ngModule: CoreModule,
      providers: [
        AuthService,
        ToastService
      ]
    };
  }

  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error('CoreModule is already loaded. Import it in the AppModule only');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Use forRoot() to provide singleton services
  • Prevent multiple imports with constructor guard
  • Import only in AppModule
  • Contains app-wide singleton services

Module Architecture Patterns

Recommended Module Structure

app/
├── core/
│   ├── core.module.ts
│   ├── services/
│   └── interceptors/
├── shared/
│   ├── shared.module.ts
│   ├── components/
│   ├── directives/
│   └── pipes/
├── features/
│   ├── business/
│   │   ├── business.module.ts
│   │   ├── business-routing.module.ts
│   │   └── components/
│   ├── users/
│   │   ├── users.module.ts
│   │   ├── users-routing.module.ts
│   │   └── components/
│   └── sites/
│       ├── site.module.ts
│       ├── site-routing.module.ts
│       └── components/
├── app.module.ts
└── app-routing.module.ts
Enter fullscreen mode Exit fullscreen mode

Module Types

  1. AppModule - Root module, bootstraps the application
  2. Feature Modules - Organize features (Business, Users, Sites)
  3. Shared Module - Reusable components, directives, pipes
  4. Core Module - Singleton services, app-wide providers
  5. Routing Modules - Route configuration for features

Lazy Loading Benefits

Performance Benefits:

  • Reduced Initial Bundle Size - Only load what's needed initially
  • Faster Startup Time - Smaller initial bundle loads faster
  • Better Code Splitting - Each feature module becomes a separate chunk
  • Improved User Experience - Users see content faster

Example Bundle Sizes:

  • Without lazy loading: 2.5 MB initial bundle
  • With lazy loading: 500 KB initial bundle + feature chunks on demand

Preloading Strategy

Configure preloading for better performance:

import { PreloadAllModules } from '@angular/router';

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules // Preload all lazy modules
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Custom Preloading Strategy

import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      return load();
    }
    return of(null);
  }
}

// Use in routes
{
  path: 'business',
  loadChildren: () => import('./business/business.module').then(m => m.BusinessModule),
  data: { preload: true }
}
Enter fullscreen mode Exit fullscreen mode

Common Patterns

Module with Providers

@NgModule({
  providers: [BusinessService]
})
export class BusinessModule { }
Enter fullscreen mode Exit fullscreen mode

Module with Exports

@NgModule({
  declarations: [SharedComponent],
  exports: [SharedComponent] // Make available to other modules
})
export class SharedModule { }
Enter fullscreen mode Exit fullscreen mode

Module with Imports

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule,
    SharedModule
  ]
})
export class FeatureModule { }
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use lazy loading for feature modules - Reduce initial bundle size
  2. Create shared modules - For reusable components, directives, and pipes
  3. Use core module - For singleton services (import only in AppModule)
  4. Keep feature modules focused - One feature or domain per module
  5. Export only what's needed - From shared modules
  6. Avoid importing the same module multiple times - Use shared modules
  7. Use forRoot() pattern - For modules with providers
  8. Organize by feature - Not by file type
  9. Keep AppModule minimal - Delegate to feature modules
  10. Use module boundaries - To enforce architecture

Module Organization Tips

  • Feature Modules - Organize by business domain (Business, Users, Products)
  • Shared Module - Common UI components used across features
  • Core Module - App-wide singleton services
  • Routing Modules - Separate routing configuration

Common Mistakes to Avoid

❌ Don't Import BrowserModule in Feature Modules

// ❌ Wrong
@NgModule({
  imports: [BrowserModule]
})

// ✅ Correct
@NgModule({
  imports: [CommonModule]
})
Enter fullscreen mode Exit fullscreen mode

❌ Don't Import CoreModule Multiple Times

// ❌ Wrong - Importing in feature module
@NgModule({
  imports: [CoreModule]
})

// ✅ Correct - Only in AppModule
@NgModule({
  imports: [CoreModule.forRoot()]
})
Enter fullscreen mode Exit fullscreen mode

❌ Don't Use forRoot() in Feature Modules

// ❌ Wrong
@NgModule({
  imports: [SharedModule.forRoot()]
})

// ✅ Correct
@NgModule({
  imports: [SharedModule]
})
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

Conclusion

Angular Modules and Lazy Loading provide a powerful way to organize code and manage dependencies. With proper module architecture and lazy loading, you can build scalable, maintainable, and performant Angular applications.

Key Takeaways:

  • Angular Modules - Organize code into cohesive blocks
  • Feature Modules - Organize by business domain
  • Shared Modules - Reusable components and directives
  • Core Module - Singleton services for app-wide use
  • Lazy Loading - Load modules on-demand for better performance
  • forRoot() Pattern - For modules with providers
  • Module Architecture - Organize by feature, not by file type
  • Performance - Lazy loading reduces initial bundle size

Whether you're building a small application or a large enterprise system, Angular modules provide the foundation you need. They organize your code, manage dependencies, and enable lazy loading for optimal performance.


What's your experience with Angular Modules and Lazy Loading? Share your tips and tricks in the comments below! 🚀


💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.

Top comments (0)