提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html

概览

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 方法。


清单 1. eval 方法用字符串作参数
				
function square(input) {
  var output;
  eval('output=(input * input)');
  return output;
}

eval 方法计算平方值并输出结果,但性能不好。此例使用字符串 output=(input*input) 作为 eval 方法的参数,无法利用 JavaScript 预编译。

清单 2 显示了一个完成此任务的更好的方法。


清单 2. 使用函数作参数的 Eval 方法
				
function square(input) {
  var output;
  eval(new function() { output=(input * input)});
  return output;
}

使用函数代替字符串作参数确保新方法中的代码能被 JavaScript 编译器优化。

函数作用域

JavaScript 函数作用域链中的每个作用域都包含几个变量。理解作用域链很重要,这样才能利用它。清单 3 显示的是一个函数作用域样例。


清单 3. 函数作用域
				
function test() {
  var localVar = “test”;
  test1(this. localVar);
var pageName = document.getElementById(“pageName”);
}

图 1 显示了作用域链结构。


图 1. 作用域链结构
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第1张图片 

使用局部变量比使用全局变量快得多,因为在作用域链中越远,解析越慢。

如果代码中有 with 或 try-catch 语句,作用域链会更复杂。图 2 显示的是 try-catch 语句的作用域链。


图 2. Try-catch 作用域链结构
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第2张图片 

字符串函数

JavaScript 中最不可取的函数是字符串连接。我通常使用 + 号来实现连接。清单 4 显示了一个此类样例。


清单 4. 字符串连接
				
var txt = “hello” + “ ” + “world”;

这条语句创建了几个包含连接结果的中间字符串。这样在后台连续创建和销毁字符串导致极低的字符串连接性能。早期的浏览器对这样的操作没有优化。我们建议您创建一个StringBuffer 类来实现,如 清单 5 所示。


清单 5. StringBuffer 对象
				
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 所示。


清单 6. 创建新字符串对象的例子
				
var str = new String(“hello”);

StringObject.indexOf 比 StringObject.match 快。当搜索简单字符串匹配时,尽可能用 indexOf 而不用正则表达式匹配。

尽量避免在长字符串中匹配(10KB 及以上),除非您别无选择。如果您确定只在字符串某一特定部分匹配,用子串而不是整个字符串比较。

DOM 性能

本章简要介绍了一些可进行调整以提升 DOM 性能的内容。

重绘(Repaint)

当之前不可见的内容变得可见,DOM 就会重绘,反过来也一样。重绘也称为重画。此行为不会改变文档布局。不改变元素尺寸、形状或位置,只改变外观也会触发重绘。

例如,给元素添加边框或改变背景色就会触发重绘。重绘的性能代价很大;它需要引擎搜索所有元素以确定哪些可见,哪些必须显示。

回流(Reflow)

回流是比重绘更显著的改变。在回流中:

  • 要操作 DOM 树。
  • 影响布局的样式会改变。
  • 元素的 className 属性会改变。
  • 浏览器窗口尺寸改变。

引擎将会对相关元素回流,以确定各部分显示在哪。子元素也会被回流以反映父元素的新布局。DOM 中元素后面的元素也会被回流,以计算新布局,因为它们可能在初始回流时被移动了。祖先元素也会因子孙元素大小变化而被回流。最后,所有内容都被重绘。

每次向文档添加一个元素,浏览器都要回流页面来计算所有内容如何定位、如何呈现。添加的东西越多,回流次数越多。如果能减少单独添加元素的次数,浏览器回流次数就更少,运行也更快。

CSS 性能

将 Cascading Style Sheets (CSS) 放在顶端。如果样式表放在底部,将最后加载。之前几秒钟,页面都是空白,浏览器等待样式表加载,然后页面上其他东西才能呈现 — 甚至是静态文本。

在 Internet Explorer 中,@import 与在底部使用 效果一样。我们建议您不要使用。

缩写属性

使用缩写属性在一个声明中一次设置几个属性,而不是每个属性用一个声明。使用缩写属性,可以减小文件大小,降低维护量。

例如,可以设置背景、边框、边框颜色、边框样式、边框侧(顶部边框、右侧边框、底部边框、左侧边框)、边框宽度、字体、页边距、轮廓、填充属性。

CSS 选择器

CSS 选择器通过从右到左 移动来匹配。清单 7 所示,浏览器必须遍历页面中每个锚元素以确定它的父元素 ID 是否是 aElement


清单 7. 选择器正从右到左匹配
				
#aElement > a{
    font-size: 18px;
}

如果从代码中移除 > ,如 清单 8 所示,性能更糟。浏览器要检查整个文档中的所有锚。这样就不是只检查锚的父元素,而是顺着文档树向上查找 ID 为 aElement 的祖先元素。如果正在检查的元素不是 aElement 的子孙,浏览器就要沿着祖先元素的树查找,直到文档根部。


清单 8. 如果没有 >,性能更糟
				
#aElement a{
    font-size: 18px;
}

最佳实践

本章将略述能帮助您提升 web 应用程序性能的最佳实践。

