Skip to main content

Deferrable Views (@defer)

Lazy load components using Angular's built-in @defer block.

Basic @defer

@Component({
template: `
@defer {
<app-comments [albumId]="albumId"></app-comments>
} @placeholder {
<p>Click to load comments</p>
}
`
})
export class DetailComponent {
albumId = input.required<number>();
}

@defer Triggers

on interaction

Load when user interacts with the trigger element:

@defer (on interaction) {
<app-comments [albumId]="albumId"></app-comments>
} @placeholder {
<a>Load comments</a>
}

on viewport

Load when element enters viewport:

@defer (on viewport) {
<app-heavy-chart [data]="chartData"></app-heavy-chart>
} @placeholder {
<div style="height: 400px">Chart will load when visible</div>
}

on idle

Load when browser is idle:

@defer (on idle) {
<app-analytics-widget></app-analytics-widget>
} @placeholder {
<div>Loading analytics...</div>
}

on immediate

Load immediately (useful for testing):

@defer (on immediate) {
<app-widget></app-widget>
}

on timer

Load after specified milliseconds:

@defer (on timer(5000)) {
<app-ads></app-ads>
} @placeholder {
<div>Ad loads in 5 seconds</div>
}

on hover

Load when user hovers over trigger:

@defer (on hover) {
<app-tooltip [content]="tooltipText"></app-tooltip>
} @placeholder {
<span>Hover for more info</span>
}

@loading Block

Show content while lazy chunk loads:

@defer (on interaction) {
<app-comments></app-comments>
} @loading {
<p>Loading comments...</p>
} @placeholder {
<a>Load comments</a>
}

minimum and after

Control when loading state shows:

@defer (on interaction) {
<app-comments></app-comments>
} @loading (after 100ms; minimum 1s) {
<p>Loading...</p>
} @placeholder {
<a>Load comments</a>
}
  • after - Delay before showing loading state
  • minimum - Minimum time to show loading state (prevents flash)

@error Block

Handle loading failures:

@defer (on viewport) {
<app-comments></app-comments>
} @error {
<p>Failed to load comments. Please try again.</p>
} @placeholder {
<div>Comments section</div>
}

@placeholder Block

Content shown before deferral triggers:

@defer (on viewport) {
<app-heavy-component></app-heavy-component>
} @placeholder (minimum 500ms) {
<div class="skeleton">Loading...</div>
}

Multiple Triggers

Combine multiple trigger conditions:

@defer (on interaction; on timer(5000)) {
<app-widget></app-widget>
} @placeholder {
<div>Widget loads on click or after 5s</div>
}

Prefetching

Prefetch lazy chunk before showing:

@defer (on interaction; prefetch on idle) {
<app-comments></app-comments>
} @placeholder {
<a>Load comments</a>
}

Named Triggers

Use specific elements as triggers:

<button #loadBtn>Load Comments</button>

@defer (on interaction(loadBtn)) {
<app-comments></app-comments>
} @placeholder {
<p>Click button above to load</p>
}

Complete Example

@Component({
template: `
<div class="album-detail">
<h1>{{ album.name }}</h1>
<p>{{ album.description }}</p>

@defer (on interaction; prefetch on idle) {
<app-comments [albumId]="album.id"></app-comments>
} @loading (after 200ms; minimum 1s) {
<div class="loading-spinner">
<p>Loading comments...</p>
</div>
} @placeholder {
<a class="load-link">Load comments</a>
} @error {
<p class="error">Failed to load comments.</p>
}
</div>
`
})
export class AlbumDetailComponent {
album = input.required<Album>();
}

Nested @defer

@defer (on viewport) {
<app-section>
@defer (on interaction) {
<app-nested-component></app-nested-component>
} @placeholder {
<button>Load more</button>
}
</app-section>
} @placeholder {
<div>Section placeholder</div>
}

Testing Deferrable Views

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';

describe('DeferrableViews', () => {
it('should render placeholder initially', () => {
@Component({
template: `
@defer {
<app-comments></app-comments>
} @placeholder {
<p>Placeholder</p>
}
`
})
class TestComponent {}

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

expect(fixture.nativeElement.textContent).toContain('Placeholder');
});
});

When to Use @defer

Good use cases:

  • Heavy components (charts, editors)
  • Below-the-fold content
  • Components with large dependencies
  • Modal/dialog content
  • Comments sections
  • Admin features

Avoid for:

  • Critical above-the-fold content
  • Small components
  • Components needed immediately
  • Navigation elements

Performance Benefits

// ❌ Bad: Eagerly loads heavy component
@Component({
template: `
@if (showComments()) {
<app-comments></app-comments>
}
`
})

// ✅ Good: Lazy loads on demand
@Component({
template: `
@defer (on interaction) {
<app-comments></app-comments>
} @placeholder {
<a>Load comments</a>
}
`
})

Best Practices

  • Use @loading with after to avoid loading flashes
  • Use minimum to prevent loading flash on fast connections
  • Prefetch on idle for better UX
  • Use @error for robust error handling
  • Combine triggers for flexibility
  • Use @placeholder (minimum) to prevent layout shifts
  • Defer heavy third-party components
  • Test deferrable views in slow network conditions

Bundle Size Impact

# Before @defer
main.js: 500kb (includes CommentsComponent)

# After @defer
main.js: 350kb
lazy-comments.js: 150kb (loaded on demand)