Mặc định, các module được eagerly loaded (tải ngay), nghĩa là ngay khi ứng dụng khởi động, tất cả module cũng được tải theo, bất kể có cần dùng ngay hay không. Điều này ổn với hầu hết ứng dụng, nhưng có thể trở thành điểm nghẽn với các app/worker chạy trong môi trường serverless, nơi độ trễ khởi động (“cold start”) là yếu tố then chốt.
Lazy loading giúp giảm thời gian bootstrap bằng cách chỉ tải các module cần thiết cho lần gọi hàm serverless cụ thể đó. Ngoài ra, bạn cũng có thể tải thêm các module khác một cách bất đồng bộ sau khi hàm serverless đã “ấm” (warm), để tăng tốc bootstrap cho những lần gọi tiếp theo (deferred modules registration - đăng ký module trì hoãn).
Gợi ý Nếu bạn quen với framework Angular, có thể bạn đã từng thấy thuật ngữ “lazy-loading modules” trước đây. Lưu ý rằng kỹ thuật này khác biệt về mặt chức năng trong Nest, nên hãy xem đây là một tính năng hoàn toàn khác, chỉ trùng tên gọi.
Cảnh báo Lưu ý rằng các lifecycle hook không được gọi trong các module và service được lazy load.
Bắt đầu
Để tải module theo yêu cầu (on-demand), Nest cung cấp class LazyModuleLoader, có thể được inject vào một class theo cách thông thường:
@Injectable()
export class CatsService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
}
@Injectable()
@Dependencies(LazyModuleLoader)
export class CatsService {
constructor(lazyModuleLoader) {
this.lazyModuleLoader = lazyModuleLoader;
}
}
Gợi ý Class LazyModuleLoader được import từ package @nestjs/core.
Ngoài ra, bạn cũng có thể lấy tham chiếu tới provider LazyModuleLoader từ trong file bootstrap ứng dụng (main.ts), như sau:
// "app" đại diện cho một instance ứng dụng Nest
const lazyModuleLoader = app.get(LazyModuleLoader);
Với điều này, giờ đây bạn có thể tải bất kỳ module nào bằng cấu trúc sau:
const { LazyModule } = await import('./lazy.module');
const moduleRef = await this.lazyModuleLoader.load(() => LazyModule);
Gợi ý Các module “lazy loaded” được cache ngay từ lần gọi đầu tiên LazyModuleLoader#load. Nghĩa là, mỗi lần gọi tiếp theo để tải LazyModule sẽ rất nhanh và trả về instance đã cache, thay vì tải lại module từ đầu.
Load "LazyModule" attempt: 1
time: 2.379ms
Load "LazyModule" attempt: 2
time: 0.294ms
Load "LazyModule" attempt: 3
time: 0.303ms
Ngoài ra, các module “lazy loaded” chia sẻ chung một module graph với những module được eager load lúc bootstrap ứng dụng, cũng như bất kỳ module lazy nào khác được đăng ký sau đó trong ứng dụng của bạn.
Trong đó lazy.module.ts là một file TypeScript export ra một module Nest bình thường (không cần thay đổi gì thêm).
Phương thức LazyModuleLoader#load trả về module reference (của LazyModule), cho phép bạn duyệt qua danh sách provider nội bộ và lấy tham chiếu tới bất kỳ provider nào bằng injection token của nó làm khóa tra cứu.
Ghi chú người dịch Cơ chế moduleRef.get() ở đây khá giống việc bạn dùng require()/dynamic import() để nạp một module Node theo yêu cầu, nhưng có thêm lớp DI container của Nest phía sau nên bạn có thể lấy ra instance đã được resolve dependency đầy đủ, chứ không chỉ là export thô của file.
Ví dụ, giả sử ta có một LazyModule với định nghĩa như sau:
@Module({
providers: [LazyService],
exports: [LazyService],
})
export class LazyModule {}
Gợi ý Module lazy loaded không thể đăng ký làm global module vì điều đó đơn giản là không hợp lý (do chúng được đăng ký lazily, theo yêu cầu, sau khi tất cả module đăng ký tĩnh đã được khởi tạo xong). Tương tự, các global enhancer đã đăng ký (guard/interceptor/v.v.) sẽ không hoạt động đúng cách.
Với điều này, ta có thể lấy tham chiếu tới provider LazyService như sau:
const { LazyModule } = await import('./lazy.module');
const moduleRef = await this.lazyModuleLoader.load(() => LazyModule);
const { LazyService } = await import('./lazy.service');
const lazyService = moduleRef.get(LazyService);
Cảnh báo Nếu bạn dùng Webpack, hãy nhớ cập nhật file tsconfig.json - đặt compilerOptions.module thành "esnext" và thêm thuộc tính compilerOptions.moduleResolution với giá trị "node":
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
...
}
}
Với các tùy chọn này, bạn sẽ có thể tận dụng tính năng code splitting.
Lazy loading controller, gateway và resolver
Vì controller (hoặc resolver trong ứng dụng GraphQL) trong Nest đại diện cho tập hợp các route/path/topic (hoặc query/mutation), bạn không thể lazy load chúng bằng class LazyModuleLoader.
Cảnh báo Controller, resolver, và gateway đăng ký bên trong module lazy loaded sẽ không hoạt động như mong đợi. Tương tự, bạn không thể đăng ký các hàm middleware (bằng cách implement interface MiddlewareConsumer) theo kiểu on-demand.
Ví dụ, giả sử bạn đang xây dựng một REST API (ứng dụng HTTP) dùng driver Fastify bên dưới (thông qua package @nestjs/platform-fastify). Fastify không cho phép bạn đăng ký route sau khi ứng dụng đã sẵn sàng/đang lắng nghe message thành công. Điều đó nghĩa là kể cả khi ta đã phân tích route mapping đăng ký trong controller của module, tất cả route lazy loaded sẽ không thể truy cập được vì không có cách nào đăng ký chúng lúc runtime.
Tương tự, một số transport strategy được cung cấp trong package @nestjs/microservices (bao gồm Kafka, gRPC, hay RabbitMQ) yêu cầu subscribe/lắng nghe các topic/channel cụ thể trước khi kết nối được thiết lập. Một khi ứng dụng của bạn bắt đầu lắng nghe message, framework sẽ không thể subscribe/lắng nghe topic mới.
Cuối cùng, package @nestjs/graphql với cách tiếp cận code first khi bật lên sẽ tự động sinh GraphQL schema ngay lập tức dựa trên metadata. Nghĩa là, nó yêu cầu tất cả class phải được tải trước. Nếu không, sẽ không thể tạo ra schema hợp lệ, phù hợp.
Ghi chú người dịch Nói cách khác: lazy loading trong Nest chỉ phù hợp cho các provider/service xử lý logic thuần túy (business logic, cron job, webhook handler…), chứ không dùng được cho bất kỳ thứ gì cần “đăng ký” với framework/transport layer ngay từ lúc khởi động - vì các layer đó (HTTP router, message broker…) đã “chốt” danh sách route/topic trước khi app sẵn sàng nhận request.
Các trường hợp sử dụng phổ biến
Phổ biến nhất, bạn sẽ thấy module lazy loaded trong các tình huống mà worker/cron job/hàm lambda & serverless/webhook của bạn cần trigger các service khác nhau (logic khác nhau) dựa trên tham số đầu vào (route path/ngày tháng/query parameter, v.v.). Mặt khác, lazy loading module có thể không quá ý nghĩa với ứng dụng monolithic, nơi thời gian khởi động không quá quan trọng.