减少 HTTP 请求数

每个 HTTP 请求都有开销,包括查找 DNS、创建连接及等待响应,因此削减不必要的请求数可减少不必要的开销。要减少请求数:

  • 合并文件。将总是同时使用的脚本合并到同一个文件中,不会减小总大小,但将会 减少请求数。还可以同样方法合并 CSS 文件和图片。可以实现文件自动合并:
    • 在构建阶段。用 标记,通过运行 Ant 合并文件。
    • 在运行时阶段。启用 mod_concat 模块。如果 httpServer 是 Apache,用 pack:Tag 作为 JSP 标签库来合并 JavaScript 和样式表文件。(pack:Tag 是一个 JSP-Taglib,可缩减、压缩及合并资源,如 JavaScript 和 CSS,并将它们在内容或普通文件中缓存。)
  • 使用 CSS Sprites。将背景图片合并成一个图片,并使用 CSS background-image 和 background-position 属性来显示所需图片部分。还可使用内联图片减少请求数。

后置加载组件

只呈现需要的组件;其余可等待。最好不要一次呈现太多组件。

某些情况下,可使用后置加载。由于浏览器可视区域外的组件可以后置加载,当这些组建进入可视区域不久后,初始呈现就会失效。

有些 JavaScript 可以在 onload 事件后后置加载,如 JavaScript 中初始呈现后拖动某个元素。

前置加载组件

通过前置加载组件,可以利用浏览器的空闲时间请求将来会用到的组件(如图像、样式和脚本)。当用户访问下个页面时,如果大多数组件都已在缓存中加载,那页面加载会快得多。

有两种前置加载:

  • 无条件:一旦触发 onload,就取得一些额外组件。
  • 有条件:根据用户的动作,推测用户下一步的方向并进行相应的前置加载。

将脚本放在底部

脚本可能会产生问题,因为它们可能会阻碍并行下载。当下载脚本时,浏览器不会再启动其他下载 — 即使那些位于不同主机。将脚本,如样式表,放在底部,以保证它们在其他下载完成后再下载。

也可以使用延时脚本,这只有 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 在企业开发领域更强大,这是由于:

  • Object-oriented programming (OOP) 编码
  • 跨平台
  • 本地数据存储的 Dojo 离线 API 支持
  • DataGrid、2D 和 3D 图形(图表组件提供了在浏览器展示报表更简单的方法)

Dojo 在很多网站广泛使用。我们将使用 Dojo 举例,分析 RIA 小部件的性能。可根据具体情况使用 Dojo 小部件调整工具,有 Page Speed、Rock Star Optimizer 及 Jiffy。我们强烈建议使用 YSlow 和 Firebug。

YSlow

YSlow 根据一组高性能 web 页面准则,通过检查页面上所有组件,包括由 JavaScript 创建的,来分析 web 页面性能。YSlow 是一个集成了 Firebug web 开发工具的 Firefox 插件;它可提供提升页面性能的建议、总结组件性能、显示页面统计数据并提供用于性能分析的工具。

图 3 显示的是 YSlow Grade 选项卡上的信息。


图 3. YSlow Grade 选项卡 
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第3张图片 

YSlow 的 web 页面建立在 22 条可测试的规则基础上,这些规则在下方按重要性和效果排列。研究显示,按照以下规则,web 页面响应时间可提升 25% 到 50%:

  • 尽量减少 HTTP 请求数。
  • 使用内容发布网络(CDN)。
  • 添加 Expires 或 Cache-Control 头部。
  • 用 Gzip 压缩内容。
  • 将样式表放在顶部。
  • 将脚本放在底部。
  • 避免使用 CSS 表达式。
  • 将 JavaScript 和 CSS 放在外部。.
  • 减少 DNS 搜索。
  • 精简 JavaScript 和 CSS。
  • 避免使用重定向。
  • 删除重复的脚本。
  • 配置 ETags。
  • 使 Ajax 可缓存。
  • 使用 GET 进行 Ajax 请求。
  • 减少 DOM 元素数。
  • 消除 404 错误。
  • 减小 cookie 大小。
  • 对组件使用无 cookie 的域。
  • 避免使用过滤器。
  • 不在 HTML 中测量图片大小。
  • 使 favicon.ico 尽可能小,可缓存。

图 4 中的 YSlow Statistics,对空缓存的访问用户和之前访问过页面的用户的页面大小做了对比。


图 4. YSlow Statistics 选项卡
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第4张图片 

Components 选项卡显示了每个组件及相关的性能信息。例如,如果组件被 gzip 压缩,或 ETag 有内容(如果的话),您都能看到。组件大小和超期时间也显示在 Components 选项卡中,如 图 5 所示。


Figure 5. YSlow Components 选项卡
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第5张图片 

Firebug

Firebug 与 Mozilla Firefox 集成,使您在浏览网站时有大量开发工具随手可用。可以即时编辑、调试、监控 web 页面中的 CSS、HTML 和 JavaScript。

