TL;DR: Angular’s @defer directive delays non-critical UI rendering, cutting initial load time by up to 97% in real apps. Use it to lazy load heavy components, reduce Time to Interactive (TTI), and improve Core Web Vitals. Includes real benchmarks and production-ready examples.
Introduction
Ever loaded up an Angular app and watched that progress bar crawl while your users bail? Yeah, me too. That’s why I got so excited when the Angular team released the @defer
feature. It’s a game-changer for those of us battling slow initial load times.
I’ve spent the last few months putting this feature through its paces in production apps, and the results have been dramatic enough that I decided to build a proper benchmark to show exactly how much difference it can make. In this post, I’ll walk you through how @defer
works under the hood and share my real-world testing results that might convince you to refactor some of your templates.
What is Angular’s @defer?
Think of @defer
as a bouncer for your Angular templates. It holds back heavy components until they’re actually needed, rather than making users download and process everything upfront. Introduced in Angular 17, it’s a simple template feature that packs a serious performance punch.
The Angular docs describe it as a way to “lazy load parts of your application only when they’re needed in the browser,” which is accurate but doesn’t quite capture how transformative it can be for real-world apps.
Here’s why it matters:
First, it slashes initial load times by keeping non-critical components out of the critical rendering path. Your users see and can interact with the important stuff way sooner.
Second, it shrinks your initial bundle size - you’re only sending what’s immediately necessary, not the kitchen sink.
Third, it dramatically improves perceived performance - the stuff users care about appears faster, making your app feel snappier.
Finally, it gives you fine-grained control over exactly when content loads through various triggers, which I’ll get into shortly.
How @defer Works
Using @defer
is dead simple, you just wrap the component you want to lazy-load:
@defer {
<heavy-component></heavy-component>
}
When Angular sees this in your template, it does something pretty clever:
First, it skips rendering that component entirely during initial page load.
Then, it can show a lightweight placeholder instead (if you want one).
Next, it waits for a specific trigger (which you can control) before loading the component.
Finally, when the component is ready, it swaps out the placeholder and renders the real thing.
The magic is that your page becomes interactive much faster because it’s not bogged down trying to load everything at once.
Loading Triggers
Here’s where things get fun. You can control exactly when your deferred content loads using these triggers:
on viewport: This is the default, and it makes perfect sense, why load something until the user actually scrolls to it? Great for content further down the page.
on idle: This is super clever. Let Angular wait until the browser isn’t busy, then load your component. Perfect for “would be nice to have ready” components that aren’t urgent.
on immediate: “I want it deferred, but as soon as possible after the initial render.” It’s still deferred, but jumps to the front of the line after the page loads.
on timer(time): Need something to appear after a specific delay? @defer (on timer(3000))
will load it after 3 seconds. Nice for timed reveals or notifications.
on interaction(selector): My go-to for modals and dialogs. Why load a heavy settings panel before the user clicks “Settings”?
on hover(selector): The sneaky performance trick I love. When a user hovers over a button, start loading the content they’ll see when they click it. By the time they actually click, it’s often already loaded!
Example
@defer (on viewport) {
<app-heavy-component></app-heavy-component>
} @placeholder {
<div class="loading-placeholder">Loading content...</div>
} @loading {
<div class="spinner"></div>
} @error {
<div class="error-message">Failed to load content</div>
}
Managing Different Stages of Deferred Loading
The @defer
directive provides several blocks to manage different loading states:
@placeholder
@defer {
<heavy-component></heavy-component>
} @placeholder {
<div>Content is coming soon...</div>
}
This is what shows up before your deferred component starts loading. Think of it as a “Coming Soon” sign that helps prevent layout shifts and gives users visual cues about what’s loading.
@loading
@defer {
<heavy-component></heavy-component>
} @loading {
<div>Loading...</div>
}
Once your trigger fires and loading actually begins, the @loading
block takes over. This is your “Work in Progress” indicator that tells users “I heard your click/scroll, and I’m working on it!”
@error
@defer {
<heavy-component></heavy-component>
} @error {
<div>Failed to load component</div>
}
Networks fail. Servers timeout. When that happens, the @error
block gives users feedback instead of just silently failing. Always include error states for a polished app.
Timing Control
Here’s a neat trick I use all the time:
@defer {
<heavy-component></heavy-component>
} @placeholder (minimum 500ms) {
<div>Placeholder shown for at least 500ms</div>
} @loading (minimum 300ms) {
<div>Loading shown for at least 300ms</div>
}
Those minimum
parameters prevent jarring flickers if content loads too quickly. I’ve found that setting minimums creates a more consistent, polished feel, even if it technically makes some content appear slightly later.
So What Makes @defer So Cool?
Real Performance Benefits
Instant UI: Push the heavy stuff aside and watch your main screen pop up in a snap. We’re talking 97% faster in our tests, that’s the difference between a 3-second wait and seeing your app almost instantly!
Browser Resource Magic: Instead of choking your browser with everything at once, it can focus on showing users what they need right now. Everything else can wait its turn.
No More Dead Clicks: Ever rage-clicked something because nothing happened? That’s what your users do when the UI freezes during loading. @defer keeps things responsive so people can actually use your app.
It’s All About Feel: Nobody times your app with a stopwatch, they just know if it feels fast or slow. @defer makes your app feel quick even if the total load time is the same.
Better For Your Users
Smooth Content Flow: Remember how websites used to slam everything onto the screen at once? With @defer, content arrives naturally as you need it, like a conversation instead of someone shouting everything at you.
The Good Stuff Now: Show users what they came for first. Nobody wants to wait for three carousels, a weather widget, and your newsletter signup before seeing the actual content.
Stable Layout: Ever tried reading something when suddenly everything jumps because an image loaded? With proper placeholders, that’s history. Users can actually focus on your content.
Works on Crappy Connections: Not everyone has fiber internet. For folks on slow connections or old phones, @defer is the difference between using your app and giving up on it.
Benchmark Implementation
To quantify the performance benefits of @defer
, we’ve created a comprehensive benchmark that compares Angular applications with and without this feature. Let’s examine how we built this benchmark and the results we observed.
Benchmark Components
Our benchmark implementation consists of the following key components:
- Heavy Component: Simulates expensive operations to represent real-world complex components
- Performance Service: Tracks and records load times for both pages and individual components
- Test Pages: One page loads components synchronously, the other uses
@defer
- Benchmark Dashboard: Visualizes and compares results
Heavy Component Implementation
The heavy component simulates CPU-intensive work that can’t be easily optimized by the JavaScript runtime:
@Component({
selector: 'app-heavy-component',
standalone: true,
imports: [CommonModule],
template: `
<div class="heavy-component">
<h2>{{ title }}</h2>
<p>Component ID: {{ componentId }}</p>
<p>Load time: {{ loadTime }}ms</p>
<div class="content">
<div *ngFor="let item of items" class="item">
<div class="item-header">
<h3>{{ item.name }}</h3>
<span class="item-value">{{ item.value }}</span>
</div>
<div class="item-details">
<p>{{ item.description }}</p>
<div class="sub-items">
<div *ngFor="let subItem of item.subItems" class="sub-item">
<span>{{ subItem.label }}: {{ subItem.value }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
`,
styles: [
`
.heavy-component {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.content {
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
}
.item-header {
display: flex;
justify-content: space-between;
}
.item-value {
font-weight: bold;
color: #0066cc;
}
.sub-items {
margin-left: 15px;
padding-top: 5px;
}
`,
],
})
export class HeavyComponentComponent implements OnInit {
@Input() title: string = 'Heavy Component';
@Input() componentId: string = '';
items: any[] = [];
loadTime: number = 0;
calculationResult: number = 0;
constructor(private performanceService: PerformanceService) {}
ngOnInit() {
const startTime = performance.now();
// Generate complex data
this.generateItems(100);
// Simulate heavy calculations
this.performHeavyCalculations();
const endTime = performance.now();
this.loadTime = Math.round(endTime - startTime);
// Record component load time in the performance service
if (this.componentId) {
this.performanceService.recordMetric(this.componentId, this.loadTime);
}
}
private generateItems(count: number): void {
for (let i = 0; i < count; i++) {
const subItems = [];
for (let j = 0; j < 5; j++) {
subItems.push({
label: `Property ${j + 1}`,
value: Math.random() * 100,
});
}
this.items.push({
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
description: `This is a description for item ${
i + 1
} with some additional text to make it longer.`,
subItems: subItems,
});
}
}
private performHeavyCalculations(): void {
// This simulation creates a calculation that can't be easily optimized away
let result = 0;
let previous = 1;
for (let i = 0; i < 5000000; i++) {
// Using modulo and conditional logic to prevent dead code elimination
if (i % 2 === 0) {
result += Math.sqrt(i) * Math.cos(i / 1000);
} else {
result += Math.sin(i / 1000) * previous;
}
// Create dependencies between iterations
previous = result % 10;
}
// Store the result so it's not optimized away
this.calculationResult = result;
}
}
Performance Service
The PerformanceService
tracks and records metrics for both page and component loading:
@Injectable({
providedIn: 'root',
})
export class PerformanceService {
private metricsSubject = new BehaviorSubject<PerformanceMetric[]>([]);
metrics$ = this.metricsSubject.asObservable();
private metrics: PerformanceMetric[] = [];
startPageLoadMetric(name: string): void {
const startTime = performance.now();
const index = this.metrics.findIndex((m) => m.name === name);
if (index !== -1) {
this.metrics[index].startTime = startTime;
} else {
this.metrics.push({
name,
startTime,
endTime: 0,
duration: 0,
});
}
}
endPageLoadMetric(name: string): void {
const endTime = performance.now();
const index = this.metrics.findIndex((m) => m.name === name);
if (index !== -1) {
this.metrics[index].endTime = endTime;
this.metrics[index].duration = endTime - this.metrics[index].startTime;
this.metricsSubject.next([...this.metrics]);
}
}
recordMetric(name: string, duration: number): void {
const index = this.metrics.findIndex((m) => m.name === name);
if (index !== -1) {
this.metrics[index].duration = duration;
} else {
this.metrics.push({
name,
startTime: 0,
endTime: 0,
duration,
});
}
this.metricsSubject.next([...this.metrics]);
}
clearMetrics(): void {
this.metrics = [];
this.metricsSubject.next([]);
}
}
export interface PerformanceMetric {
name: string;
startTime: number;
endTime: number;
duration: number;
}
Test Pages Implementation
Without @defer Page
@Component({
selector: 'app-without-defer',
standalone: true,
imports: [CommonModule, HeavyComponentComponent],
template: `
<div class="page-container">
<h1>Page Without @defer</h1>
<p class="description">
This page loads all components immediately (synchronously). The page
isn't considered loaded until all components are fully rendered.
</p>
<div class="components-container">
<app-heavy-component
title="Heavy Component 1"
componentId="component_without_defer_1">
</app-heavy-component>
<app-heavy-component
title="Heavy Component 2"
componentId="component_without_defer_2">
</app-heavy-component>
<app-heavy-component
title="Heavy Component 3"
componentId="component_without_defer_3">
</app-heavy-component>
</div>
<div class="navigation">
<a routerLink="/benchmark">Back to Benchmark</a>
</div>
</div>
`,
styles: [
`
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.description {
margin-bottom: 30px;
color: #666;
line-height: 1.5;
}
.components-container {
margin-bottom: 30px;
}
.navigation {
margin-top: 20px;
}
.navigation a {
color: #0077cc;
text-decoration: none;
}
.navigation a:hover {
text-decoration: underline;
}
`,
],
})
export class WithoutDeferComponent implements OnInit, OnDestroy {
constructor(private performanceService: PerformanceService) {}
ngOnInit() {
// Start timing when the component initializes
this.performanceService.startPageLoadMetric('withoutDefer');
// Use setTimeout to allow the components to render
setTimeout(() => {
this.performanceService.endPageLoadMetric('withoutDefer');
}, 0);
}
ngOnDestroy() {
// Ensure timing is ended if the user navigates away early
this.performanceService.endPageLoadMetric('withoutDefer');
}
}
With @defer Page
@Component({
selector: 'app-with-defer',
standalone: true,
imports: [CommonModule, HeavyComponentComponent],
template: `
<div class="page-container">
<h1>Page With @defer</h1>
<p class="description">
This page uses the @defer directive to load components
asynchronously. The page is considered loaded once the initial structure
renders, even before the heavy components are loaded.
</p>
<div class="components-container">
@defer {
<app-heavy-component
title="Heavy Component 1"
componentId="component_with_defer_1">
</app-heavy-component>
} @placeholder {
<div class="placeholder">
<h2>Heavy Component 1</h2>
<p>Loading...</p>
</div>
} @defer {
<app-heavy-component
title="Heavy Component 2"
componentId="component_with_defer_2">
</app-heavy-component>
} @placeholder {
<div class="placeholder">
<h2>Heavy Component 2</h2>
<p>Loading...</p>
</div>
} @defer {
<app-heavy-component
title="Heavy Component 3"
componentId="component_with_defer_3">
</app-heavy-component>
} @placeholder {
<div class="placeholder">
<h2>Heavy Component 3</h2>
<p>Loading...</p>
</div>
}
</div>
<div class="navigation">
<a routerLink="/benchmark">Back to Benchmark</a>
</div>
</div>
`,
styles: [
`
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.description {
margin-bottom: 30px;
color: #666;
line-height: 1.5;
}
.components-container {
margin-bottom: 30px;
}
.navigation {
margin-top: 20px;
}
.navigation a {
color: #0077cc;
text-decoration: none;
}
.navigation a:hover {
text-decoration: underline;
}
.placeholder {
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 20px;
background-color: #f9f9f9;
min-height: 150px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #666;
}
`,
],
})
export class WithDeferComponent implements OnInit, OnDestroy {
constructor(private performanceService: PerformanceService) {}
ngOnInit() {
// Start timing when the component initializes
this.performanceService.startPageLoadMetric('withDefer');
// Use setTimeout to allow initial rendering to complete
setTimeout(() => {
this.performanceService.endPageLoadMetric('withDefer');
}, 0);
}
ngOnDestroy() {
// Ensure timing is ended if the user navigates away early
this.performanceService.endPageLoadMetric('withDefer');
}
}
Benchmark Dashboard Component
The benchmark dashboard provides an interactive UI for testing and visualizing the performance difference:
@Component({
selector: 'app-benchmark',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<div class="container">
<h1>@defer Performance Benchmark</h1>
<p class="description">
This benchmark compares page load performance with and without the
@defer feature in Angular. @defer allows components to be loaded
asynchronously, improving initial page load time.
</p>
<div class="controls">
<button class="btn primary" (click)="runBenchmark()">
Run Benchmark
</button>
<button class="btn secondary" (click)="resetResults()">
Reset Results
</button>
</div>
<div *ngIf="benchmarkRunning" class="running-indicator">
<p>Running benchmark... please wait.</p>
<div class="progress-bar">
<div class="progress" [style.width.%]="progress"></div>
</div>
</div>
<div *ngIf="results.length > 0" class="results">
<h2>Benchmark Results</h2>
<div class="comparison-chart">
<div class="chart-bars">
<div class="bar-group">
<div class="bar-label">Without @defer</div>
<div
class="bar without-defer"
[style.width.%]="getBarWidth('withoutDefer')">
{{ getMetricDuration('withoutDefer') }}ms
</div>
</div>
<div class="bar-group">
<div class="bar-label">With @defer</div>
<div
class="bar with-defer"
[style.width.%]="getBarWidth('withDefer')">
{{ getMetricDuration('withDefer') }}ms
</div>
</div>
</div>
</div>
<div class="metrics-table">
<h3>Detailed Metrics</h3>
<table>
<thead>
<tr>
<th>Test</th>
<th>Load Time</th>
<th>Improvement</th>
</tr>
</thead>
<tbody>
<tr class="total-row">
<td>Without @defer (Page Load)</td>
<td>{{ getMetricDuration('withoutDefer') }}ms</td>
<td>Baseline</td>
</tr>
<tr *ngFor="let i of [1, 2, 3]">
<td class="component-row">Component {{ i }}</td>
<td>
{{ getMetricDuration('component_without_defer_' + i) }}ms
</td>
<td>-</td>
</tr>
<tr class="total-row">
<td>With @defer (Page Load)</td>
<td>{{ getMetricDuration('withDefer') }}ms</td>
<td>{{ getImprovement() }}</td>
</tr>
<tr *ngFor="let i of [1, 2, 3]">
<td class="component-row">Component {{ i }}</td>
<td>{{ getMetricDuration('component_with_defer_' + i) }}ms</td>
<td>{{ getComponentImprovement(i) }}</td>
</tr>
</tbody>
</table>
<div class="markdown-table-container">
<h4>Markdown Table Output</h4>
<p>This is how these metrics would appear in a Markdown format:</p>
<pre class="markdown-table">{{ generateMarkdownTable() }}</pre>
<div class="metrics-explanation">
<p>
<strong>Page Load</strong>: Time until the page becomes
interactive to the user
</p>
<p>
<strong>Component</strong>: Individual rendering time for each
component
</p>
<p>
<em
>Note: With @defer, components load asynchronously after
the initial page load</em
>
</p>
</div>
</div>
</div>
<div class="conclusion">
<h3>Conclusion</h3>
<p>
{{ generateConclusion() }}
</p>
<p>
<strong>Key Takeaway:</strong> Using @defer can significantly
improve perceived page load performance by deferring non-critical
content loading until it's needed, allowing the main page shell to
render faster.
</p>
</div>
</div>
<div class="navigation">
<a routerLink="/without-defer" class="nav-link"
>View Page Without @defer</a
>
<a routerLink="/with-defer" class="nav-link"
>View Page With @defer</a
>
</div>
</div>
`,
styles: [
/* CSS styles omitted for brevity */
],
})
export class BenchmarkComponent implements OnInit {
results: PerformanceMetric[] = [];
benchmarkRunning = false;
progress = 0;
constructor(
private performanceService: PerformanceService,
private router: Router
) {}
ngOnInit() {
this.performanceService.metrics$.subscribe((metrics) => {
this.results = metrics;
});
}
runBenchmark() {
this.resetResults();
this.benchmarkRunning = true;
// First stage - without defer (progress 0-50%)
this.updateProgress(0);
setTimeout(() => {
this.router.navigate(['/without-defer']);
this.updateProgress(25);
// Wait for without-defer to complete loading
setTimeout(() => {
this.updateProgress(50);
// Second stage - with defer (progress 50-100%)
setTimeout(() => {
this.router.navigate(['/with-defer']);
this.updateProgress(75);
// Wait for with-defer to complete loading
setTimeout(() => {
this.updateProgress(100);
this.benchmarkRunning = false;
this.router.navigate(['/benchmark']);
}, 2000);
}, 1000);
}, 2000);
}, 500);
}
resetResults() {
this.performanceService.clearMetrics();
}
updateProgress(value: number) {
this.progress = value;
}
getMetricDuration(name: string): number {
const metric = this.results.find((m) => m.name === name);
return metric ? Math.round(metric.duration) : 0;
}
getBarWidth(name: string): number {
const withoutDeferTime = this.getMetricDuration('withoutDefer');
const metricTime = this.getMetricDuration(name);
if (withoutDeferTime === 0 || metricTime === 0) {
return 0;
}
// Scale the bar width relative to the maximum time (withoutDefer)
const maxTime = Math.max(...this.results.map((m) => m.duration));
return (metricTime / maxTime) * 100;
}
getImprovement(): string {
const withoutDeferTime = this.getMetricDuration('withoutDefer');
const withDeferTime = this.getMetricDuration('withDefer');
if (withoutDeferTime === 0 || withDeferTime === 0) {
return 'N/A';
}
const difference = withoutDeferTime - withDeferTime;
const percentageImprovement = (difference / withoutDeferTime) * 100;
return `${Math.round(percentageImprovement)}% faster (${difference}ms)`;
}
getComponentImprovement(componentNumber: number): string {
const withoutDeferTime = this.getMetricDuration(
`component_without_defer_${componentNumber}`
);
const withDeferTime = this.getMetricDuration(
`component_with_defer_${componentNumber}`
);
if (withoutDeferTime === 0 || withDeferTime === 0) {
return 'N/A';
}
const difference = withoutDeferTime - withDeferTime;
const percentageImprovement = (difference / withoutDeferTime) * 100;
if (percentageImprovement < 0) {
return `${Math.abs(
Math.round(percentageImprovement)
)}% slower (${Math.abs(difference)}ms)`;
}
return `${Math.round(percentageImprovement)}% faster (${difference}ms)`;
}
generateConclusion(): string {
const withoutDeferTime = this.getMetricDuration('withoutDefer');
const withDeferTime = this.getMetricDuration('withDefer');
if (withoutDeferTime === 0 || withDeferTime === 0) {
return 'Not enough data to generate a conclusion. Run the benchmark first.';
}
if (withDeferTime < withoutDeferTime) {
const difference = withoutDeferTime - withDeferTime;
const percentageImprovement = (difference / withoutDeferTime) * 100;
return `Using @defer improved initial page load time by ${Math.round(
percentageImprovement
)}% (${difference}ms faster). This demonstrates how deferring non-critical components can significantly enhance the user experience by making the page interactive sooner.`;
} else {
return `In this specific benchmark, using @defer did not show a significant improvement. This can happen in simple applications or when the deferred components are immediately visible in the viewport. In more complex real-world applications with heavy components, the benefits would likely be more pronounced.`;
}
}
generateMarkdownTable(): string {
if (this.results.length === 0) {
return 'No benchmark data available. Please run the benchmark first.';
}
const withoutDeferTotal = this.getMetricDuration('withoutDefer');
const withDeferTotal = this.getMetricDuration('withDefer');
const improvement = this.getImprovement();
// Generate the markdown table
let table = '| Test | Load Time | Improvement |\n';
table += '| :--- | ---: | :---: |\n';
// Without @defer section
table += `| **Without @defer (Page Load)** | **${withoutDeferTotal}ms** | **Baseline** |\n`;
// Add individual component rows for without @defer
for (let i = 1; i <= 3; i++) {
const componentTime = this.getMetricDuration(
`component_without_defer_${i}`
);
table += `| Component ${i} | ${componentTime}ms | - |\n`;
}
// Add separator row
table += '| | | |\n';
// With @defer section
table += `| **With @defer (Page Load)** | **${withDeferTotal}ms** | **${improvement}** |\n`;
// Add individual component rows for with @defer
for (let i = 1; i <= 3; i++) {
const componentTime = this.getMetricDuration(`component_with_defer_${i}`);
const componentImprovement = this.getComponentImprovement(i);
table += `| Component ${i} | ${componentTime}ms | ${componentImprovement} |\n`;
}
return table;
}
}
Benchmark Results and Analysis
OK, let’s get to the juicy part. Here’s what our benchmark testing revealed:
Test | Load Time | Improvement |
---|---|---|
Without @defer (Page Load) | 319ms | Baseline |
Component 1 | 122ms | - |
Component 2 | 82ms | - |
Component 3 | 86ms | - |
With @defer (Page Load) | 8ms | 97% faster (311ms) |
Component 1 | 126ms | 3% slower (4ms) |
Component 2 | 87ms | 6% slower (5ms) |
Component 3 | 82ms | 5% faster (4ms) |
What’s Going On Here?
Holy smokes, that’s fast: The initial page load time dropped from 319ms to just 8ms with
@defer
, that’s a 97% improvement! Not a typo, folks.Component loads aren’t actually faster: You’ll notice the individual component times stayed pretty much the same between approaches. That makes sense, these components still have to do the same amount of work.
Some minor variations: Components 1 and 2 were actually a touch slower with
@defer
, while Component 3 was slightly faster. This is just noise from:- Normal performance fluctuations between test runs
- Tiny overhead from the
@defer
mechanism itself - Different browser optimizations when loading stuff in different ways
The big takeaway: Your page becomes interactive a whopping 311ms sooner with
@defer
. Users can start clicking and scrolling while the heavy components are still loading in the background.
So What’s Really Happening Here?
Your UI Finally Stops Blocking
The big win our benchmark shows is that @defer
lets your page become interactive without making users wait for every single component to finish loading. Let me break down what usually happens without @defer
:
- Browser has to parse ALL your components
- Execute ALL that JavaScript
- Render ALL those DOM elements
- Calculate ALL those styles
- And only THEN can the user actually do anything
But with @defer
, it’s a whole different story:
- Browser quickly parses just your main page structure
- Shows some lightweight placeholders where heavy content will eventually go
- Makes the page interactive right away (this is the magic moment!)
- Quietly loads those heavy components in the background
- Swaps in the real components when they’re ready
It’s like the difference between waiting for an entire pizza to be made before you can start eating versus getting some breadsticks right away while your pizza cooks.
Let’s Talk Trade-offs
Users see placeholders first: Yeah, your initial page isn’t “complete” with all the final content. But would you rather stare at a loading spinner for 300ms longer? For most apps, showing something quickly beats showing everything slowly.
Same resources in the end:
@defer
isn’t saving any work, your app ultimately does the same amount of processing. It just prioritizes making the UI responsive before tackling the heavy lifting.Watch those dependencies: If your deferred components contain stuff users need immediately (like a critical form or button), you’ll need a plan. Don’t defer things users need right away.
Where to Use @defer (And Where Not To)
After playing with this feature in several projects and running these benchmarks, here’s what I’ve learned about where @defer
really shines:
Slap @defer On These
Stuff Below the Fold: If users can’t see it without scrolling, why load it immediately? Prime candidate for
@defer (on viewport)
.Heavyweight Components: Got a complex data grid with filters and sorting? Or a component that crunches numbers? Defer it and let your page breathe.
Nice-to-Have Features: Those secondary panels and widgets that aren’t essential to the core experience? Defer them all day long.
Charts and Visualizations: Those fancy D3 charts or complex canvas visualizations? They’re often resource hogs that can easily be deferred.
Keep These Loading Normally
Your Main Navigation: Users need to navigate right away. Don’t make them wait for the menu to appear after the page loads!
The Main Content: Whatever the user came to your page for should probably load immediately. A news article’s text, a product’s details, etc.
Tiny Components: If it loads in 5ms anyway, the
@defer
overhead might not be worth it. Don’t overcomplicate simple things.Login Forms and Security Stuff: Anything authentication-related should usually be available immediately. Security shouldn’t be an afterthought.
Implementation Tips
Design Meaningful Placeholders: Create placeholders that give users a sense of what’s coming to reduce perceived disruption when content loads.
Use Appropriate Triggers: Choose the right loading trigger for each component based on its importance and expected usage.
Test on Various Devices: The performance benefits of
@defer
are especially important on lower-powered devices and slower networks.Combine with Other Performance Techniques: Use
@defer
alongside code splitting, lazy routes, and other Angular performance optimizations.
Wrapping Up: Should You Use @defer?
Let’s cut to the chase: 97% faster initial load times with a simple template update? Yeah, you should probably be using this.
Our benchmark wasn’t just impressive, it was legitimately game-changing. And the best part? You don’t need to refactor your entire architecture or learn some complex optimization technique. It’s just a template syntax change.
Sure, your heavy components still take the same time to render eventually. But that’s not the point. The magic is that users can start using your app almost instantly while those components load quietly in the background.
This is why I’ve started using @defer
in every Angular project I work on now. It’s one of those rare features that delivers dramatic performance improvements with minimal effort. And in a world where users bail after just a few seconds of waiting, these kinds of optimizations aren’t just nice-to-haves, they’re directly tied to your conversion rates and user satisfaction.
For any component that isn’t absolutely essential to the first interaction with your page, wrap it in a @defer
block. Your users (and your business metrics) will thank you.
Frequently Asked Questions
What is the Angular @defer
directive?
@defer
directive is a template feature that lets you lazy load components only when they’re needed, keeping them out of the initial bundle. It holds back heavy components until a specific trigger like scrolling, interaction, or a timer.How much can @defer
improve page load time?
@defer
reduced initial page load time by 97% - from 319ms to just 8ms. This makes pages become interactive almost immediately by deferring non-critical components.What are the different loading triggers available with @defer
?
@defer
offers five loading triggers- on viewport
(loads when scrolled into view), on idle
(loads during browser idle time), on immediate
(loads right after initial render), on timer
(loads after a specified delay), and on interaction
/hover
(loads when users interact with elements).How do placeholders work with @defer
?
@placeholder
block in templates. You can also set minimum display times for placeholders to prevent flickering with the minimum
parameter.Do deferred components load faster than regular components?
What are the other blocks available with @defer
?
@placeholder
, @defer
provides @loading
(shown while the component loads), @error
(shown if loading fails), and timing control for each state using the minimum
parameter.When should I use @defer
in my Angular app?
@defer
for components below the fold (not visible without scrolling), heavyweight components with complex logic, non-essential features, charts and visualizations, and anything that isn’t needed immediately when the page loads.What components should NOT use @defer
?
@defer
for main navigation, critical content the user came for, tiny lightweight components, or login forms and security-related elements.Will @defer
work on slow connections?
@defer
works especially well on slow connections and older devices. It gives users a working page quickly instead of making them wait for everything to load, which can be the difference between using your app and giving up on it.Is @defer
difficult to implement in an existing Angular project?
@defer
is remarkably easy to implement. It’s just a template syntax change - you wrap existing components in @defer
blocks and add appropriate @placeholder
templates. You don’t need to refactor your architecture or component logic.