当 AngularJS 应用程序变大时,很多问题就开始显现出来了,比如多层级视图的加载问题,如果在子视图显示之前没有预加载,则可能在需要展示时,发生视觉闪烁的情况。这种问题在网络缓慢,或者服务器使用较慢的 https 连接时更容易出现。

本文将讨论更高效加载 AngularJS 视图的系统方法。

AngularJS 视图一般原理

AngularJS 视图也并不是什么特别神奇的技术,在其内部就是按普通的 directive 来处理的。也就是说,当一个位置需要显示 view 时,AngularJS 会尝试使用某种方法获得其 HTML 模板文件的具体内容、包装成 directive,执行 directive 的标准流程,最后添加到页面上。

回想一下,directive 本身是不是正好也支持 templateUrl 属性?这就与 view 技术衔接上了。

这样说来,是不是视图模板也可以使用行内 DOM 甚至是字符串字面量值了呢?答案是肯定的!我们本来就可以使用一段行内 DOM 来作为 view 的模板。例如:

当然,作为一个大型的 AngularJS 应用程序,将所有 view 都放在字符串值里,或者行内 DOM 里是不太现实的,我们希望可以使用多个小的 HTML 文件来作为子模板。这样,虽然整个应用很大,但每个子模板的文件并不大,一般都是几 KB 的小文件,当用户点击到指定位置,需要时使用对应界面的模板时再去加载,也就显著提高了效率。

我们可以用下图来表示“行内 DOM”与“多个子模板文件”的性能对比:

AngularJS 对视图加载的优化

上面提到了“多个子模板文件”的模板组织方式,这本是一件很平常、很自然的工作方式而已。也正是因此,才让人们感觉 AngularJS 工作方式与自己的期望的一致:因为在没有使用 AngularJS 之前,人们在开发一个 Web 应用时,页面就是这样一个个组织的。

即使在以前,我们在提到性能的时候,自然会想到“缓存”。在以前,页面与页面之间的跳转使得每个页面都是相互独立的单位,因此页面内容的缓存只能有赖于浏览器了。而今,AngularJS 让所有页面子模板都在“单页应用”中加载,于是,我们在这个单页面应用内便获得了缓存页面内容的机会。AngularJS 中内建了缓存机制 templateCache:只要已经加载过某个页面子模板,就会在 templateCahce 中缓存起来,下次从服务器加载页面模板之前,先检查 templateCache,如果已有缓存则不需要从服务器上加载,直接使用。




AngularJS 中内建了 templateCache 机制之后,加载视图的过程变得高效而轻松,Web 应用本身,以及开发者都不需要关心这一过程。不过,即使有页面内的 templateCache,页面模板在初次使用时还是需要从服务器加载,因此偶尔能见到一些视觉闪烁的情况,比如标签切换、页面跳转等。


对 AngularJS templateCache 的优化

作为一种优化手段,我们很自然能想到,既然页面能够在加载之后在 templateCache 起来就能提高性能,如果在应用启动之初 templateCache 中就有了所有页面的缓存,也就根本不需要服务器了,那么在页面需要显示时,也就基本不需要加载时间了。图可以变成这样:




要实现这一目标,只需要在发布应用之前,构建额外的 templates.js 文件,在其中将所有的页面模板读取出来并提前 put 到 templateCache 中,再将形成的 templates.js 嵌入到应用中即可在 Web 应用启动时就已经拥有所有页面模板内容的缓存版本了。
不过,对于大型 AngularJS Web 应用来说,我们很快发现一个问题:这个 templates.js 文件本身的体积迅速大了起来,它又会成为一个新的性能问题。于是,我们可以使用另一个已有的经验:“异步加载”。有了异步加载的支持,在加载 templates.js 的请求还没有完成之前,可以“降级”使用 AngularJS 内建的机制,而一旦 templates.js 加载完成,就立即拥有了所有模板的缓存。