可以使用 Firebug Net 面板,如 图 6 所示,监控 web 页面产生的 HTTP 流量。它向用户展示了所有收集到的和计算出的信息。每个条目表示页面的一个请求/响应来回。


图 6. Firebug Net 
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第6张图片 

Firebug Console 面板,如 图 7 所示,提供了两种监控代码性能的方法。


图 7. Firebug Console 面板
提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html_第7张图片 
Profile
对于某个特定的函数,使用 Profiler。JavaScript Profiler 是能用来测量每个 JavaScript 代码执行时间的 Firebug 特性。使用 JavaScript Profiler 来提升代码的性能,或是查看为什么某个函数运行时间过长。它与 console.time() ;类似,但 JavaScript Profiler 能提供更多代码内部过程细节。
console.time()
对于特定代码段,使用 console.time()。控制台会显示您输入到命令行的命令的结果。可以使用  console.time(timeName) 函数测量某个特定代码或函数执行多长时间。该特性对于提升 JavaScript 代码的性能非常有用。 清单 9 显示了一个样例。

清单 9. console.time() 样例
				
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 版本。更多有关 Dojo 文档信息,见 参考资料 一章。

解析成本

为了最小化 Dojo 小部件解析成本,使用以下两个方法来优化初始化:

Tag 实例化
可以通过添加  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"/>

代码实例化
Dojo 是一个 OOP 框架,因此可以使用  new 创建小部件。要创建小部件,必须输入两个参数:一个具有属性的 JSON 对象和要解析的元素。每个小部件至少需要两个语句。 清单 11 是一个样例。


清单 11. 用 JavaScript new 创建一个 Dojo 小部件

							
new dijit.Dialog({"title":"dialog1 "},dojo.byId("dialog1")); 

提升解析性能

为了优化代码结构和性能,可考虑在创建小部件时提升解析。通过将 parseWidgets 设置为 false 来禁用自动解析,然后创建一个数组来设置元素的 ID,如 清单 12 所示。也可在运行时动态放入新元素的 ID。当文档加载时,用 dojo.forEach() 解析数组中所有元素。


清单 12. 通过迭代 searchIds 来解析小部件
				

....

....


........



解决方案

Dojo 网格的性能问题主要与输入/输出操作、大量数据访问和浏览器数据呈现有关。可以通过使用合并网格特性的机制来提升 Dojo 网格小部件的性能。

回顾缓存机制的使用。当数据从数据库加载到本机时,在内存中保存数据一段时间。这是减小服务器端请求数据的响应时间的好办法。这样直到用户更新或修改网格中的数据时才发送请求。缓存机制一般是通过 Dojo 网格本身实现的,但用户对网格进行某种操作时,问题就会发生。以下场景揭示了这些问题:

对网格排序
多数情况下,可对网格正确排序,因为网格本身在数据存储层实现了排序函数。但数据存储会影响缓存。例如,如果网格列的类型是汉字,排序结果可能会不正确,而且网格的性能会由于某些不确定因素而严重下降。

解决方案是您自己在数据存储层重定义排序函数。下方的 清单 13 和 清单 14 演示了怎么做:根据 onHeaderCellMouseDown 函数重写排序逻辑,呈现数据,并更新网格的头部标题视图。


清单 13. 重定义排序逻辑
						
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());
			});
		}
	}
}


清单 14. 呈现数据,更新网格头部视图
						
//"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 = ['
', (sortAscending == true) ? '▲' : '▼', '
']); headerNodeHtml = headerNodeHtml.concat([headerNodeObjName, '
']); headerNodeObjs[i].innerHTML = headerNodeHtml.join(" "); break; } } }

在网格中查找
当网格有大量数据时,查找函数将会导致性能低下,尤其是当网格支持实时查找和模糊匹配特性时。解决方案是在它们被转换成数据存储中使用的 JSON 对象前,用额外的存储空间缓存数据;这将避免大量的函数调用,如数据存储的  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);
}
			

延时加载机制
Dojo 网格原本就支持延时加载机制,这可以改善性能并提供更好的用户体验。Dojo 网格中的延时加载意味着在数据存储中呈现某些数据,但不是所有网格在您拖动滚动条时再显示其余的数据。

默认情况下,网格不会启用延时加载机制;必须显式启动。清单 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

前置加载机制
前置加载机制可提前加载其余的数据,即使用户只是临时使用。对于 Dojo 网格,数据存储中可能有很多的数据;用户可能会拖动滚动条查看其余数据。如果一个页面中有很多数据,看某一特定行就不方便。使用前置机制和分页技术可更方便查看(与 Google 的分页条类似),并获得更好的性能。

清单 17 显示的是分页技术的基本实现。首先,构造一些 JSON 对象,供数据存储用于初始分页条,然后当用户单击分页条的最后一页时添加一些新的 JSON 对象。


清单 17. 开始时构建一些 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 应用程序中的一些问题或瓶颈。您现在以了解一些工具、窍门和技巧,以用于调整和改善对用户的性能。

你可能感兴趣的:(提升 web 应用程序的性能---http://www.ibm.com/developerworks/cn/web/wa-webappperformance/index.html)