Angular - Cải thiện hiệu năng và trải nghiệm người dùng với Lazy Loading

Sơ qua về Lazy Loading

Lazy Loading là một design pattern thường được sử dụng trong lập trình máy tính để trì hoãn lại việc khởi tạo một đối tượng cho đến khi nào nó thực sự cần đến. Nó góp phần giúp cho hoạt động của chương trình được hiệu quả hơn nếu như được sử dụng một cách hợp lý. Nói đơn giản là: "Không load bất kỳ thứ gì nếu như bạn không cần đến"

Lazy Loading routes cải thiện hiệu năng của ứng dụng Angular như thế nào?

Một ứng dụng Angular quy mô lớn sẽ chứa rất nhiều feature modules, nếu chúng được load cùng một lúc khi ứng dụng được khởi động thì sẽ phải mất rất nhiều thời gian. Hãy thử tưởng tượng, người dùng vào website của các bạn mà phải ngồi nhìn icon loading cứ quay vòng vòng đến hàng chục giây thì liệu họ có còn muốn vào lần thứ 2 không 😃). Các feature modules đó có thể được load bất đồng bộ sau khi ứng dụng được load theo yêu cầu hoặc sử dụng các chiến lược khác nhau. Giảm kích thước của bundle khi ứng dụng được load lần đầu sẽ cải thiện được thời gian load của ứng dụng, do đó sẽ nâng cao trải nghiệm của người dùng đối với ứng dụng web của bạn.

Lazy Loading có nhiều lợi ích:

  • Bạn có thể load các feature modules chỉ khi được yêu cầu bởi người dùng
  • Bạn có thể tăng tốc thời gian load cho người dùng chỉ ghé thăm một số trang nhất định của ứng dụng
  • Bạn có thể tiếp tục mở rộng các modules được lazy load mà không tăng kích thước của bundle load lần đầu

Để cho các bạn có cái nhìn chi tiết hơn, ở đây tôi xây dựng một clone đơn giản của Gmail sử dụng những dữ liệu ngẫu nhiên. Ban đầu, code sẽ chưa được lazy load mà sử dụng eager load các feature modules để có thể làm tăng tổng kích thước bundle load ban đầu phục vụ cho việc giải thích ý tưởng của bài viết.

Đây là một ứng dụng đơn giản, nó có một file routing chung cho toàn bộ ứng dụng app.routing.ts và tất cả các features được chia ra thành các modules. Tôi đã sử dụng Augury extension cho việc debug ứng dụng Angular để có thể inspec được routes của ứng dụng. Cài đặt nó ở đây augury.angular.io.

Sau khi cài đặt xong Augury, mở dev tools, chuyển sang tab Augury, sau đó là Router Tree. Dưới đây là router tree cho ứng dụng này:

Ở đây bạn có thể thấy rằng, chúng ta có 2 tầng routing lồng nhau, và mỗi một tầng sẽ có nhiều routes. Trên các ứng dụng thực tế, chúng có thể có nhiều hơn thế và mỗi module sẽ chứa rất nhiều components, templates,... Hiện tại, tất cả các Components trên đều được load cùng một lúc khi mà người dùng mở trang web của bạn. Điều đó là không cần thiết, và nó tốn quá nhiều thời gian chờ đợi của người dùng. Hãy cùng tôi refactor lại codes giúp cho ứng dụng load nhanh hơn nhé ^_^

Refactoring ứng dụng sử dụng lazy load routes

Tôi sẽ tập trung vào lazy load Settings module và các routes liên quan.

Hiện tại tất cả các routes đều được đăng ký ở trong file app.routing.ts và sau đó sẽ được import vào trong root module app.module.ts. Dưới đây là file routing hiện tại của ứng dụng:

# app.routing.ts

const routes: Routes = [
  {
    path: '',
    redirectTo: '/inbox/primary',
    pathMatch: 'full'
  },
  {
    path: 'inbox',
    component: InboxComponent,
    children: [
      {
        path: '',
        redirectTo: 'primary',
        pathMatch: 'full'
      },
      {
        path: 'primary',
        component: PrimaryComponent
      },
      {
        path: 'social',
        component: SocialComponent
      }
      ...
    ]
  },
    {
    path: 'settings',
    component: SettingsComponent,
    children: [
      {
        path: '',
        redirectTo: 'general',
        pathMatch: 'full'
      },
      {
        path: 'general',
        component: GeneralComponent
      },
      {
        path: 'inbox',
        component: SettingsInboxComponent
      },
      ...
    ]
  },
  {
    path: 'important',
    component: ImportantComponent
  },
  {
    path: 'sent',
    component: SentComponent
  },
  ...
];

Bước 1:

Di chuyển routing cho Settings vào trong module của nó gm-settings.module.ts. Khi đăng ký các routes với RouterModule, chúng ta cần sử dụng forChild thay vì forRoot giống như đây là routing con của ứng dụng. Do đó, gm-settings.module.ts sẽ trông như sau:

export const routes: Routes = [
  {
    path: '',
    component: SettingsComponent,
    children: [
      {
        path: '',
        redirectTo: 'general',
        pathMatch: 'full'
      },
      {
        path: 'general',
        component: GeneralComponent
      },
      {
        path: 'inbox',
        component: InboxComponent
      },
      ...
    ]
  }
];


