Angular @defer Năm 2026: Lazy Loading Khai Báo Giúp Ứng Dụng Nhanh Hơn

Khối @defer trong Angular cho phép lazy loading khai báo với các trigger viewport, interaction và timer. Phân tích chuyên sâu prefetching, SSR, incremental hydration và các pattern tối ưu hiệu suất.

Angular @defer lazy loading

Khối @defer trong Angular giải quyết một bài toán mà lazy loading dựa trên router chưa bao giờ xử lý triệt để: tải riêng lẻ từng component, directive và pipe theo nhu cầu mà không cần tái cấu trúc ứng dụng thành các route riêng biệt. Ra mắt từ Angular 17 và được ổn định hóa hoàn toàn qua Angular 22, @defer biến những thao tác vốn đòi hỏi dynamic import thủ công, cờ *ngIf phức tạp và cấu hình webpack rắc rối thành một khai báo duy nhất ngay trong template.

@defer thực hiện điều gì

@defer chỉ thị cho Angular compiler tách các component được bao bọc thành các chunk JavaScript riêng biệt, chỉ được tải khi điều kiện trigger kích hoạt. Trình duyệt tải ít JavaScript hơn ngay từ đầu, giúp cải thiện chỉ số Largest Contentful Paint (LCP) và Time to First Byte (TTFB) mà không cần cấu hình code-splitting thủ công.

Cơ Chế Hoạt Động Nội Bộ Của @defer

Khi Angular compiler gặp một khối @defer, nó trích xuất mọi standalone component, directive và pipe bên trong thành một chunk riêng biệt. Bundle chính được phân phối tới trình duyệt mà không chứa các dependency này. Tại thời điểm runtime, Angular đánh giá điều kiện trigger và tải chunk thông qua lời gọi import() động.

Khối @defer hỗ trợ bốn sub-block kiểm soát nội dung hiển thị trong suốt vòng đời tải:

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 được render trước khi trigger kích hoạt. @loading xuất hiện trong lúc chunk đang được tải, với tham số minimum tùy chọn nhằm ngăn hiện tượng nhấp nháy. @error đảm nhận việc xử lý khi gặp lỗi mạng hoặc lỗi chunk. Tham số minimum trên @loading giúp tránh hiện tượng spinner xuất hiện chớp nhoáng khi chunk được tải nhanh từ cache.

Một ràng buộc quan trọng cần lưu ý: mọi dependency bên trong @defer bắt buộc phải là standalone. Các component được khai báo trong NgModule không thể bị defer và sẽ được tải eagerly bất kể có @defer bao bọc hay không.

Các Loại Trigger: Kiểm Soát Thời Điểm Tải Component

Angular cung cấp bảy trigger tích hợp sẵn, mỗi trigger nhắm đến một chiến lược tải khác nhau. Nhiều trigger có thể kết hợp bằng dấu chấm phẩy và được đánh giá theo logic OR.

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 />
}

Trigger on viewport sử dụng Intersection Observer API nội bộ. Trigger on idle ủy quyền cho requestIdleCallback, trở thành lựa chọn mặc định an toàn nhất cho nội dung nằm dưới fold. Trigger on timer chấp nhận thời lượng tính bằng mili giây (500ms) hoặc giây (3s).

Trigger when Cho Các Điều Kiện Reactive

Ngoài các trigger dựa trên sự kiện, when chấp nhận bất kỳ biểu thức boolean nào, bao gồm cả việc đọc signal:

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>
}

Việc kết hợp onwhen hoàn toàn hợp lệ. Angular xử lý chúng theo logic OR: khối sẽ tải khi bất kỳ điều kiện nào được thỏa mãn trước.

Prefetching: Tách Biệt Quá Trình Tải Xuống Và Hiển Thị

Prefetching tách rời thời điểm chunk được tải xuống khỏi thời điểm component được render. Kỹ thuật này loại bỏ độ trễ cảm nhận bằng cách tải JavaScript trước khi người dùng kích hoạt trigger.

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

Trong pattern này, trình duyệt tải xuống chunk account-settings trong thời gian rảnh rỗi. Khi người dùng click, component render ngay lập tức vì mã nguồn đã sẵn sàng. Mệnh đề prefetch chấp nhận cùng các loại trigger như mệnh đề on chính.

Một pattern phổ biến cho các trang dashboard: prefetch component nặng trong thời gian idle, đồng thời hoãn việc render cho đến khi component đi vào viewport.

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>
}

Sẵn sàng chinh phục phỏng vấn Angular?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

@defer Và Server-Side Rendering

Phía server, các khối @defer render nội dung @placeholder theo mặc định. Component được defer không bao giờ được render ở phía server. Hành vi này là có chủ đích: việc tải chunk chỉ có ý nghĩa trong ngữ cảnh trình duyệt.

Đối với nội dung quan trọng cho SEO, điều này đồng nghĩa với việc @placeholder phải chứa HTML có ý nghĩa, không chỉ đơn thuần là 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>
}

Incremental Hydration Với @defer

Angular 19 giới thiệu incremental hydration ở trạng thái developer preview, và Angular 22 ổn định hóa nó thành tính năng sẵn sàng cho production. Incremental hydration mở rộng @defer bằng trigger hydrate kiểm soát thời điểm một component đã render phía server trở nên tương tác trên client.

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

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

Khác với @defer tiêu chuẩn, incremental hydration render đầy đủ HTML của component trên server. Client nhận được markup hoàn chỉnh nhưng bỏ qua bước hydrate cho đến khi trigger kích hoạt. Kết quả là trang hiển thị giao diện hoàn thiện ngay tức thì, trong khi chi phí thực thi JavaScript được phân bổ dần theo tương tác thực tế của người dùng.

