Lazy Loading Guide
MemberJunction uses code-split lazy loading for feature chunks in MJExplorer. Instead of loading every component at startup, chunks are loaded on demand when needed. This keeps the initial bundle small and startup fast.
Table of Contents
Section titled “Table of Contents”- How It Works
- The Lazy Feature Config
- Making a Package Lazy-Loadable
- Adding a New Component
- Adding Non-Angular Classes
- Adding a New Feature Module (Subpath)
- How the Generator Works
- Coverage Audit
- Regenerating the Config
- Troubleshooting
How It Works
Section titled “How It Works”Lazy loading is handled universally by ClassFactory via registered lazy loaders. Any ClassFactory.GetRegistrationAsync() or CreateInstanceAsync() call that can’t find a registration synchronously will automatically trigger the lazy loader to pull in the correct chunk.
ApplicationManager needs HomeApplication | vClassFactory.CreateInstanceAsync(BaseApplication, 'HomeApplication') | vGetRegistration() returns null -- class not loaded yet | vClassFactory calls registered lazy loaders with('BaseApplication', 'HomeApplication') | vLazyModuleRegistry.Load('BaseApplication::HomeApplication') | vLooks up compound key in LAZY_FEATURE_CONFIG | vCalls: import('@memberjunction/ng-dashboards/core-dashboards.module') | vESBuild loads the chunk -> @RegisterClass decorators execute-> ClassFactory now has HomeApplication -> retry succeedsThree pieces make this work:
-
ClassFactory.RegisterLazyLoader()— Accepts N callback functions that are called in order when a registration is not found. Each loader receives(baseClassName, key)and returnsPromise<boolean>indicating whether it loaded the missing class. -
LAZY_FEATURE_CONFIG— An auto-generated record mapping compound keys (BaseClassName::Key) toimport()loaders. This is the bridge between “I needBaseApplication::HomeApplication” and “loadcore-dashboards.modulefromng-dashboards.” -
Subpath exports in
package.json— The bundler uses these to create separate chunks. Each subpath (e.g.,./ai-dashboards.module) becomes an independently loadable unit.
ClassFactory Async API
Section titled “ClassFactory Async API”// Async version of GetRegistration -- tries sync first, then lazy loadersconst reg = await ClassFactory.GetRegistrationAsync(BaseResourceComponent, 'HomeDashboard');
// Async version of CreateInstance -- tries sync first, then lazy loaders, then base class fallbackconst app = await ClassFactory.CreateInstanceAsync(BaseApplication, 'HomeApplication', args);
// Register a lazy loader (called by LazyModuleRegistry.WireToClassFactory())ClassFactory.RegisterLazyLoader(async (baseClassName, key) => { // Load the chunk, return true if successful});The sync GetRegistration() and CreateInstance() methods still work and are used for classes that are always eagerly loaded (entity classes, auth providers, etc.).
Wiring at Startup
Section titled “Wiring at Startup”In app.module.ts, the APP_INITIALIZER registers the lazy config and wires it to ClassFactory:
{ provide: APP_INITIALIZER, useFactory: (lazyRegistry: LazyModuleRegistry) => () => { lazyRegistry.RegisterBulk(LAZY_FEATURE_CONFIG); lazyRegistry.WireToClassFactory(); }, deps: [LazyModuleRegistry], multi: true}The Lazy Feature Config
Section titled “The Lazy Feature Config”The file at packages/Angular/Explorer/explorer-core/src/generated/lazy-feature-config.ts is auto-generated by mj codegen manifest --lazy-config. Never edit it manually.
It maps compound @RegisterClass keys to dynamic import() loaders:
// AUTO-GENERATED -- DO NOT EDIT
const loadCoreDashboardsModule = featureLoader( () => import('@memberjunction/ng-dashboards/core-dashboards.module'));
export const LAZY_FEATURE_CONFIG: Record<string, () => Promise<void>> = { 'BaseApplication::HomeApplication': loadCoreDashboardsModule, 'BaseResourceComponent::HomeDashboard': loadCoreDashboardsModule, 'BaseResourceComponent::APIKeysResource': loadCoreDashboardsModule, 'BaseDashboard::EntityAdmin': loadCoreDashboardsModule, // ...};Compound Key Format
Section titled “Compound Key Format”Keys use the format BaseClassName::Key, matching the two arguments to @RegisterClass(BaseClass, Key). This allows the lazy loader to handle any base class, not just BaseResourceComponent.
Making a Package Lazy-Loadable
Section titled “Making a Package Lazy-Loadable”A package is lazy-loadable when it has subpath exports in its package.json. The exports field is the package’s declarative marker that it supports lazy chunk loading. Without it, the generator skips the package entirely.
Adding subpath exports to a package
Section titled “Adding subpath exports to a package”For packages with multiple feature modules (like ng-dashboards):
{ "name": "@memberjunction/ng-dashboards", "exports": { ".": { "types": "./dist/public-api.d.ts", "default": "./dist/public-api.js" }, "./ai-dashboards.module": { "types": "./dist/ai-dashboards.module.d.ts", "default": "./dist/ai-dashboards.module.js" }, "./core-dashboards.module": { "types": "./dist/core-dashboards.module.d.ts", "default": "./dist/core-dashboards.module.js" } }}Each subpath becomes a separately loadable chunk. Classes reachable from that subpath’s entry point are grouped together.
What qualifies a package for lazy loading?
Section titled “What qualifies a package for lazy loading?”The generator includes a package in the lazy config when ALL of these are true:
- The package matches
--exclude-packages(e.g.,@memberjunction) - The package has at least one subpath export (key other than
"."inexports) - The package is NOT the host package where the lazy config file lives
- The package contains
@RegisterClassdecorators with key arguments
Adding a New Component
Section titled “Adding a New Component”When you add a new component with @RegisterClass(BaseResourceComponent, 'MyNewResource') to an existing feature module:
- Add the component to the feature module’s declarations and exports
- That’s it — the generator discovers it automatically on next build
The component’s @RegisterClass key is found during the AST scan, mapped to the subpath that contains it, and added to the lazy config with the compound key BaseResourceComponent::MyNewResource. No manual configuration needed.
Example
Section titled “Example”// In packages/Angular/Explorer/dashboards/src/AI/components/my-new.component.ts@RegisterClass(BaseResourceComponent, 'MyNewResource')@Component({ ... })export class MyNewComponent extends BaseResourceComponent { ... }// In packages/Angular/Explorer/dashboards/src/ai-dashboards.module.ts@NgModule({ declarations: [MyNewComponent], exports: [MyNewComponent]})export class AIDashboardsModule { }After rebuilding explorer-core, the lazy config automatically includes:
'BaseResourceComponent::MyNewResource': loadAiDashboardsModule,Adding Non-Angular Classes
Section titled “Adding Non-Angular Classes”The lazy loading system works with ANY @RegisterClass decorated class, not just Angular components. For example, HomeApplication is registered with @RegisterClass(BaseApplication, 'HomeApplication') and is lazy-loaded when ApplicationManager calls CreateInstanceAsync.
To make a non-Angular class lazy-loadable:
- Ensure it’s in a package with subpath exports
- Export it from the subpath module — this is critical. The class must be reachable from the subpath’s entry point so ESBuild includes it in the chunk:
// In core-dashboards.module.tsexport { HomeApplication } from './Home/home-application';- The generator picks up the
@RegisterClassdecorator and maps it with its base class:'BaseApplication::HomeApplication': loadCoreDashboardsModule - Any consumer using
CreateInstanceAsyncorGetRegistrationAsyncwill trigger the lazy load automatically
Adding a New Feature Module (Subpath)
Section titled “Adding a New Feature Module (Subpath)”When creating an entirely new feature area (e.g., a “Reporting” dashboard):
1. Create the feature module
Section titled “1. Create the feature module”@NgModule({ declarations: [ReportingOverviewComponent, ReportBuilderComponent], exports: [ReportingOverviewComponent, ReportBuilderComponent], imports: [CommonModule, /* ... */]})export class ReportingDashboardsModule { }2. Add the subpath export to package.json
Section titled “2. Add the subpath export to package.json”{ "exports": { "./reporting-dashboards.module": { "types": "./dist/reporting-dashboards.module.d.ts", "default": "./dist/reporting-dashboards.module.js" } }}3. Register components with keys
Section titled “3. Register components with keys”@RegisterClass(BaseResourceComponent, 'ReportingOverviewResource')export class ReportingOverviewComponent extends BaseResourceComponent { ... }4. Rebuild
Section titled “4. Rebuild”cd packages/Angular/Explorer/dashboards && npm run buildcd packages/Angular/Explorer/explorer-core && npm run buildThe generator automatically picks up the new subpath and its classes.
How the Generator Works
Section titled “How the Generator Works”The mj codegen manifest --lazy-config flag triggers lazy config generation as a post-processing step after the regular eager manifest:
- Walk the full dependency tree (ignoring
--exclude-packages) to find all packages - Filter to lazy candidates: packages matching
--exclude-packagesthat have subpath exports inpackage.json - Scan each candidate for
@RegisterClassdecorators (captures bothbaseClassNameandkey) - Resolve subpath membership: for each class, determine which subpath export it belongs to by following
.d.tsimport/export chains - Build compound keys: combine
baseClassNameandkeyintoBaseClassName::Keyformat - Detect collisions: error if the same compound key appears in different subpaths
- Generate the TypeScript config file with write-on-change semantics (preserves mtime if unchanged)
- Coverage audit: compare all
@RegisterClassclasses against the eager manifest + lazy config to detect gaps
Key files
Section titled “Key files”| File | Role |
|---|---|
packages/MJGlobal/src/ClassFactory.ts | RegisterLazyLoader, CreateInstanceAsync, GetRegistrationAsync |
packages/CodeGenLib/src/Manifest/GenerateClassRegistrationsManifest.ts | Generator implementation |
packages/MJCLI/src/commands/codegen/manifest.ts | CLI command |
packages/Angular/Explorer/explorer-core/src/generated/lazy-feature-config.ts | Generated output |
packages/Angular/Explorer/explorer-core/src/lib/services/lazy-module-registry.ts | Runtime registry + ClassFactory wiring |
Coverage Audit
Section titled “Coverage Audit”After generating the lazy config, the codegen automatically runs a coverage audit. It compares all @RegisterClass classes in excluded packages against the lazy config to find gaps — classes that won’t be loaded at runtime because they’re not in any subpath export.
--- Coverage Audit ---Coverage audit passed: all 74 keyed @RegisterClass classes are coveredIf gaps are found:
--- Coverage Audit ---Coverage gap: 1 @RegisterClass class(es) will be tree-shaken: @memberjunction/ng-dashboards: - HomeApplication (BaseApplication, 'HomeApplication') Not in any subpath export. Add to a module or the eager manifest.Strict mode
Section titled “Strict mode”Use --strict to make gaps a fatal error (useful for CI):
mj codegen manifest --exclude-packages @memberjunction --lazy-config ./lazy-config.ts --strictRegenerating the Config
Section titled “Regenerating the Config”The lazy config is regenerated automatically during explorer-core’s prebuild step. You can also run it manually:
# From repo root -- regenerates both the MJExplorer supplemental manifest and the lazy confignpm run mj:manifest:explorer
# Or regenerate ALL manifests (eager + lazy)npm run mj:manifestTroubleshooting
Section titled “Troubleshooting”Component doesn’t load when navigating to its tab
Section titled “Component doesn’t load when navigating to its tab”- Check that the component has
@RegisterClass(SomeBase, 'UniqueKey')with a key argument - Check that the component’s feature module is listed in the package’s
exportsfield inpackage.json - Rebuild the package, then rebuild
explorer-coreto regenerate the lazy config - Verify the compound key (e.g.,
BaseResourceComponent::UniqueKey) appears insrc/generated/lazy-feature-config.ts
Non-Angular class not loading via CreateInstanceAsync
Section titled “Non-Angular class not loading via CreateInstanceAsync”- The class needs
@RegisterClass(BaseClass, 'Key')with both arguments - The class must be exported from a subpath module (not just from the package’s root
public-api.ts) - Rebuild the package so the
.d.tsfiles include the export, then regenerate the lazy config
New package’s components aren’t in the lazy config
Section titled “New package’s components aren’t in the lazy config”The package needs subpath exports in its package.json. Add an exports field with at least one subpath entry pointing to the module’s .d.ts and .js files. See Making a Package Lazy-Loadable.
Coverage audit reports gaps
Section titled “Coverage audit reports gaps”A gap means a @RegisterClass class is in an excluded package but not reachable from any subpath export. Fix by:
- Adding the class to an existing subpath module (import + re-export)
- Creating a new subpath module for it
- Or moving the class to a package that’s in the eager manifest
Changes to lazy config not taking effect
Section titled “Changes to lazy config not taking effect”The lazy config lives in explorer-core, not MJExplorer. After regenerating, you need to rebuild explorer-core (not just MJExplorer) for the changes to compile into the package.