@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes),
    MdTabsModule
  ],
  declarations: [
    SettingsComponent, 
    GeneralComponent, 
    InboxComponent,
    ...
  ]
})
export class GmSettingsModule { }

Bước 2:

Cập nhật app.routing.ts để load các routes con sử dụng một đường dẫn tương đối đến module Settings và thêm cả tên lớp module đó nữa:

{
    path: 'settings',
    loadChildren: './gm-settings/gm-settings.module#GmSettingsModule'
 }

Bước 3:

Module Settings hiện tại đang được import vào trong file app.module.ts để được eager load, bây giờ chúng ta cần xóa bỏ nó đi, Angular sẽ tự động cấu hình việc đăng ký module Settings.

Như vậy là chúng ta đã tiến hành xong việc Lazy Load cho module Settings. Hãy cũng xem Router Tree để thấy kết quả thay đổi ra sao nhé:

Nhìn vào kết quả ở trên, ta có thể thấy rằng, khi ứng dụng được load, tất cả các components của module Settings đều chưa hề được load, mà chúng chỉ thực sự được load khi mà người dùng click vào tab settings.

Như vậy là chúng ta đã Lazy Load thành công được module Settings và cải thiện được đáng kể tốc độ load lần đầu của ứng dụng.

Tuy nhiên, có một điều vẫn chưa được tốt cho lắm, đó là các module chỉ được load khi mà người dùng click vào settings hoặc các email cụ thể. Điều này có thể dẫn đến sự chậm trễ khi mà người dùng phải chờ đợi cho module đó được load => làm cho trải nghiệm người dùng xấu đi.

Để khắc phục được vấn đề này, chúng ta cùng tiếp tục phần tiếp theo của bài viết này nhé.

Chiến lược Preload

Đối với vấn đề được nêu ở trên, Angular cung cấp một phương thức để nói với router load tất cả các lazy load modules bất đồng bộ ngay lập tức sau khi ứng dụng được load và không cần phải chờ cho đến khi chúng được kích hoạt bằng cách người dùng click.

# app.routing.ts

 imports: [
    RouterModule.forRoot(
      routes, 
      { 
        preloadingStrategy: PreloadAllModules 
      }
    )
  ],

Nhìn ảnh gif trên ta có thể thấy rằng, tất cả các file *.chunk.js đều được load ngay lập tức sau khi ứng dụng được khởi tạo.

Tùy chỉnh chiến lược tải

Sử dụng chiến lược Preload ở trên giúp chúng ta giải quyết được 2 vấn đều:

  • Nó sẽ cải thiện tốc độ load của ứng dụng bằng cách giảm kích thước của lần load đầu tiên
  • Tất cả các lazy load modules sẽ được load bất đồng bộ ngay sau khi ứng dụng được load xong, do vậy sẽ không có một delay nào đối với người dụng khi chuyển hướng sang bất cứ lazy load module nào.

Tuy nhiên, preload tất cả các lazy load module không phải lúc nào cũng là sự lựa chọn đúng đắn. Đặc biệt đối với các thiết bị di động hay những kết nối băng thông thấp. Chúng ta có thể sẽ phải tải những modules mà người dùng có thể rất ít khi chuyển hướng đến. Tìm ra sự cân bằng cả về hiệu năng và trải nghiệm người dùng là chìa khóa cho việc phát triển.

Ví dụ, trong ứng dụng này, chúng ta có 2 lazy load modules:

  • Settings module
  • Email module

Đây là một ứng dụng email client nên module Email sẽ được sử dụng rất rất thường xuyên. Tuy nhiên module Setings sẽ được người dùng sử dụng nhưng với tuần suất rất thấp. Do vậy mà việc preload module Email sẽ đem lại hiệu quả cao, trong khi với module Setings thì thấp.

Angular cung cấp một cách extend PreloadingStrategy để xác định một tùy chỉnh chiến lược Preload chỉ ra điều kiện cho việc preload các lazy load module. Chúng ta sẽ tạo một provider extend từ PreloadingStrategy để preload các modules mà có preload: true được xác định trong cấu hình route.

# custom-preloading.ts

import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      this.preloadedModules.push(route.path);
      return load();
    } else {
      return Observable.of(null);
    }
  }
}

CustomPreloadingStrategy nên được đăng ký vào providers trong module mà RouterModule.forRoot được khai báo.

# app.routing.ts

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

Cuối cùng, trong app.routing.ts, chúng ta thêm data: { preload: true } vào trong phần khai báo route của module muốn được custom preload:

  {
    path: ':section',
    loadChildren: './gm-email/gm-email.module#GmEmailModule',
    data: { preload: true }
  }

Nhìn vào tab nework, ta thấy 5.chunks.js (module Email) đã được preload, còn 6.chunks.js được load bất động bộ khi người dùng chuyển hướng (module Settings).
Bài viết trên đây đã giới thiệu tổng quát về lazy loading sử dụng trong Angular kết hợp với các chiến lược tải có điều kiện có thể cải thiện hiệu năng của ứng dụng và nâng cao trải nghiệm cho người dùng. Hi vọng bạn đọc sẽ áp dụng nó hiệu quả vào website của mình nhé ^_^.

Tài liệu tham khảo

  1. https://blog.cloudboost.io/angular-faster-performance-and-better-user-experience-with-lazy-loading-a4f323b2cf4a
  2. https://angular.io/guide/router#milestone-6-asynchronous-routing