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

  1. 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!

  2. 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.

  3. 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.

  4. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. Heavy Component: Simulates expensive operations to represent real-world complex components
  2. Performance Service: Tracks and records load times for both pages and individual components
  3. Test Pages: One page loads components synchronously, the other uses @defer
  4. 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 &#64;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 &#64;defer</h1>
      <p class="description">
        This page uses the &#64;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>&#64;defer Performance Benchmark</h1>
      <p class="description">
        This benchmark compares page load performance with and without the
        &#64;defer feature in Angular. &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;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 &#64;defer</a
        >
        <a routerLink="/with-defer" class="nav-link"
          >View Page With &#64;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 &#64;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 &#64;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 += `| &nbsp;&nbsp;&nbsp;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 += `| &nbsp;&nbsp;&nbsp;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:

TestLoad TimeImprovement
Without @defer (Page Load)319msBaseline
   Component 1122ms-
   Component 282ms-
   Component 386ms-
With @defer (Page Load)8ms97% faster (311ms)
   Component 1126ms3% slower (4ms)
   Component 287ms6% slower (5ms)
   Component 382ms5% faster (4ms)

What’s Going On Here?

  1. 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.

  2. 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.

  3. 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
  4. 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:

  1. Browser has to parse ALL your components
  2. Execute ALL that JavaScript
  3. Render ALL those DOM elements
  4. Calculate ALL those styles
  5. And only THEN can the user actually do anything

But with @defer, it’s a whole different story:

  1. Browser quickly parses just your main page structure
  2. Shows some lightweight placeholders where heavy content will eventually go
  3. Makes the page interactive right away (this is the magic moment!)
  4. Quietly loads those heavy components in the background
  5. 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

  1. 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.

  2. 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.

  3. 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

  1. Stuff Below the Fold: If users can’t see it without scrolling, why load it immediately? Prime candidate for @defer (on viewport).

  2. Heavyweight Components: Got a complex data grid with filters and sorting? Or a component that crunches numbers? Defer it and let your page breathe.

  3. Nice-to-Have Features: Those secondary panels and widgets that aren’t essential to the core experience? Defer them all day long.

  4. 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

  1. Your Main Navigation: Users need to navigate right away. Don’t make them wait for the menu to appear after the page loads!

  2. 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.

  3. Tiny Components: If it loads in 5ms anyway, the @defer overhead might not be worth it. Don’t overcomplicate simple things.

  4. Login Forms and Security Stuff: Anything authentication-related should usually be available immediately. Security shouldn’t be an afterthought.

Implementation Tips

  1. Design Meaningful Placeholders: Create placeholders that give users a sense of what’s coming to reduce perceived disruption when content loads.

  2. Use Appropriate Triggers: Choose the right loading trigger for each component based on its importance and expected usage.

  3. Test on Various Devices: The performance benefits of @defer are especially important on lower-powered devices and slower networks.

  4. 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?

The Angular @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?

In our real-world benchmarks, @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?

Angular @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?

Placeholders show temporary content while deferred components load. You can create them using the @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?

No, deferred components don’t load faster - they take the same time to render. The benefit is that they don’t block the initial page load, making your app feel more responsive by showing the main content first.

What are the other blocks available with @defer?

Besides @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?

Use @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?

Don’t use @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?

No, @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.

References

  1. Angular Official Documentation on @defer
  2. Angular Performance Best Practices
  3. Web Vitals and Core Web Metrics
  4. Perceived Performance: The Only Kind That Really Matters
    See other angular posts