这里是我在几天内实现 @defer 进行细粒度代码拆分的完整步骤 — 步骤详解

发布: (2025年12月11日 GMT+8 17:00)
6 min read
原文: Dev.to

Source: Dev.to

@defer 基础与设置

Angular 的 @defer 块是性能提升的关键,它可以把重量级模板区域(如图表或仪表盘)拆分成独立的 JavaScript 包,仅在视口进入、用户交互或浏览器空闲等触发事件时才加载。这样可以大幅削减首屏负载,加快最大内容绘制(LCP)以及整体应用启动速度,尤其在大型单页应用中,用户不需要立刻使用的功能可以后置加载。

成功的前置条件

  • Angular v17 或更高(从 Angular 18 起正式支持可延迟视图)。
  • @defer 块内部的组件、指令和管道必须是 standalone,且不能在同一文件的其他位置被引用(例如不能有 ViewChild 查询或会导致提前加载的外部导入)。
  • 传递依赖可以是 standalone 或基于 NgModule 的混合,但占位内容应保持轻量,因为它们的依赖会在首次渲染时一起加载。

基本语法示例

@defer (on viewport) {
  
}
@placeholder {
  
}
  • 触发方式idle(默认)、interactionhovertimer(2s),或自定义 when showLargeChart
  • 加载指示器@loading (minimum 1s) { }
  • 错误处理@error { Retry? }
  • 预取prefetch on idle 可在不渲染的情况下提前加载代码包。

验证构建成果

  1. 运行 ng build,检查是否出现类似 defer-heavy-chart.js 的新块。
  2. 启动应用,打开 Chrome DevTools → Network,确认只有在触发时才加载对应的重量级包。
  3. 单元测试中,可通过以下方式模拟 defer 状态:
TestBed.deferBlockBehavior = DeferBlockBehavior.Manual;

实战示例:仪表盘魔法

@defer (on viewport; prefetch on idle) {
  
}
@placeholder (minimum 500ms) {
  
}

将数据量大的仪表盘部件以这种方式包装,可在复杂页面上让主包体积减少 30 %+,为折叠区的指标提供流畅体验。

核心触发器与子块

触发器描述
idle通过 requestIdleCallback 在浏览器空闲时触发(默认)。
viewport当元素进入视口时加载(Intersection Observer)。
interaction在点击、键盘按下或其他用户交互时触发。
hover在鼠标悬停或获得焦点时触发。
immediate在非延迟内容渲染完毕后立即加载。
timer(2s)在指定延迟后加载。

多个触发器可用分号组合(OR 逻辑)。预取单独声明,例如:

@defer (on viewport; prefetch on idle) { ... }

子块实现无缝用户体验

  • @placeholder – 预先加载的轻量内容,在延迟包加载前展示。使用 minimum 500ms 可避免闪烁。
  • @loading – 触发后、代码包解析前显示的加载指示。可通过 after 100ms; minimum 1s 控制时机。
  • @error – 加载失败时的回退 UI。将这些块放在带有 aria-live="polite" 的容器中,以便屏幕阅读器朗读。
@defer (on hover) {
  
}
@placeholder {  }
@loading {  }
@error {  }

使用 when 的自定义触发器

@defer (when isVisible()) {
  
}

when 只会评估一次信号或表达式,评估后不会回退。也可以与预取结合使用:

@defer (when userClicked(); prefetch when dataReady) { ... }

高级模式与优化

嵌套 @defer 与级联避免

嵌套 defer 块可以实现分层懒加载,但不当的嵌套会导致多个代码包同时请求。应错开触发时机以防止请求级联:

@defer (on viewport) {
  @defer (on interaction) {
    
  } @placeholder {
    Click to expand
  }
} @placeholder {
  Scroll to see more
}
@if (isExpanded) {
  
}
  • 外层使用 viewport,仅在滚动到该区块时加载。
  • 内层等待明确的交互,避免一次性发起大量网络请求。

SSR/SSG 与水合(Hydration)魔法

  • 在服务器端,仅渲染 @placeholder 内容(或不渲染任何内容),触发器被忽略,因为没有视口或空闲状态。
  • 客户端水合时,配置好的触发器会生效,使 UI 可交互。
  • 通过 hydrate on 触发器开启增量水合,实现 SEO 友好的服务器渲染,同时不膨胀客户端包体。配合 prefetch on idle 可在水合前预加载代码。

包体分析与 HMR 陷阱

  1. 安装分析工具:npm i -D source-map-explorer
  2. 使用 source map 构建:ng build --source-map
  3. 运行分析:
npx source-map-explorer dist/**/main.js

交互式树图会展示随着重量级 standalone 组件被拆分,主包体积的缩减情况。

HMR 注意:开发服务器会提前加载所有 @defer 块,绕过触发器。为获得准确测试,请关闭 HMR:

ng serve --no-hmr

可视化示例

Tools for Measuring @defer Bundle Gains

ARIA Live 区域的可访问性

屏幕阅读器可能错过占位内容到加载后内容的切换。将 defer 块包装在带有 ARIA live 属性的容器中:

@defer (on hover) {  }
@placeholder {  }
@loading {  }
@error {  }

这样可以确保动态内容变化被自动朗读,使 UI 在保持高性能的同时也具备包容性。

Back to Blog

相关文章

阅读更多 »