Angular 中的路由完全渲染检测
背景
评估应用性能的关键要素之一是测量从一个视图跳转到另一个视图所需的时间(更多说明见文末)。如果能够长期追踪应用视图间的渲染时间,你就能拥有强大的洞察力,一眼看出哪些路由出现了意外的卡顿。
你在测量什么?
当你测量从一个视图跳转到另一个视图所需的时间时,你真正测量的是什么?虽然我们可以花一个下午的时间讨论哪些指标最为重要,但我将另辟蹊径。让我先分享一下我需要解决的用户故事。
作为用户,我想测量从我开始浏览到下一页完全渲染所需的时间。
所以,一旦导航开始,用户就想知道下一个视图完全渲染需要多长时间。太好了!现在我知道要测量什么了,那么如何在 Angular 中实现呢?我们来谈谈如何检测这两个不同的时刻:导航开始和视图完全渲染。
检测到“导航启动”
所有带有路由的前端框架都可以轻松检测导航的起始点,Angular 也不例外。在 Angular 中,我们可以使用 `onEventListener` 来实现Router这一点。以下代码块可以运行,仅捕获NavigationStart事件,然后跟踪事件发生的时间。
@Injectable({providedIn: 'root'})
export class PerfService {
navStart$ = this.router.events.pipe(
filter( event => event instanceof NavigationStart),
startWith(null), // Start with something, because the app doesn't fire this on appload, only on subsequent route changes
tap(event => /* Place code to track NavigationStart here */),
).subscribe();
constructor(private router: Router) {}
}
上面的代码成功地检测到了导航的开始。很简单!
现在让我们看看当下一个视图完全渲染后我们需要做什么。
检测到“视图已完全渲染”
Angular 并没有提供让我们直接知道视图是否已完全渲染的方法。Angular 或 React 中都没有生命周期钩子会发出“一切就绪,我已完全渲染!”这样的信号。Angular 中最接近的机制是 `onDescription` AfterViewInit,但它只能告诉你组件的初始视图已经渲染完成。任何向服务器发出的请求都会破坏这个状态。
那么现在怎么办?
过于简单的方法
检测视图何时完全渲染的最简单方法是查看其代码,一旦所有数据都已检索并设置到组件上,就可以说此视图已完全渲染。
但想想看……
您必须修改应用程序中的每个组件,使其能够检测并报告Fully Rendered事件。这意味着三件事:
- 您需要修改应用程序中的每个顶级组件,并使其能够报告此事件。
- 您需要创建一份审计报告,以确保您的团队成员不会在以后的任何路线中忘记报告此事件。
- 报告此操作的代码
Fully Rendered分散在整个应用程序中。🤢
这并非一个理想的解决方案。所以,让我来告诉你我是怎么做的。我想要一种无需手动修改每个组件并将检测代码分散到整个应用程序的方法。我想要一种联合式的方法,将所有检测代码集中在一个地方。那么,我该如何实现呢?
实现完全渲染的精确方法
zonejs来救场啦!使用 Angular 的一大好处就是你zonejs的应用里已经包含了它。而对于这个问题来说,zonejs它简直完美!
当 Angular 应用中发生任何值得注意的事情时,它NgZone会提示应用不稳定。一旦所有值得注意的事情都已完成,它又会提示应用恢复稳定。因此,一旦NavigationStart触发该事件,你就可以要求NgZone它在所有值得注意的事情都完成后再次通知你。这样,你就可以区分应用正在渲染下一个视图和应用完成数据检索并渲染下一个视图这两个状态。太棒了!
在 Angular 中,任何值得注意的操作都会在 Angular 区域中创建一个称为宏任务(macrotask)的组件。每当我们获取新数据或组件决定是否需要重新渲染时,都会在区域中创建一个宏任务。因此,我们可以查询该区域是否有待处理的宏任务。
注意:我没有使用 `is` 函数,zone.isStable因为有时zone.isStable即使有待处理的宏任务,它也会返回 true。所以最好查看待处理的宏任务。
火灾发生后NavitationStart,你可以使用类似这样的代码:
// Once NavigationStart has fired, start checking regularly until there are
// no more pending macrotasks in the zone
// Have to run this outside of the zone, because the call to interval
// is itself a macrotask. Running that interval inside the zone will
// prevent the macrotasks from ever reaching zero. Running this outside
// of Angular will not track this interval as a macrotask. IMPORTANT!!
this.zone.runOutsideAngular(() => {
// Check very regularly to see if the pending macrotasks have all cleared
interval(10)
.pipe(
startWith(0), // So that we don't initially wait
// To prevent a memory leak on two closely times route changes, take until the next nav start
takeUntil(this.navigationStart$),
// Turn the interval number into the current state of the zone
map(() => !this.zone.hasPendingMacrotasks),
// Don't emit until the zone state actually flips from false to true
distinctUntilChanged(),
// Filter out unstable event. Only emit once the state is stable again
filter(stateStable => stateStable === true),
// Complete the observable after it emits the first result
take(1),
tap(stateStable => {
// FULLY RENDERED!!!!
// Add code here to report Fully Rendered
})
).subscribe();
});
沃拉
至此,我们已经检测到了事件发生时间NavigationStart和Fully Rendered事件发生时间。现在我们可以使用类似工具PerfumeJS来测量这些时间。PerfumeJS测量完成后,我们需要将这些数据导入分析数据库,以便跟踪这些时间的变化。
按路由查看这些数据随时间的变化,能让你以非常有意义的方式了解你的应用。如果某个 API 运行速度变慢,受影响的视图加载速度也会变慢,而这项测试会告诉你这一点。如果有人提交了一些糟糕的代码,导致应用性能严重下降,这项测试也能告诉你。
我建议大家开始检查自己应用程序的性能,并使用类似这样的方法来进行检查。
事后思考
如果你的应用内存在长时间运行的setInterval调用,那么应用的进程可能macrotasks永远不会稳定下来。如果这种方法对你无效,那么你的应用可能存在其他问题。很容易找到第三方组件或库,它们可以setInterval阻止应用的进程稳定下来,使其无法将待处理的宏任务数量清零。如何查找和修复这些问题是另一篇博文的主题,以后有机会再详细讨论。如果你也遇到这种情况,并且想讨论如何解决,请在 Twitter 上私信我 :)
补充说明
关于衡量应用性能最重要的指标……有很多指标可以衡量,例如首字节响应时间(TTFB)或首段有效内容响应时间(TTFC) 。本文并非要宣称这是前端性能衡量中最重要的指标,但它确实是评估应用性能时需要考虑的重要因素。
文章来源:https://dev.to/herodevs/route-filled-rendered-detection-in-angular-2nh4
