原文:http://www.sencha.com/blog/ext-js-4-1-performance/
在本文,将讲述几个影响Ext JS应用性能的因素。
- 网络延时会严重影响初始化启动时间,尤其是Store的加载时间。
- CSS处理。
- Javascript的执行。
- DOM操作。
网络延时
为了最大限度的减少应用启动时间,必须牢记的是,任何域对浏览器的网络连接并发数量是有限制。
这意味着,如果从一个域请求许多文件,一旦达到上限,随后的下载将要排队,只有当一个连接槽释放时,他们才会被处理。新的浏览器都有较高范围,但对旧的、慢的浏览器进行优化就很重要。
解决办法是使用Sencha SDK工具将应用所需的脚本文件生成一个单一的串联的脚本文件。
具体信息可查看Ext JS 4.0入门。
SDK的“create”命令会分析加载到页面的应用,加载类定义中requires和user属性引用的所有文件,然后以正确的顺序创建一个包含所有所需的类定义的脚本文件。
可在Ext JS 4.1的文档中查阅Sencha类系统的详细信息。
另外一种减少网络延时的方法是在Web服务器对Ext JS的页面及其相关的脚本和样式文件启用GZIP压缩。
CSS处理
CSS选择是跟随DOM的父节点指针从右到左匹配的。
这意味着以下的样式处理过程是,现在文档中找到匹配的span,然后沿着父节点这条轴去寻找符合两个指定样式类名的祖先节点。
- .HeaderContainer .nav span
因而,在元素上使用单一的,确定类名的样式会更高效。
Javascript执行
优化Javascript代码必须牢记以下几点:
- 避免使用旧的或差的Javascript引擎的写法。
- 优化经常重复出现的代码。
- 优化在渲染或布局时执行的代码
- 最好是尽量不要在初始化渲染或布局时执行任何额外的代码。
- 将不变的表达式移到循环外面。
- 使用for循环代替 Ext.Array.each。
- 如果函数中条件内代码经常被调用,必要时,将条件移到函数外(可参考Ext JS代码库对fireEvent的调用)。
- 在差的Javascript引擎安装和拆卸调用框架(让函数调用所需的设备)会很慢。
代码优化示例
试想一下,一个统计应用为Grid的数据列提供了一些操作,在Grid的列标题菜单中增加菜单项,以便对任何调用列进行所需的操作。
处理程序必须获得相关的上下文信息及将要进行操作的当前活动的列标题信息:
在GitHub的示例演示了两种执行这个操作的方式,它可以运行在任何版本的SDK示例目录下。
以下是第一种用来合计活动列的写法:
- function badTotalFn(menuItem) {
- var r = store.getRange(),
- total = 0;
- Ext.Array.each(r, function(rec) {
- total += rec.get(menuItem.up('dataIndex').dataIndex);
- });
- }
这里有几个错误做法。首先,使用Ext.each为数组中的每个记录调用传递函数。正如看到的,函数设置会影响性能。其次,menuItem.up('dataIndex')表达式的结果是不变的,它只需要执行一次,可以放到循环之外。
因此,可将代码优化成以下代码:
- function goodTotalFn(menuItem) {
- var r = store.getRange(),
- field = menuItem.up('dataIndex').dataIndex;
- total = 0;
- for (var j = 0, l = r.length; j < l; j++) {
- total += r[j].get(field);
- }
- }
这可能似乎微不足道的差异,但性能差异显著。
在下面表格,计算功能迭代了10000次后,提供了一个可衡量的时间:
Browser | Bad | Good |
---|---|---|
Chrome | 1700ms | 10ms |
IE9 | 18000ms | 500ms |
IE6 | Gave up | 532ms |
正如所看到的,即使没有迭代,使用第一个方式,IE9都要花费1.8秒去处理操作。
使用页面分析器去衡量性能
页面分析器是SDK的一个示例,在example/page-analyzer目录可找到它。它会在捕捉框架中运行同一个域的页面,然后捕捉Ext JS示例,以便分析布局性能和任何对象的任何方法的性能。
如果是使用Chrome,需要在它的命令行使用“-enable-benchmarking”命令开启微秒计时精度。
要分析以上示例的关键位置的性能,切换到“性能”标签,然后在左下角的标签面板选择“Accumulators”标签,并粘贴以下代码到textarea:
- {
- "Component.up": {
- "Ext.Component": "up"
- },
- "CQ.is": {
- "Ext.ComponentQuery": "!is"
- }
- }
确保迭代两个合计计算函数10000次,才能获得准确的性能状态。
然后在页面分析器中加载Grid性能测试示例,使用“Get total in bad way”开始合计,并单击页面分析器的右上角“UPdate Stats”。
然后单击“Reset”清空accumulators,增加“Build”计时器,使用“Get total in good way”进行合计并单击页面分析器的“Update Stats”按钮。
在“性能”标签的“Grid”子标签内,会看到下图所示的两个运行结果:
从图中可以看到,不变表单式在循环外调用ComponentQuery方法的次数要少得多。
合并多个布局操作
Ext JS 4会根据内容变化或大小变化自动进行布局。这意味着,当一个按钮的文本变化,会导致它所在的工具栏重新布局(因为按钮的高度可能会改变),而工具栏所在的面板也要重新布局(因为工具栏的高度可能改变)。
基于这个原因,在内容或大小发生改变时,将多个布局操作合并在一起显得非常重要,可以使用以下代码实现:
- {
- Ext.suspendLayouts();
- // batch of updates
- Ext.resumeLayouts(true);
- }
参数true的传递意味着重新启用布局,它运行到这点时,会清理任何排队的布局请求。
减少DOM负担
通过减少嵌套的容器或组件以实现尽可能少的层数相当重要,这可以避免重复布局运行和DOM回流,因为这些的开销是很昂贵的。
另外一个要遵循的原则是使用最简单的容器或布局做必要的工作。
常见的例子是在将一个Grid放到TabPanel的时候再嵌套一个组件。以下就是习以为常的写法:
- {
- xtype: "tabpanel",
- items: [{
- title: "Results",
- items: {
- xtype: "grid"
- ...
- }
- }]
- }
不知道为什么要添加一个普通面板作为标签面板的子条目,然后再面板内放置Grid,而这样的嵌套层毫无用处。
而事实上,这破坏了标签面板的运作,因为没有为封装的面板配置布局,从而不能处理它的子组件Grid的大小变化,这意味着它不能自适应标签面板的大小,并根据标签面板的高度进行滚动。
正确的写法是:
- {
- xtype:”tabpanel“,
- items: [{
- title: ”Results“,
- xtype: ”grid“,
- ...
- }]
- }
为什么这个原则很重要?
保持组件树(或DOM树)尽可能的轻量化是相当重要的,其主要原因是在Ext JS 4中,许多组件是带有子组件的容器并有执行自己的布局管理器,例如,面板标题现在是一个容器类,在它里面可以配置除标题文本和工具按钮之外的其它的组件。
虽然将标题作为层次化的容器带来了额外的开销,但是这让UI设计更灵活了。
此外,在Ext JS 4中,组件会使用组件布局管理器去管理内部DOM结构的大小和位置,而不象Ext JS 3.x那样,使用onResize方法去处理。
可视化的组件树
在遵循以上原则去设计UI的时候,可以把UI想象为树结构,例如,一个Viewport可以想象为:
为了渲染yield组件树,它会运作2次。
第一遍,会调用每个组件的beforeRender,然后调用getRenderTree产生可生成HTML标记的DomHelper的配置对象,并将它添加到渲染缓存中。
第一遍之后,一次性将代表整树的HTML代码插入到文档中,这样,就减少了创建应用结构时的DOM处理。
然后树再走一遍,调用每个组件onRender方法将组件连接到他们相关的DOM节点。然后调用afterRender完成渲染处理。
在此之后,完整的初始布局就执行完了。
而这就是为什么创建轻量级UI非常重要的原因。
研究一下下面面板的构成:
- Ext.create('Ext.panel.Panel', {
- width: 400, height: 200,
- icon: '../shared/icons/fam/book.png',
- title: 'Test',
- tools: [{
- type: 'gear'
- }, {
- type: 'pin'
- }],
- renderTo: document.body
- });
它会导致相当复杂的UI结构:
尽可能避免收缩封装(shrinkwrapping,根据内容自动调整大小)
尽管Ext JS 4提供了可自动根据内容调整大小的容器(在Ext JS中简称为“shrinkwrapping”),但这会加重部分布局的负担,其次是计算结果会造成浏览器的回流,随后还要使用计算的高度和宽度重新布局。
避免DOM刷新大小,可提高性能。
尽可能避免限制大小(minHeight, maxHeight, minWidth, maxWidth)
如果限制被命中,那么整个布局就要进行重新计算,例如,一个使用flex定义的盒子布局的子组件在收到其计算宽度小于定义的minWidth时,它会被固定为最小宽度,然后整个盒子布局将不得不重新进行计算。
类似的情形是,一个盒子布局使用stretchMax配置项时,所有子组件将切换为固定的垂直尺寸(例如,Hbox布局高度),而布局则会重新计算。
避免在渲染后处理组件的DOM
为了避免DOM回流和重画,尽量避免在组件渲染后处理其DOM结构。替×××法是在生成HTML代码之前,使用提供的钩子去修改组件的配置。
如果实在是要修改ODM结构,重写getRenderTree方法是最后的方式。
Grid的性能
表格的大小会影响性能,尤其是列的数量,因而要保持尽可能少的列数。
如果数据集非常大,而且不想在UI中使用分页工具条,那么就使用俗称为“无限Grid”的缓冲渲染方式。
要实现这个,只需要在Store中添加以下配置项:
- buffered: true,
- pageSize: 50, // Whatever works best given your network/DB latency
- autoLoad: true
然后像往常一样加载和维护它。
它是如何工作的
Grid会计算渲染表格的大小并根据PagingScroller对象的配置项来监控滚动位置。以下是滚动时需要配置的配置项:
- trailingBufferZone :保持在可视区域上的已渲染记录数。
- leadingBufferZone:保持在可视区域下已渲染的记录数。
- numFromEdge:在表格刷新之前,表格滚动时与可视区域之间的边界值。
渲染的表格需要包含足够的行数来填充视图的高度,还要加上缓冲期的大小,以及前导缓冲区的大小,再加上(numFromEdge * 2)以便创建滚动溢出。
表格的滚动结果,会被监控,并在表格末尾与视图之间的行数小于numFromEdge时,使用数据集中的下一块数据重新渲染表格,然后定位,以便让行的可视位置不变。
在最好的情况,重新渲染所需的行已经在页面缓存中,而操作是瞬时的和察觉不到的。
要配置这些值,可在Grid的verticalScroller配置项中配置:
- {
- xtype: 'gridpanel',
- verticalScroller: {
- numFromEdge: 5,
- trailingBufferZone: 10,
- leadingBufferZone: 20
- }
- }
这意味着将有40行的溢出数据提供给Grid可视区域实现平滑滚动,而重新渲染将会在表格边界与可视区域之间少于5行时发生。
保持管道完整
保持页面缓存为将来的滚动准备数据是Store的工作。Store也有trailingBufferZone和leadingBufferZone。
每当表格请求重新渲染的行时,在返回请求行之后,Store会确保缓存中的数据涵盖两个区域所需的数据,如果数据不在缓存,则会向服务器请求数据。
这两个区域都有相当大的默认值,开发人员可以调小或调大他们保持在管道中的页数。
缓存失败
当“瞬移”到数据集不在缓存部分时,会显示加载遮蔽和延时渲染,因为需要从服务器请求数据,不过,这种情况已经优化过了。
包含显示区域所需数据的页面范围会优先发生请求,当数据一到达就立刻重新渲染。 trailingBufferZone 和leadingBufferZone所需的数据将会在UI所需数据加载后立刻发送请求。
修剪缓存
默认情况下,缓存会计算最大尺寸,除此之外,它还会丢弃最近使用的页。页面数量的大小为滚动条的leadingBufferZone加上可视区域大小,再加上trailingBufferZone和Store的purgePageCount配置项。增加purgePageCount意味着一旦一个页面被访问,就可以很快的返回它,而不是向服务器发送请求。
如果purgePageCount的值为0,意味着缓存可以不断增长,而不用修剪,最终可能增长到包含整个数据集。这在数据集不是大得离谱时是一个非常有用的选项。记住,人类无法理解太多的数据,因而在Grid显示多于千行的数据实际上没有多大用处,这可能意味着,他们使用了错误过滤条件并需要重新查询。
将整个数据集放在客户端
如果数据集不是天文数据集,将整个数据集缓存在页面是可行的。
可以通过SDK示例目录下的(examples/grid/infinite-scroll-grid-tuner.html)的“Infinite Grid Tuner”示例来测试下其可行性。
如果设置Store的leadingBufferZone为50000,并设置purgePageCount为0,这将产生预期的效果。
leadingBufferZone会让Store去保持管道完整,50000意味折非常完整。
purgePageCount为0意味着页数的增长没有限制。
因此,当单击“Reload”,会看到可视区域需要的数据页会最先被请求,然后渲染。
然后,会看到Store会努力去填满巨大的leadingBufferZone。很快,整个数据集就被缓存了,然后,在滚动区域任何地方的数据访问都是即时的。
作者:Nige "Animal" White
Nigel brings more than 20 years experience to his role as a software architect at Sencha. He has been working with rich Internet applications, and dynamic browser updating techniques since before the term "Ajax" was coined. Since the germination of Ext JS he has contributed code, documentation, and design input.