概览
Rich Internet applications (RIAs) 在 Web 2.0 领域非常流行。为了提供新鲜、别致的用户体验,很多网站用 JavaScript 或 Flash 将复杂的内容从后台服务器搬到前台客户端。如果 数据较少的话,这提供了一个方便、新颖,流畅的用户界面(UI)。如果要将大量内容从服务器转移到客户端并在浏览器呈现,性能将显著下降。挑战是找到瓶颈,确定解决方案。
在浏览器中调整性能问题比在 Java 应用程序中更难。开发人员在各种浏览器中调试 JavaScript 的方法要少得多。在 Mozilla Firefox 中,可以使用 Firebug 调试 JavaScript,但仍然不能调整很多性能问题,如浏览器呈现消耗时间。为了解决这些问题,有必要开发浏览器插件来监控时间响应,以及确定其他对应解决方案如部分呈现或延时加载。
学习诊断 web 应用程序中的性能问题,找到客户端内容中的瓶颈,并调整性能。
JavaScript 和 HTML DOM
JavaScript 是用在 Internet 上的最流行的脚本语言。数以万计的 web 页面设计师使用 JavaScript 来改善设计、验证表单、检查浏览器以及创建 cookie。HTML Document Object Model (DOM) 定义了访问和操作 HTML 文档的标准方法。它将 HTML 文档表示成节点树,其中包含元素、属性和文本内容。
通过使用 HTML DOM,JavaScript 能访问 HTML 文档中所有节点并操作它们。很多主流浏览器都支持 JavaScript 和 HTML DOM,而且很多网站也使用它们。它们使用的性能显著影响到整个 RIAs 的性能。
回页首
JavaScript 性能和函数
在 JavaScript 中,当需要某一功能时,使用 函数。尽管有些情形下可以用字符串代替函数,我们还是建议您尽可能使用函数。在 JavaScript 中,函数在使用前会预编译。
例如,看 清单 1 中的 eval
方法。
eval
方法用字符串作参数
function square(input) { var output; eval('output=(input * input)'); return output; } |
eval
方法计算平方值并输出结果,但性能不好。此例使用字符串 output=(input*input)
作为 eval
方法的参数,无法利用 JavaScript 预编译。
清单 2 显示了一个完成此任务的更好的方法。
function square(input) { var output; eval(new function() { output=(input * input)}); return output; } |
使用函数代替字符串作参数确保新方法中的代码能被 JavaScript 编译器优化。
函数作用域
JavaScript 函数作用域链中的每个作用域都包含几个变量。理解作用域链很重要,这样才能利用它。清单 3 显示的是一个函数作用域样例。
function test() { var localVar = “test”; test1(this. localVar); var pageName = document.getElementById(“pageName”); } |
图 1 显示了作用域链结构。
使用局部变量比使用全局变量快得多,因为在作用域链中越远,解析越慢。
如果代码中有 with
或 try-catch
语句,作用域链会更复杂。图 2 显示的是 try-catch 语句的作用域链。
字符串函数
JavaScript 中最不可取的函数是字符串连接。我通常使用 +
号来实现连接。清单 4 显示了一个此类样例。
var txt = “hello” + “ ” + “world”; |
这条语句创建了几个包含连接结果的中间字符串。这样在后台连续创建和销毁字符串导致极低的字符串连接性能。早期的浏览器对这样的操作没有优化。我们建议您创建一个StringBuffer
类来实现,如 清单 5 所示。
function StringBuffer() { this.buffer = []; } StringBuffer.prototype.append = function append(val) { this.buffer.push(val); return this; } StringBuffer.prototype.toString = function toString () { return this.buffer.join(“”); } |
对字符串对象(而非值)定义了所有的属性和方法。当您引用一个字符串值的属性或方法时,ECMAScript 引擎在方法执行前隐式创建一个具有相同值的新字符串对象。此对象只用于特定请求,当下一次使用字符串值的方法时重新创建。
这种情况下,对那些方法会被调用多次的字符串使用新的语句。
新字符串对象的例子如 清单 6 所示。
var str = new String(“hello”); |
StringObject.indexOf 比 StringObject.match 快。当搜索简单字符串匹配时,尽可能用 indexOf 而不用正则表达式匹配。
尽量避免在长字符串中匹配(10KB 及以上),除非您别无选择。如果您确定只在字符串某一特定部分匹配,用子串而不是整个字符串比较。
回页首
DOM 性能
本章简要介绍了一些可进行调整以提升 DOM 性能的内容。
重绘(Repaint)
当之前不可见的内容变得可见,DOM 就会重绘,反过来也一样。重绘也称为重画。此行为不会改变文档布局。不改变元素尺寸、形状或位置,只改变外观也会触发重绘。
例如,给元素添加边框或改变背景色就会触发重绘。重绘的性能代价很大;它需要引擎搜索所有元素以确定哪些可见,哪些必须显示。
回流(Reflow)
回流是比重绘更显著的改变。在回流中:
引擎将会对相关元素回流,以确定各部分显示在哪。子元素也会被回流以反映父元素的新布局。DOM 中元素后面的元素也会被回流,以计算新布局,因为它们可能在初始回流时被移动了。祖先元素也会因子孙元素大小变化而被回流。最后,所有内容都被重绘。
每次向文档添加一个元素,浏览器都要回流页面来计算所有内容如何定位、如何呈现。添加的东西越多,回流次数越多。如果能减少单独添加元素的次数,浏览器回流次数就更少,运行也更快。
回页首
CSS 性能
将 Cascading Style Sheets (CSS) 放在顶端。如果样式表放在底部,将最后加载。之前几秒钟,页面都是空白,浏览器等待样式表加载,然后页面上其他东西才能呈现 — 甚至是静态文本。
在 Internet Explorer 中,@import
与在底部使用 效果一样。我们建议您不要使用。
缩写属性
使用缩写属性在一个声明中一次设置几个属性,而不是每个属性用一个声明。使用缩写属性,可以减小文件大小,降低维护量。
例如,可以设置背景、边框、边框颜色、边框样式、边框侧(顶部边框、右侧边框、底部边框、左侧边框)、边框宽度、字体、页边距、轮廓、填充属性。
CSS 选择器
CSS 选择器通过从右到左 移动来匹配。清单 7 所示,浏览器必须遍历页面中每个锚元素以确定它的父元素 ID 是否是 aElement
。
#aElement > a{ font-size: 18px; } |
如果从代码中移除 >
,如 清单 8 所示,性能更糟。浏览器要检查整个文档中的所有锚。这样就不是只检查锚的父元素,而是顺着文档树向上查找 ID 为 aElement
的祖先元素。如果正在检查的元素不是 aElement
的子孙,浏览器就要沿着祖先元素的树查找,直到文档根部。
#aElement a{ font-size: 18px; } |
回页首
最佳实践
本章将略述能帮助您提升 web 应用程序性能的最佳实践。
减少 HTTP 请求数
每个 HTTP 请求都有开销,包括查找 DNS、创建连接及等待响应,因此削减不必要的请求数可减少不必要的开销。要减少请求数:
mod_concat
模块。如果 httpServer 是 Apache,用 pack:Tag 作为 JSP 标签库来合并 JavaScript 和样式表文件。(pack:Tag 是一个 JSP-Taglib,可缩减、压缩及合并资源,如 JavaScript 和 CSS,并将它们在内容或普通文件中缓存。)后置加载组件
只呈现需要的组件;其余可等待。最好不要一次呈现太多组件。
某些情况下,可使用后置加载。由于浏览器可视区域外的组件可以后置加载,当这些组建进入可视区域不久后,初始呈现就会失效。
有些 JavaScript 可以在 onload 事件后后置加载,如 JavaScript 中初始呈现后拖动某个元素。
前置加载组件
通过前置加载组件,可以利用浏览器的空闲时间请求将来会用到的组件(如图像、样式和脚本)。当用户访问下个页面时,如果大多数组件都已在缓存中加载,那页面加载会快得多。
有两种前置加载:
将脚本放在底部
脚本可能会产生问题,因为它们可能会阻碍并行下载。当下载脚本时,浏览器不会再启动其他下载 — 即使那些位于不同主机。将脚本,如样式表,放在底部,以保证它们在其他下载完成后再下载。
也可以使用延时脚本,这只有 Internet Explorer 支持。DEFER
属性表示脚本不含document.write()
。这就告诉浏览器他们可以持续呈现。
使用无 cookie 域组件
当浏览器发出对静态图片的请求,并随之发送 cookie 时,服务器不会使用那些 cookie。由于这些 cookie 只会造成不必要的网络流量,确保用无请求来请求静态组件。然后使用子域和主机保存这些静态组件。
将 JavaScript 和 CSS 放在外部
现实世界中使用外部文件通常会使页面运行更快,因为 JavaScript 和 CSS 文件被浏览器缓存。HTML 文档内联的 JavaScript 和 CSS 会在每次请求 HTML 文档时被下载。这减少了需要请求的 HTTP 的数量,但增加了 HTML 文档的大小。另一方面,如果 JavaScript 和 CSS 在被浏览器缓存的外部文件中,就会减小 HTML 文档大小,而不会增加请求数。
回页首
RIA 小部件性能
主流 RIA Ajax 框架,如 ExtJS、YUI、Dojo 及其他,都提供一些精巧的小部件库,以增强用户体验。与其他框架相比,Dojo 在企业开发领域更强大,这是由于:
Dojo 在很多网站广泛使用。我们将使用 Dojo 举例,分析 RIA 小部件的性能。可根据具体情况使用 Dojo 小部件调整工具,有 Page Speed、Rock Star Optimizer 及 Jiffy。我们强烈建议使用 YSlow 和 Firebug。
YSlow
YSlow 根据一组高性能 web 页面准则,通过检查页面上所有组件,包括由 JavaScript 创建的,来分析 web 页面性能。YSlow 是一个集成了 Firebug web 开发工具的 Firefox 插件;它可提供提升页面性能的建议、总结组件性能、显示页面统计数据并提供用于性能分析的工具。
图 3 显示的是 YSlow Grade 选项卡上的信息。
YSlow 的 web 页面建立在 22 条可测试的规则基础上,这些规则在下方按重要性和效果排列。研究显示,按照以下规则,web 页面响应时间可提升 25% 到 50%:
GET
进行 Ajax 请求。图 4 中的 YSlow Statistics,对空缓存的访问用户和之前访问过页面的用户的页面大小做了对比。
Components 选项卡显示了每个组件及相关的性能信息。例如,如果组件被 gzip 压缩,或 ETag 有内容(如果的话),您都能看到。组件大小和超期时间也显示在 Components 选项卡中,如 图 5 所示。
Firebug
Firebug 与 Mozilla Firefox 集成,使您在浏览网站时有大量开发工具随手可用。可以即时编辑、调试、监控 web 页面中的 CSS、HTML 和 JavaScript。
可以使用 Firebug Net 面板,如 图 6 所示,监控 web 页面产生的 HTTP 流量。它向用户展示了所有收集到的和计算出的信息。每个条目表示页面的一个请求/响应来回。
Firebug Console 面板,如 图 7 所示,提供了两种监控代码性能的方法。
console.time(timeName)
函数测量某个特定代码或函数执行多长时间。该特性对于提升 JavaScript 代码的性能非常有用。 清单 9 显示了一个样例。
var timeName = 'measuringTime'; console.time(timeName); //start of the timer for(var i=0;i<1000;i++){ //do something console.timeEnd(timeName); //end of the timer |
measuringTime:xxms
将显示在控制台。
回页首
Dojo 小部件性能
本章将探索能改进 Dojo 小部件性能的方法。
加载成本
如 “Improving performance of Dojo-based web applications" (E. Lazutkin, Feb 2007)中所指出,大多数 Dojo 用户对它的第一印象是,它非常巨大。例如,dojo-release-1.5.0-src.zip 是 19M,即使压缩过的 dojo-release-1.5.0.zip 也有 4.5M。最简版本中的大多数文件都是多余的,永远都用不到。所有的 Dojo 版本都有全部 Dojo 文件的拷贝及合并了所有常用文件的定制 dojo.js 文件。缩减加载成本的最佳方法是使用合适的 Dojo 版本。
dojo.js 可激活 Dojo 对象,并动态加载其余模块,除非它们已经被 dojo.js 中可选部分加载。当浏览器第一次加载 dojo.js 文件时,它将会上传并安装以下文件:
为了减小加载时间,需要考虑哪个版本最适合您的应用程序。否则,就要定制一个 Dojo 版本。更多有关 Dojo 文档信息,见 参考资料 一章。
解析成本
为了最小化 Dojo 小部件解析成本,使用以下两个方法来优化初始化:
dojoType
属性创建一个带有 HTML 标签的 Dojo 小部件,如 清单 10 所示。此方法运行的前提是 dojo.parser 包含在
dojo.require("dojo.parser");
中,并且
djConfig="parseOnLoad:true"
。这是个很简单的方法,可在轻量级代码中声明组件。页面中所有具有
dojoType
属性的标签都会在文档加载后自动解析。该方法对小型应用程序非常方便,但会显著增加具有大量 HTML 的 web 应用程序的启动时间。解析器将访问所有元素,检查是否要解析。使用配置文件,如 Firebug 中所提供的。
如果发现在 dj_load_init()、modulesLoaded() 或其他类似初始装载的内容上花费过多时间,就考虑小部件初始化。
Listing 10. 创建 dojoType 的 Dojo 小部件
id="errorDialog" dojoType="dijit.Dialog" title='dialog1' class="pop-window"/> |
new
创建小部件。要创建小部件,必须输入两个参数:一个具有属性的 JSON 对象和要解析的元素。每个小部件至少需要两个语句。 清单 11 是一个样例。
清单 11. 用 JavaScript new
创建一个 Dojo 小部件
new dijit.Dialog({"title":"dialog1 "},dojo.byId("dialog1")); |
提升解析性能
为了优化代码结构和性能,可考虑在创建小部件时提升解析。通过将 parseWidgets
设置为 false 来禁用自动解析,然后创建一个数组来设置元素的 ID,如 清单 12 所示。也可在运行时动态放入新元素的 ID。当文档加载时,用 dojo.forEach()
解析数组中所有元素。
.... .... ........ |
解决方案
Dojo 网格的性能问题主要与输入/输出操作、大量数据访问和浏览器数据呈现有关。可以通过使用合并网格特性的机制来提升 Dojo 网格小部件的性能。
回顾缓存机制的使用。当数据从数据库加载到本机时,在内存中保存数据一段时间。这是减小服务器端请求数据的响应时间的好办法。这样直到用户更新或修改网格中的数据时才发送请求。缓存机制一般是通过 Dojo 网格本身实现的,但用户对网格进行某种操作时,问题就会发生。以下场景揭示了这些问题:
解决方案是您自己在数据存储层重定义排序函数。下方的 清单 13 和 清单 14 演示了怎么做:根据 onHeaderCellMouseDown
函数重写排序逻辑,呈现数据,并更新网格的头部标题视图。
grid.onHeaderCellMouseDown = function(event){ var items = DataStore.data.items; //Sort the "Name" column which might contain characters like Chinese and so on if (event.cellIndex == 1) { sortAscending = ! sortAscending ; //Change the string to localestring before comparison with localeCompare method if (sortAscending) { items.sort(function(m, n){ return m["name"].toString(). localeCompare(n["name"].toString()); }); }else { items.sort(function(m, n){ return n["name"].toString(). localeCompare(m["name"].toString()); }); } }else //Sort the "Date" column if (event.cellIndex == 2) { sortAscending = !sortAscending; //Compare the date with milliseconds computed from 1970/07/01 if (sortAscending) { items.sort(function(m, n){ return Date.parse(m["date"].toString()) - Date.parse(n["date"].toString()); }); }else { items.sort(function(m, n){ return Date.parse(n["date"].toString()) - Date.parse(m["date"].toString()); }); } } } |
//"sorColIdx" is the index of the column to be sorted updateGridAfterSort : function(sortColIdx){ //Render the data of the grid var store = new dojo.data.ItemFileWriteStore(DataStore.data); grid.setStore(store, null, null); grid.update(); //Update the header view of the gird var headerNodeObjs = document.getElementsByTagName("th"); for (var i = 0; i < 2; ++i) { //"gridLayout" is a global array defining the layout of the grid var headerNodeObjName = gridLayout[0].cells[0][i].name; var headerNodeHtml = [' |
getItem
。 清单 15 显示了一个样例。
清单 15. 将从数据库取出的数据缓存到数组中,并查找
//Fetch data from database getData : function() { function callback(ResultSet) { //ResultSet is an array of records fetched from database and make variable //rawData refer to it to cache it in memory GlobalVaribles.rawData = ResultSet; //Convert the raw data ResultSet to JSON for data store use GlobalVaribles.dataJSON = JSONUtil.convert2JSON(ResultSet); } DBUtil.fetchAll(callback); } //Search functionality search: function(col, value){ if (value == null || value == "") { return; } //Used here var rawData = GlobalVaribles.rawData; var newResults = []; for (var i = 0; i < rawData.length; i++) { var result = rawData[i]; //Fuzzy match if(result[col].toLowerCase().indexOf(value.toLowerCase()) != -1){ newResults[newResults.length] = result; } } //Render the new results GridManager.renderNewResults(newResults); } |
默认情况下,网格不会启用延时加载机制;必须显式启动。清单 16 演示了用两种方法启动。rowsPerPage
和 keepRows
属性是关键组件。
清单 16. 启动演示加载机制
//The programmatic way var grid = new dojox.grid.DataGrid({ store: store, //data store structure: gridLayout, rowsPerPage: 10, //Render 10 rows every time keepRows: 50, //Keep 50 rows in rendering cache }, "grid"); //The declarative way using HTML label |
清单 17 显示的是分页技术的基本实现。首先,构造一些 JSON 对象,供数据存储用于初始分页条,然后当用户单击分页条的最后一页时添加一些新的 JSON 对象。
//Fetch data from database and convert them to JSON objects getData : function() { function callback(ResultSet) { GlobalVaribles.rawData = ResultSet; //"convert2JSONs" method convert the raw data to several JSON objects //stored in Global array "dataJSONs". GlobalVaribles.dataJSONs = JSONUtil.convert2JSONs(ResultSet); } DBUtil.fetchAll(callback); } //Initial status var dataStore = new dojo.data.ItemFileWriteStore({data:GlobalVaribles.dataJSONs[0]}); var grid = new dojox.grid.DataGrid({ store: dataStore , structure: gridLayout, }, "grid"); grid.startup(); //Assuming that the user clicks the i-th item in the paging bar, we just update the data //store for the grid simply and the performance is still very good. dataStore = new dojo.data.ItemFileWriteStore({data:GlobalVaribles.dataJSONs[i-1]}); grid.setStore(dataStore, null, null); grid.update(); |
回页首
结束语
本文中,您学习了如何识别您的 web 应用程序中的一些问题或瓶颈。您现在以了解一些工具、窍门和技巧,以用于调整和改善对用户的性能。