Skip to main content
Version: 4.0.0-preview

Lazy Loading

Zero-config module loading with automatic route detection.

Overview

FeatureDescription
Zero-ConfigRoutes auto-detected from @controller()
One-Liner SetupsetupLazyLoadingForExpress()
Warmup StrategiesIdle, immediate, scheduled
Preload HintsLow, medium, high, never
Performance MetricsBuilt-in tracking
Auto MiddlewareLoads middleware for lazy routes
ExpressoTS v4 lazy loading: HTTP request checks module load state, loads and binds module on first access, registers routes, then executes handler with optional warmup strategy

Quick Start

1. Create Lazy Modules

import { CreateLazyModule } from "@expressots/core";
import { ReportsController } from "./reports.controller";
import { AdminController } from "./admin.controller";

// Reports module - loads when /reports routes are accessed
export const ReportsLazyModule = CreateLazyModule(
[ReportsController],
{ name: "ReportsModule" }
).withPreloadHint("low");

// Admin module - only loads when accessed
export const AdminLazyModule = CreateLazyModule(
[AdminController],
{ name: "AdminModule" }
).withPreloadHint("never");

2. Setup Lazy Loading

import { setupLazyLoadingForExpress } from "@expressots/adapter-express";

export class App extends AppExpress {
private config: AppContainer = this.configContainer([
// Eager modules (loaded at startup)
CoreModule,
AuthModule,
UserModule,

// Don't add lazy modules here!
]);

async configureServices(): Promise<void> {
// One-liner setup!
const { lazyModulesCount, middleware, routeMappings } =
setupLazyLoadingForExpress(this.container.Container, {
lazyModules: [ReportsLazyModule, AdminLazyModule],
globalPrefix: "/api",
enableMetrics: true,
enableWarmup: true,
warmupConfig: {
strategy: "idle",
delay: 10000,
hints: ["low", "medium"]
}
});

// Add auto-load middleware
if (middleware) {
this.Middleware.add(middleware);
}

console.log(`Registered ${lazyModulesCount} lazy modules`);
console.log("Auto-detected routes:", routeMappings);
}
}

3. Define Controllers

Routes are automatically detected from @controller() decorators:

// routes automatically detected!
@controller("/reports")
export class ReportsController {
@Get("/")
getAllReports() {
return { reports: [] };
}

@Get("/generate")
generateReport() {
return { status: "generating" };
}
}