Các Pattern Thực Tế Và Chiến Lược Hiệu Suất

Pattern 1: Dashboard Nặng Với Nhiều Panel Defer

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>

Trong một ứng dụng doanh nghiệp, dashboard thường chứa hàng chục widget nhưng người dùng hiếm khi sử dụng tất cả trong cùng một phiên. Tách mỗi panel thành một khối @defer riêng cho phép trình duyệt chỉ tải code khi thực sự cần thiết. Kết hợp prefetch on idle đảm bảo rằng khi người dùng cuộn đến vị trí panel, code đã sẵn sàng để render ngay lập tức.

Pattern 2: Tải Tính Năng Có Điều Kiện Với Signal

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 />
}

Pattern này kết hợp tối ưu hiệu suất với logic nghiệp vụ. Người dùng gói miễn phí không bao giờ tải chunk chứa AI assistant, tiết kiệm hoàn toàn băng thông và thời gian phân tích cú pháp JavaScript. Đồng thời, @placeholder hiển thị banner nâng cấp, vừa đóng vai trò giao diện thay thế vừa phục vụ mục tiêu chuyển đổi người dùng.

Tránh Các Lỗi Thường Gặp

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 />
}

Việc lồng nhiều khối @defer với cùng loại trigger (mặc định là on idle) khiến tất cả các chunk được tải đồng thời, làm giảm hiệu quả tối ưu hóa. Phân tầng trigger giữa các lớp lồng nhau -- ví dụ khối ngoài sử dụng on idle và khối trong sử dụng on viewport -- đảm bảo việc tải diễn ra tuần tự và có trật tự.

So Sánh @defer Với Lazy Loading Dựa Trên Router

| Khía cạnh | Router Lazy Loading | @defer | |--------|-------------------|--------| | Mức độ chi tiết | Cấp route | Cấp component | | Trigger | Sự kiện điều hướng | viewport, idle, interaction, hover, timer, điều kiện | | Hành vi SSR | Render đầy đủ phía server | Placeholder phía server | | Yêu cầu standalone | Không (hoạt động với NgModule) | Có (chỉ standalone) | | Prefetching | preloadingStrategy | Mệnh đề prefetch on | | Trường hợp sử dụng | Feature module, trang | Widget, panel, UI có điều kiện |

Hai phương pháp này bổ sung cho nhau thay vì loại trừ lẫn nhau. Router lazy loading phù hợp để tách ứng dụng thành các khu vực tính năng cấp cao (module quản trị, module báo cáo), trong khi @defer xử lý việc tải các component nặng bên trong mỗi khu vực đó. Một kiến trúc Angular hiện đại nên áp dụng đồng thời cả hai để đạt hiệu quả code-splitting đa tầng.

Câu Hỏi Phỏng Vấn Về Angular @defer

Hỏi: Điều gì xảy ra nếu đặt một component non-standalone bên trong @defer? Component đó sẽ được tải eagerly bất kể có @defer bao bọc hay không. Angular compiler không thể trích xuất các component khai báo trong NgModule thành chunk riêng biệt. Không có lỗi nào được ném ra, nhưng việc tối ưu hóa thất bại một cách hoàn toàn im lặng.

Hỏi: @defer hoạt động như thế nào trong quá trình SSR? Server render nội dung @placeholder. Component defer không bao giờ được render phía server. Hydration diễn ra trên client khi trigger kích hoạt (hoặc thông qua hydrate on cho incremental hydration).

Hỏi: @defer có tương thích với OnPush change detection không? Hoàn toàn tương thích. @defer là tính năng ở cấp template và hoạt động với mọi chiến lược change detection. Component defer tuân theo cấu hình change detection riêng của nó sau khi được tải.

Hỏi: Sự khác biệt giữa on idleon immediate là gì? on idle chờ trình duyệt hoàn thành tất cả công việc đang chờ thông qua requestIdleCallback. on immediate kích hoạt ngay sau khi Angular hoàn thành lần render ban đầu, không chờ trạng thái idle. on immediate phù hợp cho các component cần sẵn sàng sớm nhưng không nên chặn quá trình first paint.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Kết Luận

  • @defer tách các standalone component thành chunk JavaScript riêng biệt, chỉ tải khi trigger kích hoạt, giúp giảm đáng kể kích thước bundle ban đầu mà không cần thay đổi cấu trúc route
  • Bảy loại trigger tích hợp (idle, viewport, interaction, hover, timer, immediate, when) bao phủ toàn bộ phổ chiến lược tải, từ thụ động theo vị trí cuộn đến chủ động theo hành vi người dùng
  • Mệnh đề prefetch on cho phép tải trước chunk trong thời gian rảnh rỗi, loại bỏ hoàn toàn độ trễ cảm nhận khi người dùng kích hoạt component
  • Incremental hydration với hydrate on render đầy đủ HTML phía server nhưng hoãn việc hydrate sang client, cải thiện đáng kể chỉ số LCP trên các trang giàu nội dung
  • @placeholder được render trong SSR và cần chứa HTML có cấu trúc ngữ nghĩa rõ ràng để đảm bảo SEO -- tuyệt đối tránh chỉ dùng spinner hoặc skeleton trống
  • @defer bổ sung cho router lazy loading: sử dụng route để tách ở cấp feature module, sử dụng @defer để tách ở cấp component bên trong template
  • Tất cả dependency bên trong @defer bắt buộc phải có thuộc tính standalone -- component non-standalone sẽ được tải eagerly mà không có cảnh báo nào

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#angular
#performance
#lazy-loading

Chia sẻ

Bài viết liên quan