首页
/ Angular 可延迟视图(Deferrable Views)实战:@defer 语法、空闲加载机制与懒加载分块原理

Angular 可延迟视图(Deferrable Views)实战:@defer 语法、空闲加载机制与懒加载分块原理

2026-09-07 14:34:53作者:虞亚竹Luna

Angular 的 deferrable views(可延迟视图)让开发者用 @defer 模板语法把"非首屏必需"的组件延迟下载与渲染,从而缩短首屏时间。本文以官方交互式教程的第一课("What are deferrable views?")为主体,完整讲解如何在模板中包裹一个组件实现延迟加载、默认"浏览器空闲时加载"的行为如何验证,并结合 Angular 仓库源码深入剖析 on idle 触发器的解析规则、IdleScheduler 的批量调度实现,以及空闲回调结束后自动触发变更检测的底层细节。

一、为什么需要 deferrable views

一个完整渲染的 Angular 页面往往包含大量组件、指令与管道。虽然页面的某些部分应当立即展示给用户,但总有一些内容可以稍后再呈现。Angular 的可延迟视图正是为此设计:通过 @defer 语法告诉 Angular——那些不需要立刻显示的部分,其 JavaScript 代码可以推迟下载。

对应仓库中的交互式教程位于 adev/src/content/tutorials/deferrable-views,其示例应用是一个"博文 + 评论区"页面:正文文章需要立即呈现,而评论区组件 article-comments 完全可以晚一些再加载。教程的目标就是学会用 deferrable views 延迟加载组件模板中的某一部分。

二、给模板中的区域添加 @defer 块

教程示例工程的初始版本位于 src/app/app.ts,根组件 App 的模板结构如下:

import {Component} from '@angular/core';
import {ArticleComments} from './article-comments';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h1>How I feel about Angular</h1>
      <article>
        <p>
          Angular is my favorite framework, and this is why. Angular has the coolest deferrable view
          feature that makes defer loading content the easiest and most ergonomic it could possibly
          be.
        </p>
      </article>

      <article-comments />
    </div>
  `,
  imports: [ArticleComments],
})
export class App {}

其中被延迟加载的目标是 article-comments.ts 中定义的 ArticleComments 组件——一个渲染三条静态评论的轻量组件:

@Component({
  selector: 'article-comments',
  template: `
    <h2>Comments</h2>
    <p class="comment">Building for the web is fantastic!</p>
    <p class="comment">The new template syntax is great</p>
    <p class="comment">I agree with the other comments!</p>
  `,
  styles: [
    `
      .comment {
        padding: 15px;
        margin-left: 30px;
        background-color: paleturquoise;
        border-radius: 20px;
      }
    `,
  ],
})
export class ArticleComments {}

操作步骤

按照教程(README.md)的指引,在 app.ts 中把 article-comments 组件包裹进一个 @defer 块即可实现延迟加载:

@defer {
  <article-comments />
}

修改后的完整模板见教程的参考答案 answer/src/app/app.ts

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h1>How I feel about Angular</h1>
      <article>
        <p>
          Angular is my favorite framework, and this is why. ...
        </p>
      </article>

      @defer {
        <article-comments />
      }
    </div>
  `,
  imports: [ArticleComments],
})
export class App {}

这里有两点值得注意:

  1. @defer 是纯模板级语法:无需额外导入任何指令,也不需要在 imports 中登记任何延迟加载配置——组件本身仍通过 imports: [ArticleComments] 正常声明,编译工具链会自动为其生成独立的懒加载资源。
  2. 默认行为是"空闲时加载":教程明确指出,@defer 不写任何触发器参数时,会在浏览器空闲时(when the browser is idle)加载 article-comments 组件。也就是说,正文渲染完成、主线程忙完初始工作后,评论区代码才会在空闲间隙被下载并执行。

三、验证懒加载分块:初始 chunk 与 lazy chunk 分离

教程要求读者在浏览器开发者控制台中验证延迟加载是否生效:可以看到 article-comments-component 对应的 lazy chunk 文件是单独加载的(具体文件名每次构建可能不同)。教程给出的构建产物报告示例如下:

Initial chunk files Names Raw size
chunk-NNSQHFIE.js - 769.00 kB
main.js main 229.25 kB
Lazy chunk files Names Raw size
chunk-T5UYXUSI.js article-comments-component 1.84 kB

这组数据说明了 @defer 的核心收益所在:

  • 首屏只下载 Initial chunk:主入口与首屏必需代码进入初始 chunk,被 @defer 包裹的组件代码不进初始包;
  • 被延迟的组件生成独立的 Lazy chunkarticle-comments-component 被单独打包为一个约 1.84 kB 的懒加载分块,只有当触发条件满足时才发起请求;
  • 文件名由构建工具生成chunk-NNSQHFIE.js 这类带哈希的命名意味着每次构建文件名可能变化,验证时应关注 "Lazy chunk files" 分组与 chunk 名称,而不是具体文件名。

四、源码深挖:触发器是如何被解析的

@defer 块中可写的不只是"什么都不写"。从编译器的触发器解析源码 packages/compiler/src/render3/r3_deferred_triggers.ts 可以看到,Angular 定义的完整触发器集合包括:

  • on idle(可选毫秒/秒参数,如 on idle(2000));
  • on timer(500)(必须带一个时间参数);
  • on interaction / on hover(可选事件选择器参数);
  • on immediate(不允许参数);
  • on viewport(可选引用名或 {rootMargin, amount, trigger} 形式的选项对象,且 root 选项不被支持);
  • on never
  • when 表达式(绑定布尔表达式,动态触发);
  • 以及可与上述触发器组合使用的 prefetchhydrate 前缀(例如 @defer (on viewport prefetch on interaction)@defer (on viewport prefetch on idle))。

