+4

Các kỹ thuật tối ưu hiệu suất trong Angular, giá như mình biết sớm hơn?

Giới thiệu

Hiệu suất luôn là một trong những yếu tố quan trọng hàng đầu khi phát triển ứng dụng web. Trong một thế giới mà người dùng mong đợi mọi thứ diễn ra ngay lập tức, các ứng dụng chậm chạp không chỉ gây khó chịu mà còn dẫn đến mất người dùng. Angular, một framework mạnh mẽ của Google, mang lại nhiều tính năng giúp xây dựng ứng dụng web quy mô lớn, nhưng nếu không được tối ưu hóa đúng cách, hiệu suất có thể bị ảnh hưởng nghiêm trọng.

Khi ứng dụng có hiệu suất kém, các vấn đề thường gặp bao gồm:

  • Giao diện phản hồi chậm khi dữ liệu thay đổi.
  • Tải trang mất nhiều thời gian.
  • UI bị giật lag khi tương tác.
  • Bộ nhớ bị rò rỉ do không quản lý tốt các subscription hoặc DOM elements.

Trong bài viết này, chúng ta sẽ cùng khám phá các kỹ thuật tối ưu hiệu suất trong Angular. Từ việc tối ưu Change Detection, tận dụng Lazy Loading, cho đến các cách thức quản lý template hiệu quả, tất cả sẽ giúp bạn xây dựng những ứng dụng Angular nhanh hơn, mượt mà hơn và đáp ứng tốt hơn nhu cầu thực tế. Hãy cùng bắt đầu với mình nhé! 🚀

1. Sử dụng Change Detection Strategy hợp lý

Angular sử dụng cơ chế Change Detection để kiểm tra và cập nhật giao diện mỗi khi dữ liệu thay đổi. Mặc định, Angular kiểm tra toàn bộ component tree, điều này có thể gây ảnh hưởng đến hiệu suất khi ứng dụng lớn dần.

Bằng cách sử dụng ChangeDetectionStrategy.OnPush, Angular chỉ cập nhật component khi có sự thay đổi rõ ràng về dữ liệu đầu vào (@Input).

🔹 Ví dụ không tối ưu Component dưới đây sẽ kiểm tra lại toàn bộ component tree mỗi khi có bất kỳ thay đổi nào trong ứng dụng.

@Component({
  selector: 'app-child',
  template: `{{ data }}`,
})
export class ChildComponent {
  @Input() data: any;
}

👉 Vấn đề: Mỗi lần có thay đổi ở bất kỳ component nào trong ứng dụng, Angular sẽ chạy lại Change Detection cho cả ChildComponent, ngay cả khi data không thay đổi.

Cách tối ưu với OnPush

