AngularJs优化
例子
An AngularJS log viewer
本质上,日志视图就是一个日志消息的列表,每个字都可以点击。所以把Angular的指令 加到DOM元素中,简单实现如下:
<span class='logLine' ng-repeat='line in logLinesToShow'> <span class='logToken' ng-repeat='token in line'> {{token | formatToken}} </span> </span>
在单页面应用中有个数千个tokens是很正常的,在早期的测试中,我们发现进入日志的 下一页会花费好几秒来执行JavaScript。更糟的是,不相关的操作(比如点击导航下拉框) 延迟也不轻,AngularJS的大神说最好把数据元素绑定的数量控制在200以下。对于一个单词 就是一个元素的我们来说,早已远超这个数。
分析
用Chrome的JavaScript profiler工具,我们可以快速定位两个拖延点。首先,每次更新要
花大量时间在DOM元素的创建和销毁上,如果新的view有不同的行数,或者任何一行有不同
数量单词,Angular的ng-repeat
指令就会创建或者销毁DOM元素,这个代价太大了。
其次,每一个单词都有自己的change watcher,AngularJS会watch这些单词,一旦鼠标点击 就会触发,这个是影响不相关操作(下拉菜单导航)延迟的罪魁祸首。
优化#1:缓存DOM elements
我们创建了一个ng-repeat
指令的变体,在我们的版本中,如果绑定数据的数量减少了,
超出的DOM元素会隐藏而不是销毁,如果元素的数量过会儿有增加了,我们会重用这些缓存
的元素。
优化#2:Aggregate watchers
用来调用change watchers的所有时间大部分都浪费了,在我们的应用中,特定单词上的
数据绑定都是永远不会改变的除非整个日志消息变化,为了达成这一点,我们创建了一个
指令hides
隐藏掉了子元素的change watchers,只有等特定父元素表达式修改的时候才会
调用他们。就这样,我们避免了在每一次鼠标点击或者其他微小的修改而导致的全盘change
watchers(为了实现这个想法,我们稍微修改了AngularJS的抽象层,我们稍后再细说)。
优化#3:推迟元素创建
前面说了,我们为日志里的每一个单词单独创建了DOM,我们可以利用每一行的单个DOM元素 得到相同的视觉呈现;其他元素都是为响应鼠标点操作而创建的,因此,我们决定推迟这 部分创建,只有当鼠标移动到某行的时候我们再创建他。
为了实现这个,我们为每一行创建了两个版本,一个就是简单的文本元素来显示完整的日志 信息,另外一行就是个占位符,用来显示最终为每一个单词填充后的效果。这个占位符开始 是隐藏的,当鼠标移动到那一行的时候才会显示,而简单文本那一行这个时候就隐藏掉。 下面会讲到,显示占位符是如何填充单词元素的。
优化#4:避开对隐藏元素的监视
我们创建了另外一个指令,用来阻止对隐藏元素的监视,这个指令支持优化#1,相较于 原数据,我们多了更多的隐藏DOM节点,所以必须消除对多出来的DOM节点的监视。这也支持 优化#3,让推迟单词节点的创建更加容易。因为直到这行数据的tokenized版本出现我们 才会创建他 。
下面的代码就是所有的优化后的样子,我们自定义的指令是粗体显示。
<span class='logLine' sly-repeat='line in logLinesToShow' sly-evaluate-only-when='logLines'> <div ng-mouseenter="mouseHasEntered = true'> <span ng-show='!mouseHasEntered'>{{logLine | formatLine }} </span> <div ng-show='mouseHasEntered' sly-prevent-evaluation-when-hidden> <span class='logToken' sly-repeat='tokens in line'>{{token | formatToken }}</span> </div> </div> </span>
Sly-repeat
是ng-repeat
的变体,用来隐藏多出来的DOM元素而不是销毁他们,
sly-evaluate-only-when
阻止内部change watchers除非logLines
变量修改,
sly-prevent-evaluation-when-hidden
主要负责当鼠标移动到指定行的上面的时候,
隐藏的div才显示。
这里展示出了AngularJS对于封装和分离的控制力,我们做了复杂的优化但是并没有影响 模板的结构(这里展示的代码并不是真正产品里的代码,但是他展示了所有的要点)。
结果
我们来看一下效果,我们添加了一些代码来衡量,从鼠标点击开始,一直到Angular的
$digest
循环结束(意味着更新DOM结束)。
我们衡量点击「下一页」按钮的性能是通过Tomcat日志,环境用的是MacBook Pro上的Chrome ,结果见下表(每个数据都是10次测试的平均值):
数据已经缓存 | 从服务器获取数据 | |
---|---|---|
简单实现 | 1190 ms | 1300 ms |
优化后 | 35 ms | 201 ms |
这些数据不包括浏览器用在DOM布局和重绘(JavaScript执行完成后)的时间,每次大概30 毫秒。尽管如此,效果也显而易见;下一页的响应时间从1200毫秒骤降至35毫秒(如果算上 渲染是65毫秒)。
「从服务器获取数据」里的数据包括了我们使用AJAX从后端获取log数据的时间。这个跟点击 下一页按钮不同,因为我们预取下一页的log数据,但是或许适用于其他的UI响应。即使 这样,优化后的程序也可以做到实时更新。
总结
这些代码正式运转俩月了,结果相当让人满意。想看实际效果的请移步scalyr.com点击页面 最下面的「Try The Demo」链接,然后点击「Log View」,试一下下一页按钮。很快是吧。 你一定不敢相信这是从一个运行着的server上看到实时数据。
实现上述优化确实花了不少时间。看起来是我们创建了一个自定义指令用来生成所有的log
视图,绕开ng-repeat
。然后,这些都是有违AngularJS精神的,还要承担代码维护的成本
,测试成本以及其他因素。因为log视图是我们对AngularJS做的测试工程,我们需要验证
这个解决方案的可行性。而且,我们创建的这些新指令已经用到了应用的别的部分了。
我们尽最大努力践行Angular精神,但是我们必须对AngularJS的抽象层做出修改才可以实现
这些优化。我们僭越了Scope的$watch
来拦截watcher注册,然后必须倍加小心的操作
Scope的实例变量来控制watcher在$digest
过程里的运行。
下一次
这篇文章讨论了一系列技术点,我们是效率最大化的忠实拥趸,前面介绍的优化只是我们用 的一些小妙招而已,在后续的文章里我们会继续讨论如何减少网络请求,网络延迟,服务器 执行时间等等。当然我们会继续讨论在开发自己的应用时是如何构建AngularJS的。如果你 对这些感兴趣,请留下你们宝贵的意见。