Bài viết được dịch từ: toddmotto.com

Bài viết này sẽ nói về code spliting trong Angular, lazy-loading và sprinkle của Webpack. Code spliting cho phép chúng ta chia codebase thành các phần nhỏ hơn và load khi cần (cái chúng ta gọi là "lazy loading"). Hãy học cách để làm điều đó và một vài khái niệm/ thuật ngữ đằng sau nó.

Mục lục

  • Các thuật ngữ
    • Code spliting
    • Lazy loading
  • Thiết lập Webpack
    • Chọn một router loader
  • Lazy @NgModules
    • Feature modules
    • Async module lazy loading
    • Sync module loading
  • Hiệu năng
    • Lazy loading modules
    • Preloading lazy modules

Bạn có thể tham khảo source code trên GitHub hoặc live demo.

Hình trên minh họa cho lazy loading, bạn có thể thấy 0-chunk.j1-chunk.js đều được tải thông qua mạng khi điều hướng đến các route tương ứng. 

Các thuật ngữ

Để hiểu rõ hơn, hãy làm quen với một vài thuật ngữ.

Code splitting

Code spliting là quá trình chia nhỏ code. Nhưng chia cái gì, như thế nào và ở đâu? Chúng ta sẽ biết điều này, qua tiến trình được mô tả trong phần tiếp theo,  nhưng về bản chất code splitting cho phép chia file bundle của ứng dụng thành nhiều phần khác nhau. Và Webpack cho phép chúng ta làm điều này cực kỳ dễ dàng với một loader cho Angular. Nói tóm lại, ứng dụng của bạn trở thành nhiều ứng dụng nhỏ, thường gọi là "chunks". Các chunk này có thể load khi cần.

Lazy loading

Lazy loading là quá trình load các phần code đã được chia nhỏ của ứng dụng, và chỉ load chúng khi cần. Angular router, cho phép chúng ta thực hiện lazy load. Chúng ta gọi nó là "lazy" bởi vì nó không load các tài nguyên ngay từ đầu. Lazy loading giúp tăng hiệu suất quá trình khởi động - vì chúng ta chỉ download một phần bundle của ứng dụng thay vì toàn bộ. Với Angular chúng ta cho thể code split (chia nhỏ code) trên @NgModule và phục vụ chúng khi cần thông qua router. Với mỗi route cụ thể, router của Angular sẽ load phần code của module đó.

Thiết lập Webpack

Bạn có thể xem cấu hình đầy đủ để biết cách mọi thứ làm việc cùng nhau, nhưng về bản chất, chúng ta chỉ cần quan tâm một vài phần chính.

Chọn một router loader

Bạn có thể sử dụng angular-router-loader hoặc ng-router-loader để thực hiện lazy loading - Tôi chọn angular-router-loader bởi vì nó khá đơn giản.

Đây là cách tôi thêm nó vào cấu hình Webpack:

{
  test: /\.ts$/,
  loaders: [
    'awesome-typescript-loader',
    'angular-router-loader',
    'angular2-template-loader'
  ]
}

Bước tiếp theo là thuộc tính output trong cấu hình Webpack:

output: {
  filename: '[name].js',
  chunkFilename: '[name]-chunk.js',
  publicPath: '/build/',
  path: path.resolve(__dirname, 'build')
}

Đây là nơi chúng ta chỉ định tên của chunk, nó thường động và giống như thế này:

0-chunk.js
1-chunk.js
2-chunk.js
3-chunk.js

Lazy @NgModules

Để minh họa thiết lập như trong live demo, chúng có 3 feature module giống nhau, ngoại trừ tên của module và component.

Feature modules

Feature modules (hay các module con), là các modules chúng ta có thể lazy load sử dụng router. Đây là tên 3 module con:

DashboardModule
SettingsModule
ReportsModule

Và module parent, app module:

AppModule

AppModule chịu trách nhiệm "import" các module khác. Có một vài cách để làm điều này, bất đồng bộ và đồng bộ.

Async module lazy loading

Tất cả chúng ta cần là thuộc tính loadChildren trên các định nghĩa routing.

Đây là ReportsModule:

// reports.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// containers
import { ReportsComponent } from './reports.component';

// routes
export const ROUTES: Routes = [
  { path: '', component: ReportsComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(ROUTES)
  ],
  declarations: [
    ReportsComponent
  ]
})
export class ReportsModule {}

Chú ý cách chúng ta sử dụng một path rỗng:

// reports.module.ts
export const ROUTES: Routes = [
  { path: '', component: ReportsComponent }
];

Module này có thể sử dụng cùng với loadChildrenpath trong parent module - AppModule. Điều này, tạo ra một cấu trúc module linh hoạt, nơi các feature module không cần biết path tuyệt đối của mình, chúng trở thành các path tương đối dựa trên các path AppModule.

