lacolaco's marginalia

Angular v22: Lazy Loading Business Logic with injectAsync

The injectAsync function added in Angular v22 provides a new option for improving loading speeds. I’ll introduce this new API while reviewing the lazy loading APIs that have existed until now.

injectAsync

The inject function used for dependency injection creates a static reference to the target being injected. Therefore, if the dependent side is included in the initial load JS, the side being depended upon will also increase the bundle size. In the following example, the ReportView component and the ReportExporter service are statically coupled, and their loading timing cannot be separated.

// Static import
import { ReportExporter } from './exporter';

@Component({...})
export class ReportView {
  // ReportView statically references ReportExporter
  exporter = inject(ReportExporter);
  
  export() {
    this.exporter.export();
  }
}

If ReportExporter is not a feature that is always used, we might want to keep only ReportView in the initial load and delay ReportExporter so it’s only loaded when the export button is pressed. That’s what injectAsync achieves. This function asynchronizes the injection of a specific service, allowing for bundle splitting and making the module lazy-loadable.

Following the example below, replace the reference to the ReportExporter service from a static import to a dynamic import in the argument of the injectAsync function. By doing this, the this.exporter field becomes a function that returns a reference to the service asynchronously when called. By await-ing the return value, you can use the service just as before.

@Component({...})
export class ReportView {
  // Dynamic import
  // ReportView dynamically references ReportExporter
  exporter = injectAsync(() => import('./exporter').then(m => m.ReportExporter));
  
  async export() {
    const exporter = await this.exporter();
    exporter.export();
  }
}

Usage Constraints

Only services declared with @Injectable({ providedIn: 'root' }) or the @Service() decorator can use injectAsync. Because they are integrated into the dependency injection system dynamically as modules are loaded, complex provider definitions are not supported.

Lazy Loading Methods and Use Cases

As in the example above, if a portion of the features provided by an application is infrequently used, it’s worth considering lazy loading to avoid compromising the experience for users who don’t use those features. Angular provides several lazy loading APIs, but how to choose between them depends on the application domain—specifically, what granularity and at what boundaries the application’s features can be divided.

Lazy Loading via Routes

The largest granularity for lazy loading is splitting bundles by the Router’s page loading unit (route). When you want to lazy load pages under a certain path together using parent-child route relationships, you can use loadChildren; when you want to lazy load a specific route, you can use loadComponent.

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

export const routes: Routes = [
  {
    path: 'login',
    // Lazy load the login page
    loadComponent: () => import('./components/auth/login-page'),
  },
  {
    path: 'admin',
    // Lazy load the admin pages
    loadComponent: () => import('./admin/admin.component'),
    loadChildren: () => import('./admin/admin.routes'),
  },
];

In cases where accessible pages are clearly separated, such as by user permissions, introducing route-based lazy loading would likely be effective.

Lazy Loading Partial Views

Using the @defer block, parts of a component view can be made lazy-loadable. The view is loaded and initialized only when that area enters the viewport or when the user performs an action.

@defer {
  <large-component />
} @placeholder {
  <p>プレースホルダーコンテンツ</p>
}

This is an option when a specific component within a descendant view has a large impact on bundle size, and its display speed is not mission-critical. For example, calendar widgets or complex forms come to mind. The comment section of a blog post that isn’t part of the initial view would also be a prime candidate for this.

In cases where the frequency and timing of feature use vary depending on the screen area, lazy loading views with @defer would work effectively.

Lazy Loading Services

When a part of the logic, rather than the view, is deferrable, injectAsync, which I’ve introduced here, is the choice. If logic that uses a large third-party library is not needed for the initial load and its loading can be triggered when needed, that’s where injectAsync comes in.

Lazy Loading Modules

Finally, it’s a method often forgotten because it’s not an Angular API, but lazy loading at the module level can be achieved simply by using standard ES Module dynamic imports (import(...)). This is because dynamic imports are converted into lazy loading within the Angular CLI build process. If the target to be loaded is just a function, class, or constant, it doesn’t involve Angular’s dependency injection or component system, so using plain dynamic imports is sufficient.

You could split by the service that uses it—such as a library used only in specific use cases—or you could reference the service statically and then split by module inside it. This can be decided based on implementation preference. The only difference is whether the responsibility for asynchronous processing lies outside or inside the service. Personally, I think if injectAsync can cover it, that’s fine, but when complex DI provider definitions are required, I wonder if it might be better to combine it with module-level lazy loading as appropriate.

Summary

  • injectAsync enables bundle splitting and lazy loading at the service level by replacing DI references with dynamic imports.
  • It is limited to services declared with @Injectable({ providedIn: 'root' }) or @Service(), and does not support complex provider definitions.
  • Lazy loading methods and units provided by Angular:
    • Routes: loadChildren / loadComponent
    • Partial Views: @defer
    • Services: injectAsync
    • Other modules: import(...)