import { Component, ChangeDetectionStrategy, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `{{ data }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() data: any;
}

🔹Lợi ích:

  • Angular chỉ cập nhật ChildComponent khi data thay đổi về tham chiếu, thay vì kiểm tra toàn bộ component tree.
  • Giảm thiểu số lần chạy Change Detection, giúp ứng dụng chạy nhanh hơn.

2. Sử dụng trackBy trong ngFor để tối ưu danh sách

Khi dùng *ngFor để lặp qua danh sách, Angular sẽ tạo lại toàn bộ danh sách mỗi khi có thay đổi, kể cả khi chỉ một phần tử bị thay đổi. Điều này gây tốn tài nguyên và ảnh hưởng hiệu suất.

🔹 Ví dụ không tối ưu

<li *ngFor="let item of items">
  {{ item.name }}
</li>

👉 Vấn đề: Nếu có thay đổi nhỏ trong items, Angular sẽ xóa và render lại toàn bộ danh sách, ngay cả khi chỉ một item thay đổi. ✅ Cách tối ưu với trackBy

<li *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</li>
trackByFn(index: number, item: any): number {
  return item.id; // Sử dụng id để nhận diện phần tử
}

🔹Lợi ích:

  • Angular sẽ chỉ cập nhật các phần tử thực sự thay đổi, thay vì render lại toàn bộ danh sách.
  • Giảm thiểu việc thao tác DOM, giúp ứng dụng nhanh hơn.

3. Hạn chế Change Detection bằng NgZone.runOutsideAngular

Mặc định, Angular theo dõi mọi sự kiện trong ứng dụng và kích hoạt Change Detection khi có thay đổi. Tuy nhiên, trong một số trường hợp như sự kiện cuộn trang, animation hoặc timer, bạn có thể chạy chúng ngoài vùng quản lý của Angular để tránh làm chậm ứng dụng.

🔹 Ví dụ không tối ưu

window.addEventListener('scroll', () => {
  console.log('User is scrolling...');
});

👉 Vấn đề: Mỗi lần người dùng cuộn trang, Angular sẽ kích hoạt Change Detection, gây ảnh hưởng lớn đến hiệu suất.

Cách tối ưu với NgZone.runOutsideAngular

import { Component, NgZone } from '@angular/core';

@Component({
  selector: 'app-example',
  template: `<div (click)="doSomething()">Click me</div>`
})
export class ExampleComponent {
  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
      window.addEventListener('scroll', () => {
        console.log('User is scrolling...');
      });
    });
  }
}

🔹 Lợi ích:

  • Angular không kích hoạt Change Detection khi có sự kiện scroll, giúp cải thiện hiệu suất.

4. Tránh gọi hàm trực tiếp trong template

Khi bạn gọi một hàm trong template, hàm đó sẽ được thực thi mỗi khi Angular thực hiện Change Detection, ngay cả khi dữ liệu không thay đổi.

🔹 Ví dụ không tối ưu

<div>{{ calculateValue() }}</div>
calculateValue(): number {
  return Math.random() * 100;
}

👉 Vấn đề:

  • calculateValue() sẽ được gọi mỗi lần Change Detection chạy, gây ảnh hưởng đến hiệu suất.

Cách tối ưu: Lưu giá trị vào biến

<div>{{ value }}</div>
export class ExampleComponent {
  value: number;

  ngOnInit() {
    this.value = this.calculateValue();
  }

  calculateValue(): number {
    return Math.random() * 100;
  }
}

🔹 Lợi ích:

  • Hàm chỉ được gọi một lần thay vì bị gọi lại nhiều lần không cần thiết.

5. Sử dụng Lazy Loading để tối ưu tải trang

Thay vì tải toàn bộ ứng dụng ngay từ đầu, bạn có thể tải từng module khi cần thiết bằng Lazy Loading.

Ví dụ Lazy Loading trong Angular

const routes: Routes = [
  { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) }
];

🔹Lợi ích:

  • Giảm kích thước bundle khi tải trang ban đầu.
  • Tăng tốc độ tải trang, đặc biệt hữu ích cho ứng dụng lớn.

6. Sử dụng Lazy Loading cho Component với Standalone Components

Trong Angular 14+, bạn có thể tải Component theo yêu cầu thay vì tải tất cả khi ứng dụng khởi chạy. Điều này rất hữu ích cho các trang ít được truy cập hoặc có nội dung nặng.

Ví dụ Lazy Loading Component

import { Component, ViewContainerRef, inject } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<button (click)="loadComponent()">Tải Component</button> <ng-container #container></ng-container>`,
})
export class AppComponent {
  container = inject(ViewContainerRef);

  async loadComponent() {
    const { LazyComponent } = await import('./lazy.component');
    this.container.createComponent(LazyComponent);
  }
}

🔹Lợi ích:

  • Giảm thời gian tải ban đầu của ứng dụng.
  • Chỉ tải những gì cần thiết khi người dùng cần.

7. Prefetching và Preloading Modules để tăng tốc tải trang

Nếu bạn đã sử dụng Lazy Loading cho các module nhưng vẫn muốn cải thiện tốc độ, bạn có thể sử dụng Preloading Strategy để tải trước một số module quan trọng.

Cách bật Preloading Strategy

import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';