理想中,templateCache 最好能达到最佳的性能表现,但实际应用中,如果不加优化,templates.js 文件本身的体积会令这种优化效果有所折扣,而加上异步加载 templates.js 和降级到逐个加载单个 htm 模板文件之后,又有了一些改善。

浏览器缓存

现在再来讨论一下浏览器缓存,可以结合上一节的 templates.js 一起来讨论了。浏览器缓存是浏览器里内置的一种缓存功能,当服务器正确配置了对 htm 和 js 文件的缓存支持时,浏览器将按指示缓存这些文件。不管是对一个个 htm 模板,还是对 templates.js,都可能被缓存。也就是说,只要在服务器上正确配置,那么上一节所述的“异步 templates.js”,以及“降级的多个 htm 模板文件”都可以被浏览器缓存。这样,我们将加载 htm 模板文件和 templates.js 的需求都减少到第一次使用应用之时。

但在服务器上配置缓存也需要谨慎,如果配置不当,就会出现当服务器上文件已经更新,但客户端浏览器仍在使用老的缓存版本的问题。由于 AngularJS 应用使用绑定表达式显示界面,因此如果程序已经更新,而视图还是老版本,那么绑定表达式很可能失效。这种情况下,轻则局部界面错乱,重则整个 Web 应用完全无法使用。



浏览器缓存原本是一个“杀手锏”,不管是只使用单个模板文件,还是使用 templateCache,浏览器缓存都可以极大地改善其性能效果。但一旦缓存配置不当致使客户端浏览器里使用了错误的版本,就直接导致应用错误,更不谈性能表现了。
要处理缓存问题也有成熟的经验可供借鉴:也就是在文件名上使用版本号,每次需要更新文件内容时,同时更改版本号,那么整个文件名也就发生变化,也就不会发生缓存版本错误问题。结合上面的论述,我们在 templates.js 上添加上版本号,另一方面配置 AngularJS,在加载单个 htm 模板文件时,也会在请求上附上版本号,即可解决这一问题。当然,我们希望在开发时,标记要使用的视图模板时,不需要指定这个需要经常变化的版本号,从而最大程度地保障开发体验,并将维护成本降到最低。



总结

上面讨论了 AngularJS 视图各种可能的方式,分别实施的方法,以及其性能表现差异。主要值得关注的是经优化的 templateCache 机制,以及结合浏览器缓存的 templateCache 方法。总结来说,可以形成这样一个更直观的图形:




经过一番努力,最终我们能够达到这样的结果:

  1. 在应用里添加仅在生产环境才生效的策略:支持在加载视图模板文件时在文件名中添加版本号(从页面中 templates.js 的文件路径中分析版本号)
  2. 开发时不需要经过改变
  3. 发布时预读取所有模板的内容,并生成带版本号的 templates.js,嵌入应用页面中
  4. 在服务器上配置所有 htm 模板文件及 templates.js 的缓存策略为“允许缓存”
  5. 用户首次使用应用时,集中所有网络带宽加载 AngularJS 基础脚本,以及应用程序业务逻辑系统,令应用程序尽早能够使用;此时应用使用 htm 模板文件作为视图模板
  6. 异步加载 templates.js;加载完成之后应用开始使用页面内模板缓存
  7. 用户再次使用应用时,从浏览器缓存中加载 templates.js
  8. 再次发布应用时,修改 templates.js 文件名中的版本号,嵌入页面中
    所以,在首次用户使用应用时,其网络加载图形就像这样:



    最先加载的是应用程序 AngularJS 框架本身,以及业务逻辑,这时候应用已经可用;此时再异步去加载 templates.js 文件。事实上,上面的图形即是我们实际项目中的状况,具体实现在这里就不贴了,也欢迎读者一起探讨更多的可能性。

从本文的讨论不难看出,只要通过各种方法,好好管理浏览器的加载行为,形成一个系统方法,便能令视图加载的性能表现变得更好。