Angular 懒加载(Lazy Loading)实战指南:按需加载模块与组件,优化首屏性能
Angular 懒加载(Lazy Loading)实战指南:按需加载模块与组件,优化首屏性能
本文围绕 Angular 路由懒加载技术展开,讲解如何通过将应用拆分为多个 bundle、在路由激活时异步加载 JavaScript 组件,从而显著降低首屏加载时间。读完本文,你将掌握基于 NgModule 的 loadChildren 懒加载、基于 Standalone 组件的 loadComponent 懒加载、预加载策略配置,以及懒加载与 Angular 特性模块、可延迟视图(@defer)、AOT 编译之间的配合关系,可直接在项目中落地实施。
什么是懒加载:为什么 Angular 应用需要它
懒加载(Lazy Loading)是 Angular 中一项核心性能优化技术:它允许你在特定路由被激活时,才异步加载对应的 JavaScript 组件。默认情况下,Angular 应用的所有模块都是急切加载(Eager Loading)的——应用一启动,所有 NgModule 都会立即下载并执行,无论当前用户是否真的需要它们。
对于路由数量多的大型应用,急切加载会带来两个明显问题:
- 首屏 bundle 过大:所有功能代码被打包进少数几个初始文件,用户首次访问就要下载大量用不到的代码,加载时间被拉长;
- 带宽浪费:用户可能只访问其中一两个页面,却被迫下载了整个应用。
懒加载正是解决这两个问题的设计模式:它将应用拆分成多个 bundle,按需加载——用户导航到哪个路由,才下载哪个路由对应的代码块。从仓库中 Angular 学习路径的配套文档 Lazy Loading Modules 可以看到同样的定义:懒加载能让初始 bundle 更小,从而帮助缩短加载时间。
一句话概括其核心价值:减少初始加载量,把“所有代码一次下载”变成“用到的才下载”,以更快的首屏换取更好的用户体验。
懒加载的两种主流实现方式
在 Angular 中实现路由级懒加载,取决于你的应用是基于 NgModule 架构还是 Standalone 组件架构。两种方式都在路由配置层面声明,Angular Router 会在导航发生时按需拉取对应代码。
方式一:NgModule 懒加载(loadChildren)
传统 NgModule 架构中,路由通过 loadChildren 指定要懒加载的特性模块(Feature Module)。这也是 Module Architecture 文档强调的典型场景——特性模块“often lazily loaded”(通常被懒加载)。
典型的配置如下:
// app-routing.module.ts
const routes: Routes = [
{
path: 'customers',
// 关键:loadChildren 接收一个工厂函数,返回被加载的模块
loadChildren: () =>
import('./customers/customers.module').then((m) => m.CustomersModule),
},
{
path: 'orders',
loadChildren: () =>
import('./orders/orders.module').then((m) => m.OrdersModule),
},
];
这里的 import() 是标准的 ECMAScript 动态导入语法,配合打包工具(如 webpack、Vite)会把 customers.module 及其依赖单独拆成一个 chunk。只有用户导航到 /customers 时,这个 chunk 才会被网络请求并执行。
对应地,CustomersModule 内部需要为自身声明路由:
// customers/customers.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CustomersComponent } from './customers.component';
const routes: Routes = [{ path: '', component: CustomersComponent }];
@NgModule({
declarations: [CustomersComponent],
imports: [RouterModule.forChild(routes)],
})
export class CustomersModule {}
注意其中的 RouterModule.forChild(routes):懒加载模块必须使用 forChild 注册自己的子路由,最终由根模块的 RouterModule.forRoot(routes) 统一调度。这与 Feature Modules 文档中“用特性模块把特定功能代码与其他代码隔离”的组织理念一致——懒加载是特性模块在加载时机上的进一步增强。
方式二:Standalone 组件懒加载(loadComponent)
Angular 15 之后,应用可以全面转向 Standalone 组件架构(组件元数据中设置 standalone: true,直接导入模板中使用的组件、指令和管道,参见 Standalone 文档)。在 Standalone 架构下,不再需要为每个路由准备一个 NgModule,可以直接在路由上懒加载组件本身:
// app.routes.ts
export const routes: Routes = [
{
path: 'customers',
// 直接懒加载一个 Standalone 组件
loadComponent: () =>
import('./customers/customers.component').then(
(m) => m.CustomersComponent
),
},
];
此时 CustomersComponent 只需是一个普通的 Standalone 组件即可:
// customers/customers.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-customers',
standalone: true, // Standalone 组件无需 NgModule 包裹
template: `<h2>Customers</h2>`,
})
export class CustomersComponent {}
loadComponent 是比 loadChildren 更轻量的懒加载单元:它去掉了“为懒加载而额外创建 NgModule”的样板代码,直接以组件为粒度拆分 bundle。对于新项目,这是官方推荐的主流写法。
预加载策略:在懒加载与性能之间取平衡
懒加载把下载推迟到导航发生时,但用户点击进入新页面时仍会有一段网络等待。Angular Router 提供了预加载策略(PreloadStrategy),让应用可以在空闲时提前拉取懒加载 chunk,从而兼顾首屏体积与导航体验。
在 RouterModule.forRoot() 中配置:
import { PreloadAllModules } from '@angular/router';
RouterModule.forRoot(routes, {
// 构建时全量预加载所有懒加载模块
preloadingStrategy: PreloadAllModules,
})
- 默认(不配置):完全不预加载,严格按需下载;
PreloadAllModules:全部预加载,应用空闲后后台拉取所有懒加载 chunk;- 自定义策略:实现
PreloadStrategy接口,按业务优先级选择性地预加载(例如只预加载高频页面,跳过大型低频页面)。
选择哪种策略取决于应用场景:首屏体积优先选默认不预加载;追求页面切换“秒开”体验且网络条件较好时,可用 PreloadAllModules;两者兼顾则用自定义策略做精细化控制。
懒加载在整个 Angular 性能体系中的位置
懒加载并不是孤立的技巧,它是 Angular 性能优化体系中的一环。仓库的 Performance 文档将其与另外几种关键技术并列:
| 技术 | 作用层面 | 目标 |
|---|---|---|
| 懒加载(Lazy Loading) | 路由 / 模块 / 组件粒度 | 拆分 bundle,按需下载,优化首屏 |
可延迟视图(Deferrable Views,@defer) |
模板内部依赖粒度 | 推迟模板中组件/指令/管道及其 CSS 的加载 |
| 图片优化(Image Optimization) | 静态资源 | 降低图片下载开销 |
| 减少 Zone 污染(Zone Pollution) | 变更检测 | 精简变更检测触发频率 |
| 慢计算优化(Slow Computations) | 运行时 | 提升运行时效率 |
| 水合(Hydration) | SSR 应用 | 让服务端渲染应用更快可交互 |
两者可以组合使用:路由层用懒加载拆分页面 bundle,页面模板内部再用 @defer 块延迟加载非关键依赖——@defer 以声明式的方式把模板中一段区域包起来,并指定加载条件(参见 Deferrable Views 文档):
<!-- 只有在可见时才加载该区域依赖的组件、指令、管道及其 CSS -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div>图表加载中…</div>
}
懒加载与构建:AOT 编译与打包
懒加载之所以能生效,依赖于构建工具在编译打包阶段就把动态 import() 的模块拆成独立 chunk。Angular 应用在浏览器运行前必须先经过编译——AoT Compilation 文档指出,AOT 编译器会在构建阶段把 Angular 的 HTML 和 TypeScript 代码转换为高效 JavaScript。AOT 编译与懒加载是天然搭档:
- AOT 在构建期完成模板与组件的编译,产出更小的产物、更快的浏览器渲染;
- 懒加载依赖构建期对动态导入的静态分析,打包器据此自动生成命名清晰的 chunk 文件;
- 构建完成后,你可以在产物目录中看到类似
customers-*.js、orders-*.js的独立文件,这就是懒加载拆分出的 bundle,只有导航到对应路由时才会被浏览器请求。
从源码结构看,Angular CLI 构建体系(ng build)会为每个懒加载路由生成对应的 chunk,并在路由配置中写入映射关系;运行时 Router 依据该映射按需请求 chunk、实例化模块/组件后渲染到 Router Outlet 标记的出口区域。
实践建议与注意事项
结合上述原理,落地懒加载时有几点值得注意:
- 以路由为拆分边界:每个懒加载单元对应一个相对独立的业务页面或功能域,避免把一个页面拆成过多碎 chunk(增加请求数量反而拖慢性能);
- Standalone 优先:新代码推荐
loadComponent+ Standalone 组件,省去 NgModule 样板,也让拆分粒度更细; - 搭配预加载策略:对用户最可能访问的页面使用自定义
PreloadStrategy预加载,降低导航等待; - 路由守卫照常生效:懒加载不影响 Guards 的认证/授权拦截逻辑——导航发生时先执行守卫,再按需加载目标代码,可避免未授权用户触发无谓下载;
- 模板内小依赖用
@defer:路由级懒加载负责“页面级”拆分,页面内部的次要组件交给@defer处理,两级配合效果最佳。
小结
懒加载是 Angular 应用性能优化中投入产出比极高的技术:通过 loadChildren(NgModule 架构)或 loadComponent(Standalone 架构)把应用拆分为多个 bundle,让 JavaScript 组件在对应路由激活时才被异步加载,从而显著缩小首屏包体积、加快应用初始加载速度。结合预加载策略、@defer 可延迟视图与 AOT 编译,你可以在不牺牲用户体验的前提下,把大型应用的首屏性能做到最优。