Trong app.module.ts, chúng ta có thể làm điều này:

// app.module.ts
export const ROUTES: Routes = [
  { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' }
];

Điều này nói với Angular "khi gặp /reports, hãy load module này". Chú ý cách định nghĩa routing trong ReportModule là một path rỗng. Tương tự, cho 2 module còn lại:

// reports.module.ts
export const ROUTES: Routes = [
  { path: '', component: ReportsComponent }
];

// settings.module.ts
export const ROUTES: Routes = [
  { path: '', component: SettingsComponent }
];

// dashboard.module.ts
export const ROUTES: Routes = [
  { path: '', component: DashboardComponent }
];

Routing của AppModule sẽ như thế này:

export const ROUTES: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'dashboard' },
  { path: 'dashboard', loadChildren: '../dashboard/dashboard.module#DashboardModule' },
  { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' },
  { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' }
];

Chúng ta gọi đây là "lazy loading" khi gọi tới một chunk bất đồng bộ. Khi sử dụng loadChildren và chuỗi giá trị trỏ tới một module, nó sẽ là load bất đồng bộ, trừ khi bạn chỉ định loading đồng bộ.

Sync module loading

Nếu giống như trong ứng dụng của tôi, path của bạn redirect tới một route khác, như thế này:

{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },

Bạn có một nơi có thể chỉ định một module để load đồng bộ. Điều này có nghĩa là nó sẽ được bundled vào app.js. Như tôi redirect khá đơn giản tới DashboardModule, có lợi gì khi tôi chunking nó? Có và không.

Có: nếu tới /settings đầu tiên (refresh trang), chúng ta không muốn load nhiều code, vì thế một lần nữa tiết kiệm trọng tải trong lần đầu tiên.

Không: Module này không sử dụng thường xuyên, vì thế tốt nhất là load nó đầu tiên.

Đây là cách chúng ta load đồng bộ DashboardModule sử dụng một import và arrow function:

import { DashboardModule } from '../dashboard/dashboard.module';

export const ROUTES: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'dashboard' },
  { path: 'dashboard', loadChildren: () => DashboardModule },
  { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' },
  { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' }
];

Tôi thích cách này vì nó đã ngầm thể hiện ý tưởng. Tại điểm này, DashboardModule sẽ được bundled với AppModule và được phục vụ trong app.js. Bạn có thể tự thử nghiệm bằng cách chạy projec locally và thay đổi vài thứ.

angular-router-loader có một tính năng khá thú vị là cho phép các module load đồng bộ bằng cách chèn ?sync=true tới chuỗi của chúng ta như sau:
 

loadChildren: '../dashboard/dashboard.module#DashboardModule?sync=true'

Điều này có tác dụng tương tự arrow function.

Hiệu năng

Với một ứng dụng demo đơn giản, bạn sẽ không thực sự thấy được sự gia tăng hiệu năng, tuy nhiên với một ứng dụng có một codebase có kích thước lớn, bạn sẽ được hưởng lợi rất nhiều từ code spliting và lazy loading!

Lazy loading modules

Hãy tưởng tượng chúng ta có:

vendor.js [200kb] // angular, rxjs, etc.
app.js [400kb] // our main app bundle

Bây giờ giả sử chúng ta code split:

vendor.js [200kb] // angular, rxjs, etc.
app.js [250kb] // our main app bundle
0-chunk.js [50kb]
1-chunk.js [50kb]
2-chunk.js [50kb]

Một lần nữa, trên quy mô lớn hơn, hiệu năng sẽ tăng rất nhiều với những thứ như PWAs (Progressive Web Apps), tải trọng các requets ban đầu giảm đáng kể .

Preloading lazy modules

Một tùy chọn khác, chúng ta có là tính năng PreloadAllModules, nó cho phép Angular khi khởi tạo lấy tất cả module chunks còn lại từ server của bạn. Điều này có thể lại là một phần của câu chuyện về hiệu năng và bạn chọn download tất cả chunk ngay từ đầu. Nó giúp điều hướng giữa các module nhanh hơn, và chúng download bất đồng bộ khi bạn thêm nó tới routing của module gốc. Ví dụ:

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

export const ROUTES: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'dashboard' },
  { path: 'dashboard', loadChildren: '../dashboard/dashboard.module#DashboardModule' },
  { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' },
  { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' }
];

@NgModule({
  // ...
  imports: [
    RouteModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules })
  ],
  // ...
})
export class AppModule {}

Trong ứng dụng demo của tôi, Angular sẽ khởi tạo đầu tiên và tải các chunk còn lại sử dụng hướng tiếp cận này.

Bạn có thể tham khảo source code trên GitHub hoặc live demo.

Tôi khuyến khích bạn thử nhiều cách và kịch bản khác nhau, để có thể vẽ lên một bức tranh về hiệu năng của riêng mình.