const routes: Routes = [
  { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

🔹 Lợi ích:

  • Tăng tốc độ chuyển trang vì module đã được tải trước khi người dùng truy cập.
  • Tối ưu cho ứng dụng có nhiều trang nhưng không làm chậm thời gian tải ban đầu.

8. Giảm kích thước Bundle bằng Tree Shaking và Tối ưu Import

Khi build ứng dụng, Angular có thể loại bỏ mã không sử dụng để giảm kích thước file xuất ra. Tuy nhiên, nếu bạn import sai cách, mã thừa vẫn sẽ được giữ lại.

🔹 Ví dụ import không tối ưu (Import toàn bộ thư viện)

import * as lodash from 'lodash';
console.log(lodash.shuffle([1, 2, 3, 4, 5]));

👉 Vấn đề: Cả thư viện lodash (~70KB) sẽ bị import, ngay cả khi bạn chỉ dùng 1 hàm!

Cách tối ưu: Chỉ import những gì cần thiết

import { shuffle } from 'lodash-es';
console.log(shuffle([1, 2, 3, 4, 5]));

🔹 Lợi ích:

  • Giảm kích thước file build, tải trang nhanh hơn.
  • Tree Shaking loại bỏ các module không cần thiết.

9. Tránh Memory Leak bằng cách Unsubscribe Observable

Nếu bạn không unsubscribe một Observable sau khi component bị hủy, nó vẫn tiếp tục chạy trong nền, gây rò rỉ bộ nhớ.

🔹 Ví dụ không tối ưu

export class ExampleComponent implements OnInit {
  ngOnInit() {
    interval(1000).subscribe(() => console.log('Tick'));
  }
}

👉 Vấn đề:

  • Khi component bị hủy, Observable vẫn tiếp tục chạy, gây rò rỉ bộ nhớ. ✅ Cách tối ưu: Dùng takeUntil để Unsubscribe
import { Component, OnDestroy } from '@angular/core';
import { Subject, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

export class ExampleComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(1000).pipe(takeUntil(this.destroy$)).subscribe(() => console.log('Tick'));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

🔹 Lợi ích:

  • Giải phóng bộ nhớ, tránh rò rỉ dữ liệu không cần thiết.

10. Bật NgOptimizedImage để tối ưu tải hình ảnh

Từ Angular 15+, Angular cung cấp NgOptimizedImage, giúp tối ưu hóa việc tải ảnh, giảm Cumulative Layout Shift (CLS) và tăng tốc trang.

Cách sử dụng NgOptimizedImage

<img ngSrc="https://example.com/image.jpg" width="600" height="400">

🔹 Lợi ích:

  • Cải thiện hiệu suất tải ảnh, giảm thời gian tải trang.
  • Giảm lỗi layout shift, giúp trang hiển thị ổn định hơn.

Kết luận

Việc tối ưu hóa hiệu suất trong Angular là cần thiết để đảm bảo ứng dụng chạy nhanh và ổn định. Một số phương pháp quan trọng bao gồm:

Sử dụng ChangeDetectionStrategy.OnPush để tối ưu Change Detection.

Dùng trackBy trong *ngFor để tránh render lại danh sách không cần thiết.

Chạy tác vụ ngoài Angular bằng NgZone.runOutsideAngular để giảm tải Change Detection.

Tránh gọi hàm trong template để tối ưu hiệu suất.

Dùng Lazy Loading để giảm kích thước bundle và tăng tốc tải trang.

Sử dụng Lazy Loading cho Component với Standalone Components (Angular 14+)

Sử dụng Lazy Loading & Preloading để tối ưu tải trang.

Giảm kích thước Bundle bằng Tree Shaking và Tối ưu Import để giúp ứng dụng tải nhanh hơn.

Dọn dẹp Observable để tránh memory leak và đảm bảo hiệu suất ổn định.

Bật NgOptimizedImage để tối ưu tải hình ảnh (Angular 15+)

Bằng cách áp dụng những kỹ thuật trên, bạn có thể cải thiện đáng kể hiệu suất ứng dụng Angular của mình. 🚀

Tạo ra một ứng dụng Angular nhanh như chớp ⚡

Hiệu suất không chỉ là một con số – đó là trải nghiệm người dùng. Một ứng dụng Angular chậm có thể khiến người dùng rời đi trong vài giây, nhưng một ứng dụng nhanh, mượt mà sẽ khiến họ quay lại nhiều lần.

Bằng cách áp dụng những kỹ thuật tối ưu mà chúng ta đã thảo luận – từ tối ưu Change Detection, Lazy Loading, đến quản lý bộ nhớ hiệu quả – bạn không chỉ cải thiện tốc độ ứng dụng mà còn giúp đội ngũ phát triển dễ dàng bảo trì và mở rộng dự án hơn.

🚀 Bây giờ là lúc hành động! Hãy bắt đầu bằng cách kiểm tra hiệu suất ứng dụng của bạn với Lighthouse hoặc Chrome DevTools, xác định các điểm yếu và thử ngay một số kỹ thuật trên. Bạn sẽ thấy Angular có thể nhanh hơn rất nhiều khi được tối ưu đúng cách!

👉 Bạn đã từng áp dụng kỹ thuật tối ưu nào trong Angular chưa? Hoặc có mẹo nào khác mà tôi chưa đề cập? Hãy chia sẻ trong phần bình luận nhé! 🔥

Hy vọng bài viết này giúp ích cho bạn trong hành trình lập trình của mình!

Cảm ơn các bạn đã xem bài viết của mình. Happy coding!!! 😃


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí