Angular @defer in 2026: Declarative Lazy Loading for Faster Apps

Master Angular @defer blocks for declarative lazy loading. Deep dive into triggers, prefetching, incremental hydration, SSR behavior, and real-world performance patterns.

Angular defer loading and lazy loading performance optimization

Angular @defer blocks solve a problem that router-based lazy loading never could: loading individual components, directives, and pipes on demand without restructuring the application into separate routes. Introduced in Angular 17 and stabilized through Angular 22, @defer turns what used to require dynamic imports, *ngIf flags, and webpack configuration into a single declarative template block.

What @defer does

@defer instructs the Angular compiler to split wrapped components into separate JavaScript chunks, loaded only when a trigger condition fires. The browser downloads less JavaScript upfront, improving Largest Contentful Paint (LCP) and Time to First Byte (TTFB) without manual code-splitting configuration.

How @defer Works Under the Hood

When the Angular compiler encounters a @defer block, it extracts every standalone component, directive, and pipe inside it into a separate chunk. The main bundle ships without these dependencies. At runtime, Angular evaluates the trigger condition and fetches the chunk via a dynamic import() call.

The block supports four sub-blocks that control what the user sees during the loading lifecycle:

app.component.htmltypescript
@defer (on viewport) {
  <analytics-dashboard />
} @placeholder {
  <div class="h-64 bg-muted animate-pulse"></div>
} @loading (minimum 200ms) {
  <loading-spinner />
} @error {
  <p>Failed to load the dashboard. Please refresh.</p>
}

@placeholder renders before the trigger fires. @loading appears while the chunk downloads, with an optional minimum duration to prevent flicker. @error catches network failures or chunk errors. The minimum parameter on @loading avoids a flash of spinner when the chunk loads quickly from cache.

One constraint: every dependency inside @defer must be standalone. Non-standalone components declared in an NgModule cannot be deferred and will be eagerly loaded regardless of the @defer wrapper.

Trigger Types: Controlling When Components Load

Angular provides seven built-in triggers, each targeting a different loading strategy. Multiple triggers can be combined with semicolons and evaluate as OR conditions.

product-page.component.htmltypescript
// Loads when the browser goes idle (default)
@defer {
  <recommendation-engine />
}

// Loads when the element enters the viewport
@defer (on viewport) {
  <customer-reviews [productId]="product.id" />
}

// Loads on click or keydown
@defer (on interaction) {
  <product-configurator />
} @placeholder {
  <button>Configure this product</button>
}

// Loads on mouseenter or focusin
@defer (on hover) {
  <quick-preview />
}

// Loads after 3 seconds
@defer (on timer(3s)) {
  <chat-widget />
}

// Loads immediately after initial render
@defer (on immediate) {
  <notification-center />
}

The on viewport trigger uses the Intersection Observer API internally. The on idle trigger delegates to requestIdleCallback, making it the safest default for below-the-fold content. The on timer trigger accepts durations in milliseconds (500ms) or seconds (3s).

The when Trigger for Reactive Conditions

Beyond event-based triggers, when accepts any boolean expression, including signal reads:

dashboard.component.tstypescript
export class DashboardComponent {
  showAdvanced = signal(false);
}

// dashboard.component.html
@defer (when showAdvanced()) {
  <advanced-analytics />
} @placeholder {
  <button (click)="showAdvanced.set(true)">Show advanced analytics</button>
}

Combining on and when is valid. Angular treats them as OR: the block loads when either condition is met.

Prefetching: Separating Download from Display

Prefetching decouples when the chunk downloads from when the component renders. This eliminates perceived latency by fetching the JavaScript before the user triggers it.

settings.component.htmltypescript
@defer (on interaction; prefetch on idle) {
  <account-settings />
} @placeholder {
  <button>Open settings</button>
}

In this pattern, the browser downloads the account-settings chunk during idle time. When the user clicks, the component renders instantly because the code is already available. Prefetching accepts the same trigger types as the main on clause.

A common pattern for dashboards: prefetch heavy components on idle while deferring render to viewport entry.

analytics.component.htmltypescript
@defer (on viewport; prefetch on idle) {
  <chart-widget [data]="salesData()" />
} @placeholder (minimum 100ms) {
  <div class="h-48 bg-muted rounded-none"></div>
}

The minimum on @placeholder prevents a layout flash when the user scrolls fast and the deferred component loads almost immediately.

Ready to ace your Angular interviews?

Practice with our interactive simulators, flashcards, and technical tests.

@defer and Server-Side Rendering

On the server, @defer blocks render the @placeholder content by default. The deferred component is never server-rendered. This behavior is intentional: the chunk download only makes sense in a browser context.

For SEO-critical content, this means @placeholder must contain meaningful HTML, not just a spinner:

blog-post.component.htmltypescript
@defer (on viewport) {
  <comment-section [postId]="post.id" />
} @placeholder {
  <section aria-label="Comments">
    <h2>Comments</h2>
    <p>Loading comments...</p>
  </section>
}

Search engines index the placeholder content during SSR. A descriptive placeholder ensures the page remains semantically complete.

Incremental Hydration with @defer