几个对实践有直接影响的事实(均可在 r3_deferred_triggers.ts 中确认):

  • 时间参数的格式由正则 ^\d+\.?\d*(ms|s)?$ 约束,解析函数 parseDeferredTime 会把 s 单位乘以 1000 统一为毫秒——所以 on timer(2s)on timer(2000) 等价,而 on timer(2.5ms) 也是合法的;
  • 同名触发器不能重复trackTrigger 在发现重复(如两个 on idle)时直接报错 Duplicate "xxx" trigger is not allowed
  • 参数个数有严格限制timer 必须恰好一个参数、immediate 不允许参数、hover/interaction/idle 最多一个参数。

教程第一课刻意只使用无参数的 @defer 默认行为,正是为了先建立"延迟 = 空闲时自动加载"这一心智模型,后续课程(steps 目录 中的 2-loading-error-placeholder、3-defer-triggers)再逐步引入占位符与显式触发器。

五、源码深挖:on idle 的运行时调度机制

"默认在浏览器空闲时加载"这句话背后的实现位于 packages/core/src/defer/idle_scheduler.ts。核心是一个根级可注入服务 IdleScheduler,以及辅助函数 onIdle(callback, injector, options?)。其设计有四个值得理解的细节:

  1. 按超时选项分桶(bucketing)批量调度IdleScheduler 把所有回调按 IdleRequestOptions 的序列化键(这里就是 timeout 值)分组成桶,每个桶只发起一次 requestIdleCallback。注释中说明了动机——避免 defer 块定义在 @for 循环里时,每个块都单独调用一次 requestIdleCallback,造成调度开销。不同选项的桶互相独立,互不干扰。

  2. 空闲时间片耗尽后自动续订:桶的回调在每次触发时逐个执行队列中的 defer 块渲染函数,每执行一个就调用一次 this.applicationRef._tick() 做变更检测。代码注释特别指出这是"一次优化过的变更检测检查",并且把新建视图的变更检测耗时也计入当前空闲时间片;当 deadline.timeRemaining() === 0 && !deadline.didTimeout 时立即 break,若桶里还有剩余回调则重新 requestIdleCallback 续订。也就是说,多个延迟块不会在一个空闲时间片里硬跑到底,而是尊重浏览器的空闲预算

  3. 回调被包进 NgZone 执行requestIdleCallback 目前不在 Zone.js 的补丁范围内,因此 scheduleBucket 中用 this.ngZone.run(...) 显式把回调拉回 Angular 的 zone,保证 zone-based 应用里变更检测能正常收尾。

  4. 服务端短路:从 packages/core/src/defer/triggering.tsscheduleDelayedTrigger 可以看到,先 renderPlaceholder 渲染占位内容,随后在不应触发 defer 块的场景(如服务端渲染)直接提前返回,避免不必要的 setTimeout/空闲调度延迟服务端序列化——这解释了为什么 SSR 场景下 @defer 主要产出占位 HTML,真正的 JS 加载推迟到浏览器端。

此外,triggering.tsonIdle 的清理函数(cleanup fn)通过 storeTriggerCleanupFn 存入 defer 块的运行时详情,意味着若父视图在空闲回调触发前被销毁,对应的空闲调度会被取消,不会渲染到已删除的视图上。

六、编译器还会为你检查触发器配置错误

Angular 不只是把触发器翻译成代码,还在类型检查阶段主动拦截明显的配置错误。仓库中的扩展检查 defer_trigger_misconfiguration 会分析 on timer / on idle 与其对应的 prefetch 触发器组合:例如主触发器是 timer(2000) 而 prefetch 是 timer(1000),或者两个无时限 idle 组合等可疑配对,会被静态识别并提示。从源码结构看,该检查通过比较主触发器与 prefetch 触发器的类型与时间参数(isTimerPairisIdlePairsameUntimedIdle 等判断)来定位"prefetch 永远无法先于主触发器生效"或"prefetch 配置形同虚设"这类问题。使用 @defer 时建议保持该检查开启,可以在构建期发现触发器时序配置错误。

七、小结与实操要点

回到教程第一课的结论:给模板中某个区域加一个 @defer { ... } 块,就能把该区域内的组件(如 article-comments)从首屏初始包中拆出去,生成独立的 lazy chunk,并在浏览器空闲时自动下载与渲染。结合源码可以看到这一"默认行为"并非空话:

关注点 行为 源码依据
默认触发时机 无参数 @defer 等价于空闲触发,先渲染占位,空闲时再加载 JS triggering.tsscheduleDelayedTrigger
空闲调度方式 按超时选项分桶,一次 requestIdleCallback 服务多个块;时间片耗尽自动续订 idle_scheduler.ts
变更检测 每个块渲染后执行 applicationRef._tick() 优化变更检测 idle_scheduler.ts
触发器合法性 时间参数支持 ms/s 单位、同名触发器不可重复、参数个数受限 r3_deferred_triggers.ts
配置错误拦截 类型检查期检查 timer/idle 与 prefetch 的时序配对 defer_trigger_misconfiguration

后续可以沿着同一教程目录继续学习:第 2 步为 @defer 添加 @placeholder / @error 状态,第 3 步系统学习 on viewporton hoverprefetch 等触发器组合,从而把"延迟加载"从默认的空闲行为扩展为面向交互与滚动位置的精细化调度。

登录后查看全文
热门项目推荐
相关项目推荐