No manual route mapping needed! The system automatically:

  • Detects /api/reports/* routes
  • Maps them to ReportsLazyModule
  • Loads module on first access

Auto-Detection System

How It Works

The lazy loading system automatically detects routes from your controllers:

@controller("/admin")
export class AdminController {
@Get("/dashboard") // Detected: /api/admin/dashboard
dashboard() {}

@Get("/users") // Detected: /api/admin/users
users() {}
}

Behind the scenes:

  1. System scans @controller() decorators
  2. Extracts route prefixes ("/admin")
  3. Combines with globalPrefix ("/api")
  4. Maps routes to lazy modules automatically

Result: /api/admin/*AdminLazyModule

Manual Route Mapping (Optional)

For complex scenarios, provide manual mappings:

setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule],
routePrefixes: {
ReportsModule: "/reports", // Manual mapping: module name -> route prefix
},
globalPrefix: "/api"
});

Preload Hints

Control when/if modules are preloaded using hints:

Never (Default for Admin Modules)

Never preload - only load when accessed:

const AdminLazyModule = CreateLazyModule(
[AdminController],
{ name: "AdminModule" }
).withPreloadHint("never");

Use for: Admin panels, rarely-used features

Low Priority

Preload during idle time (low priority):

const ReportsLazyModule = CreateLazyModule(
[ReportsController],
{ name: "ReportsModule" }
).withPreloadHint("low");

Use for: Features used by some users occasionally

Medium Priority

Preload after initial startup:

const AnalyticsLazyModule = CreateLazyModule(
[AnalyticsController],
{ name: "AnalyticsModule" }
).withPreloadHint("medium");

Use for: Features used by many users

High Priority

Preload immediately after startup:

const SearchLazyModule = CreateLazyModule(
[SearchController],
{ name: "SearchModule" }
).withPreloadHint("high");

Use for: Features used by almost all users


Warmup Strategies

Preload modules during idle time:

setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule, AdminLazyModule],
enableWarmup: true,
warmupConfig: {
strategy: "idle", // Load during idle
delay: 10000, // Wait 10s after startup
hints: ["low", "medium"] // Only preload low/medium priority
}
});

Best for: Most applications (doesn't block startup)

Immediate Warmup

Preload all modules immediately after startup:

warmupConfig: {
strategy: "immediate",
hints: ["low", "medium", "high"]
}

Best for: Applications where startup time isn't critical

Scheduled Warmup

Start warmup after a fixed delay:

warmupConfig: {
strategy: "scheduled",
delay: 30000, // Start warmup 30s after setup
hints: ["low", "medium"]
}

Best for: Deferring warmup until after peak startup traffic

Manual Warmup

Omit warmupConfig and trigger warmup (or load modules) yourself using the instances returned by setup:

const { manager, warmup } = setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule, AdminLazyModule],
enableWarmup: true
// No warmupConfig: warmup does not start automatically
});

// Later in code
await warmup?.start({ strategy: "immediate", hints: ["low", "medium"] });

// Or load specific modules directly
await manager.load("ReportsModule");
await manager.loadByHint("low");

Best for: Custom warmup logic based on conditions

No Warmup

Only load on-demand (maximum lazy):

enableWarmup: false

Best for: Microservices, minimal memory usage


Advanced Configuration

Prefetch Configuration

Configure when to prefetch modules:

const AdminLazyModule = CreateLazyModule(
[AdminController],
{ name: "AdminModule" }
)
.withPreloadHint("never")
.withLazyConfig({
prefetchOn: [
{
route: "/dashboard",
reason: "Admin link visible in menu"
}
],
prefetchAfterIdle: 30000 // Prefetch after 30s idle
});

Use cases:

  • Prefetch when user hovers over link
  • Prefetch when user visits dashboard (admin likely next)
  • Prefetch after idle timeout

Load Timeout

Configure maximum load time (default is 30000ms):

const HeavyLazyModule = CreateLazyModule(
[HeavyController],
{ name: "HeavyModule" }
).withLazyConfig({
timeout: 5000 // 5 second timeout
});

Monitoring & Metrics

Enable Metrics

setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule, AdminLazyModule],
enableMetrics: true // Enable performance tracking
});

Access Metrics

import { LazyModuleManager, LazyLoadMetrics } from "@expressots/core";

@controller("/diagnostics")
export class DiagnosticsController {
constructor(
@inject(LazyModuleManager)
private manager: LazyModuleManager,
@inject(LazyLoadMetrics)
private metrics: LazyLoadMetrics
) {}

@Get("/lazy-loading")
getStatistics() {
const stats = this.manager.getStatistics();

return {
totalModules: stats.totalModules,
loadedModules: stats.loadedModules,
lazyModules: stats.lazyModules,
failedModules: stats.failedModules,
avgLoadTime: stats.avgLoadTime,
recommendations: this.metrics.getRecommendations()
};
}
}

Metrics Available:

  • Total registered modules
  • Loaded / lazy / failed module counts
  • Average and total load time
  • Estimated memory saved
  • Per-module recommendations (via LazyLoadMetrics)

Real-Time Monitoring

const manager = this.Provider.get(LazyModuleManager);

// Inspect module state
console.log(manager.getLoadedModules()); // ["ReportsModule"]
console.log(manager.getPendingModules()); // ["AdminModule"]
console.log(manager.getFailedModules()); // []

// Check if a module is loaded
if (manager.isLoaded("AdminModule")) {
console.log("Admin module is ready");
}

// Load a module manually
await manager.load("AdminModule");

Route Mappings

After setup, inspect auto-detected routes:

const { routeMappings } = setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule, AdminLazyModule]
});

console.log(routeMappings);
// [
// { prefix: "/reports", moduleName: "ReportsModule", loaded: false },
// { prefix: "/admin", moduleName: "AdminModule", loaded: false }
// ]

Prefixes are auto-detected from @controller() decorators. The globalPrefix (e.g. "/api") is prepended at request-matching time.


Performance Characteristics

Startup Time Reduction

Before Lazy Loading:

Application startup: 2500ms
- CoreModule: 500ms
- AuthModule: 300ms
- UserModule: 400ms
- ReportsModule: 800ms ← Heavy module
- AdminModule: 500ms ← Rarely used

After Lazy Loading:

Application startup: 1200ms (52% faster!)
- CoreModule: 500ms
- AuthModule: 300ms
- UserModule: 400ms
- ReportsModule: Lazy (on-demand)
- AdminModule: Lazy (on-demand)

Memory Usage

Before: 450MB (all modules loaded)
After: 280MB (only eager modules loaded)
Savings: 170MB (38% reduction)

First Request Latency

Initial Load: +50-200ms (one-time cost)
Subsequent Requests: 0ms overhead
Warmup: Eliminates first-request latency


Best Practices

1. Identify Good Candidates

✅ Good candidates for lazy loading:

  • Admin panels (rarely accessed)
  • Reporting modules (heavy, occasional use)
  • Feature flags (may not be enabled)
  • Premium features (limited user access)
  • Analytics dashboards
  • Export functionality
  • Batch processing endpoints

❌ Poor candidates:

  • Authentication modules
  • Core API endpoints
  • High-traffic routes
  • Small, lightweight modules

2. Use Appropriate Hints

// ✅ Good: Logical hints
AdminModule → "never" // Rarely used
ReportsModule → "low" // Occasional use
SearchModule → "medium" // Frequent use
CoreApiModule → "high" // Critical, but large

// ❌ Bad: Everything "never"
AdminModule → "never"
ReportsModule → "never"
SearchModule → "never" // Search is frequently used!

3. Configure Warmup

// ✅ Good: Idle warmup for production
warmupConfig: {
strategy: "idle",
delay: 10000,
hints: ["low", "medium"]
}

// ❌ Bad: Immediate warmup (defeats purpose)
warmupConfig: {
strategy: "immediate",
hints: ["low", "medium", "high", "never"] // Loads everything!
}

4. Monitor Performance

// ✅ Good: Enable metrics in development
setupLazyLoadingForExpress(container, {
enableMetrics: process.env.NODE_ENV === "development"
});

// ❌ Bad: No metrics (can't optimize)
setupLazyLoadingForExpress(container, {
enableMetrics: false
});

5. Test Lazy Loading

// ✅ Good: Test lazy routes
test("Reports module loads on demand", async () => {
const response = await request(app)
.get("/api/reports")
.expect(200);

// Verify module loaded
const manager = container.get(LazyModuleManager);
expect(manager.isLoaded("ReportsModule")).toBe(true);
});

Migration from Eager Loading

Before (v3 / Eager Loading)

export class App extends AppExpress {
private config: AppContainer = this.configContainer([
CoreModule,
AuthModule,
UserModule,
ReportsModule, // Always loaded (slow startup)
AdminModule, // Always loaded (rarely used)
AnalyticsModule // Always loaded (heavy)
]);
}

Startup: 2500ms
Memory: 450MB

After (v4 / Lazy Loading)

// 1. Create lazy modules
export const ReportsLazyModule = CreateLazyModule([ReportsController], { name: "ReportsModule" }).withPreloadHint("low");
export const AdminLazyModule = CreateLazyModule([AdminController], { name: "AdminModule" }).withPreloadHint("never");
export const AnalyticsLazyModule = CreateLazyModule([AnalyticsController], { name: "AnalyticsModule" }).withPreloadHint("medium");

// 2. Setup
export class App extends AppExpress {
private config: AppContainer = this.configContainer([
CoreModule,
AuthModule,
UserModule
// Lazy modules NOT included here
]);

async configureServices(): Promise<void> {
const { middleware } = setupLazyLoadingForExpress(
this.container.Container,
{
lazyModules: [
ReportsLazyModule,
AdminLazyModule,
AnalyticsLazyModule
],
globalPrefix: "/api",
enableMetrics: true,
enableWarmup: true,
warmupConfig: {
strategy: "idle",
delay: 10000,
hints: ["low", "medium"]
}
}
);

if (middleware) {
this.Middleware.add(middleware);
}
}
}

Startup: 1200ms (52% faster)
Memory: 280MB (38% reduction)


Troubleshooting

Module Not Loading

Check route mapping:

const { routeMappings } = setupLazyLoadingForExpress(...);
console.log(routeMappings); // Verify routes detected

Check controller decorator:

// ✅ Good
@controller("/reports")
export class ReportsController {}

// ❌ Bad: No decorator
export class ReportsController {}

Routes Not Auto-Detected

Provide manual mapping:

setupLazyLoadingForExpress(container, {
lazyModules: [ReportsLazyModule],
routePrefixes: { ReportsModule: "/reports" } // Manual mapping
});

Module Loads Too Slowly

Reduce load timeout:

CreateLazyModule([Controller], { name: "Module" })
.withLazyConfig({
timeout: 3000 // Reduce from default 30000ms
});

Check module dependencies:

  • Heavy imports?
  • Database connections in constructor?
  • Synchronous operations?

Warmup Not Working

Check strategy:

// ✅ Good
warmupConfig: {
strategy: "idle",
hints: ["low"] // Only warm up modules with "low" hint
}

// ❌ Bad: Wrong hint
warmupConfig: {
strategy: "idle",
hints: ["high"] // But module has "low" hint!
}

API Reference

setupLazyLoadingForExpress()

Exported from @expressots/adapter-express:

function setupLazyLoadingForExpress(
container: Container,
options?: {
lazyModules?: ILazyModule[];
routePrefixes?: Record<string, string>; // { moduleName: "/prefix" }
globalPrefix?: string;
enableAutoLoad?: boolean; // default: true
enableMetrics?: boolean; // default: true in development
enableWarmup?: boolean; // default: true
alwaysLoad?: string[]; // module names to load eagerly
neverLoad?: string[]; // module names to skip entirely
logLevel?: "debug" | "info" | "warn" | "error" | "none";
warmupConfig?: {
strategy: "idle" | "immediate" | "scheduled";
delay?: number;
maxConcurrent?: number;
priority?: string[];
hints?: ("low" | "medium" | "high" | "never")[];
};
}
): {
loader: LazyModuleLoader;
manager: LazyModuleManager;
metrics?: LazyLoadMetrics;
warmup?: LazyModuleWarmup;
middleware?: RequestHandler;
routeMappings: LazyRouteMapping[];
lazyModulesCount: number;
eagerModulesCount: number;
}

CreateLazyModule()

Exported from @expressots/core:

function CreateLazyModule(
controllers: Array<new (...args: unknown[]) => unknown>,
config?: {
name?: string;
preloadHint?: "high" | "medium" | "low" | "never"; // default: "low"
timeout?: number; // default: 30000
prefetchOn?: Array<{ route: string; reason?: string }>;
prefetchAfterIdle?: number;
dependencies?: string[];
routePrefixes?: string[];
estimatedMemoryUsage?: number;
}
): ILazyModule

The returned ILazyModule supports the fluent methods .withPreloadHint(hint) and .withLazyConfig(config), plus load(), status, isLoaded, loadTime, and error.

There is also createLazyModule(factory, config?) for building a lazy module from a custom ContainerModule factory.

LazyModuleManager

Exported from @expressots/core:

interface ILazyModuleManager {
isLoaded(moduleName: string): boolean;
getLoadedModules(): string[];
getPendingModules(): string[];
getFailedModules(): string[];
load(moduleName: string): Promise<void>;
loadByHint(hint: "high" | "medium" | "low" | "never"): Promise<void>;
getStatistics(): ModuleLoadStatistics;
unload(moduleName: string): Promise<boolean>;
}

Comparison with Other Frameworks

FeatureExpressoTSNestJSSpring Boot
Zero-Config✅ Auto-detection❌ Manual❌ Manual
One-Liner Setup✅ Yes❌ Complex setup❌ Complex setup
Preload Hints✅ 4 priorities❌ Not available❌ Not available
Warmup Strategies✅ 3 strategies❌ Not available⚠️ Custom only
Auto Route Detection✅ From decorators❌ Manual❌ Manual
Performance Metrics✅ Built-in❌ Custom only⚠️ Via actuator
Prefetch Hints✅ Built-in❌ Not available❌ Not available

Support the Project

ExpressoTS is MIT-licensed open source. See the support guide to contribute.