Angular 19 introduced incremental hydration in developer preview, and Angular 22 stabilizes it as a production-ready feature. Incremental hydration extends @defer with a hydrate trigger that controls when a server-rendered component becomes interactive on the client.

product-page.component.htmltypescript
@defer (hydrate on viewport) {
  <product-gallery [images]="product.images" />
}

@defer (hydrate on interaction) {
  <product-reviews [productId]="product.id" />
}

Unlike standard @defer, incremental hydration renders the full component HTML on the server. The client receives complete markup but skips hydrating the component until the trigger fires. The result: the browser paints the full page immediately, but JavaScript execution is deferred.

The Angular team reports consistent 40-50% improvements in LCP scores when using incremental hydration on content-heavy pages. Angular DevTools in version 22 visualizes hydration states (pending, hydrated, error) directly in the component inspector.

Real-World Patterns and Performance Strategy

Pattern 1: Heavy Dashboard with Multiple Deferred Panels

dashboard.component.htmltypescript
<div class="grid grid-cols-2 gap-4">
  @defer (on viewport; prefetch on idle) {
    <sales-chart />
  } @placeholder {
    <skeleton-card height="300px" />
  }

  @defer (on viewport; prefetch on idle) {
    <user-activity-feed />
  } @placeholder {
    <skeleton-card height="300px" />
  }

  @defer (on interaction) {
    <export-panel />
  } @placeholder {
    <button>Export data</button>
  }
</div>

Each panel loads independently. The export panel stays unloaded until the user explicitly requests it, saving a full chunk download for users who never export.

Pattern 2: Conditional Feature Loading with Signals

editor.component.tstypescript
export class EditorComponent {
  user = inject(UserService).currentUser;
  isPremium = computed(() => this.user()?.plan === 'premium');
}

// editor.component.html
@defer (when isPremium()) {
  <ai-assistant />
} @placeholder {
  <upgrade-banner />
}

Free-tier users never download the AI assistant chunk. The upgrade banner serves as both placeholder and conversion prompt.

Avoiding Common Pitfalls

Nesting @defer blocks with the same trigger causes simultaneous chunk downloads. Stagger triggers across nested blocks:

typescript
// Avoid: both fire on idle simultaneously
@defer {
  @defer {
    <nested-heavy-component />
  }
}

// Better: outer on idle, inner on viewport
@defer (on idle) {
  <wrapper-component />
}
// Inside wrapper-component template:
@defer (on viewport) {
  <nested-heavy-component />
}

Another pitfall: using @defer for components already in the critical rendering path. Deferring above-the-fold content increases LCP because the browser must download and execute the chunk before painting. Reserve @defer for below-the-fold or user-triggered content.

@defer vs Router-Based Lazy Loading

Router-based lazy loading and @defer complement each other. Router lazy loading splits at the route level, deferring entire feature modules. @defer splits within a template, deferring individual components.

| Aspect | Router Lazy Loading | @defer | |--------|-------------------|--------| | Granularity | Route level | Component level | | Trigger | Navigation event | viewport, idle, interaction, hover, timer, condition | | SSR behavior | Full server render | Placeholder on server | | Requires standalone | No (works with NgModules) | Yes (standalone only) | | Prefetching | preloadingStrategy | prefetch on clause | | Use case | Feature modules, pages | Widgets, panels, conditional UI |

A typical application uses both: router lazy loading for top-level feature areas and @defer for heavy components within those areas.

Interview Questions on Angular @defer

Technical interviews increasingly cover @defer as it becomes central to Angular performance strategy. Here are patterns that commonly appear in Angular interview questions.

Q: What happens if a non-standalone component is placed inside @defer? The component loads eagerly regardless of the @defer block. The Angular compiler cannot extract NgModule-declared components into separate chunks. No error is thrown, but the optimization silently fails.

Q: How does @defer behave during SSR? The server renders the @placeholder content. The deferred component is never rendered server-side. Hydration occurs on the client when the trigger fires (or via hydrate on for incremental hydration).

Q: Can @defer be used with OnPush change detection? Yes. @defer is a template-level feature and works with any change detection strategy. The deferred component follows its own change detection configuration once loaded.

Q: What is the difference between on idle and on immediate? on idle waits for the browser to finish all pending work via requestIdleCallback. on immediate triggers right after Angular completes the initial render pass, without waiting for idle state. on immediate is useful for components that should load soon but not block the first paint.

For a broader set of Angular interview preparation, see the Angular change detection module and the Angular signals module.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Conclusion

  • @defer splits standalone components into separate chunks loaded on demand, reducing initial bundle size without route restructuring
  • Seven trigger types (idle, viewport, interaction, hover, timer, immediate, when) cover every loading strategy from passive to user-driven
  • prefetch on decouples chunk download from component rendering, eliminating perceived latency on user-triggered components
  • Incremental hydration (hydrate on) renders full HTML on the server while deferring JavaScript execution to the client, delivering 40-50% LCP improvements on content-heavy pages
  • @placeholder content renders during SSR and must be semantically meaningful for SEO
  • @defer complements router-based lazy loading: routes for feature-level splitting, @defer for component-level splitting within templates
  • Non-standalone components cannot be deferred and will silently load eagerly

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#angular
#angular-defer
#lazy-loading
#performance
#ssr
#signals
#deep-dive

Share

Related articles