《高性能网站建设进阶指南》学习笔记

来源:互联网 发布:js实现文件下载功能 编辑:程序博客网 时间:2024/06/05 16:39
第1章  理解 Ajax 性能
过早的优化是万恶之源-Donald Knuth
1.1 权衡
时间、质量和成本,三选二。
1.2 优化原则
把重点放在对程序整体开销影响最大的部分。
仔细观察程序的执行时间后,我们会发现其大部分时间都消耗在循环上。
1.3 Ajax
依据 YSlow 规则来重构代码。
1.4 浏览器
运行程序的最大开销往往是 DOM 而非 JavaScript。
1.5 哇!
无需滚动,采用分页展示。
1.6 JavaScript
通常情况下,瓶颈不是 JavaScript 而是 DOM。


第2章  创建快速响应的 Web 应用
浏览器使用单线程从队列中取出事件,然后对事件本身进行处理或执行 JavaScript。
2.1 怎样才算足够快
0.1秒:用户直接操作 UI 中对象的感觉极限。超过0.1秒会有不够平滑快捷的感觉。
1秒:用户随意地在计算机指令空间进行操作而无需过度等待的感觉极限。0.2-1.0秒的延迟会被用户注意到,会感觉到计算机处于对指令的“处理中”,超过1秒会感到应用程序缓慢。
10秒:用户专注于任务的极限。超过10秒的任何操作都需要一个百分比完成指示器,以及一个方便用户中断操作且有清晰标识的方法。超过10秒,用户将非常沮丧。
2.2 测量延迟时间
手动代码检测
function myJavaScriptFunction() {
var start = new Date().getMilliseconds();


// 这里是一个开销很大的代码


var stop = new Date().getMilliseconds();
var executionTime = stop - start;
console.log("myJavaScriptFunction() executed in " + executionTime + " milliseconds");
}
自动代码检测
Firebug
2.3 线程处理
JavaScript 并不支持多线程,所以无法使用 JavaScript 代码创建一个后台线程来执行开销很大的代码。
2.4 确保响应速度
2.4.1 Web Worker
// 创建并开始执行 worker
var worker = new Worker("js/decrypt.js");


// 注册事件处理程序,当 worker 给主线程发送信息时执行
work.onmessage = function(e) {
alert("The decrypted value is " + e.data);
}


// 发送信息给 worker, 这里是指待解密的值
worker.postMessage(getValueToDecrypy());


// js/decrypt.js
// 注册用来接收来自主线程信息的处理程序
onmessage = function(e) {
// 获取传过来的数据
var valueToDecrypt = e.data;


// TODO:这里实现解密功能


// 把值返回主线程
postMessage(decryptedValue);
}
2.4.2 Gears
// 创建 Worker Pool,它会产生 Worker
var workerPool = google.gears.factory.create('beta.workerpool');


// 注册事件处理程序,它接收来自 Worker 的信息
workerPool.onmessage = function(ignorel, ignore2, e) {
alert("The decrypted value is " + e.body);
}


// 创建 Worker
var workerId = workerPool.createWorkerFromUrl("js/decrypt.js");


// 发送信息到这个 Worker
workerPool.sendMessage(getValueToDecrypt(), workerId);


// js/decrypt.js
var workerPool = google.gears.workerPool;
workerPool.onmessage = function(ignore1, ignore2, e) {
// 获取传过来的数据
var valueToDecrypt = e.body;


// TODO:这里实现解密功能


// 把值返回主线程
workerPool.sendMessage(decryptedValue, e.sender);
}
2.4.3 定时器
var functionState = {};


function expensiveOperation() {
var startTime = new Date().getMilliseconds();
while((new Date().getMilliseconds() - startTime) < 100) {
// TODO, 在迭代的语句块中执行100毫秒内完成的工作,然后修改函数外部 functionState 中的状态
}


if(!functionState.isFinished) {
// 退出10毫秒后再次执行 expensiveOperation
setTimeout(expensiveOperation(), 10);
}
}
2.4.4 内存使用对响应时间的影响
2.4.5 虚拟内存
分页会导致全面的、无处不在的停顿,而 GC 停顿往往会导致离散且孤立地停顿,并且停顿的长度会随时间而增长。
2.4.6 内存问题的疑难解答
使用 delete 关键字从内存中移除不再需要的 JavaScript 对象
从网页的 DOM 树上移除不再是必需的节点
var page = {address: "http://some/url"};


page.contents = getContents(page.address);


...


// 以后,这些内容不再是必需的了
delete page.contents;


...


var nodeToDelete = document.getElementById("redundant");


// 从 DOM 树中移除节点,同时从内存中删除这个节点
delete nodeToDelete.parent.removeChild(nodeToDelete);


第3章  拆分初始化负载
3.1 全部加载
3.2 通过拆分来节省下载量
3.3 寻找拆分
把要下载的 JavaScript 代码拆分成两个文件,一个用于页面初始化,另一个则可以延后加载。onload 事件之后的功能可以在初始页面渲染完成时开始加载。
3.4 未定义标识符和竞争状态
给每个被引用但又被降级为延迟下载的函数创建一个桩函数。桩函数是一个与原函数名称相同但是函数体为空,或者是一些用临时代码代替原有内容的函数。


第4章  无阻塞加载脚本
4.1 脚本阻塞并行下载
4.2 让脚本运行得更好
4.2.1 XHR Eval
通过 XMLHttpRequest(XHR) 从服务端获取脚本,当响应完成时通过 eval 命令执行内容。
var xhrObj = getXHRObject();
xhrObject.onreadystatechange = 
function() {
if(xhrObj.readyState == 4 && 200 == xhrObject.status) {
eval(xhrObj.responseText);
}
};
xhrObject.open('GET', 'A.js', true); // 必须和主页面在同一个域中
xhrObject.send('');


function getXHRObject() {
var xhrObj = false;
try {
xhrObj = new XMLHttpRequest();
}
catch(e) {
var progid = ['MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
for(var i = 0; i < progid.length; ++i) {
try {
xhrObj = new ActiveXObject(progid[i]);
}
catch(e) {
continue;
}
break;
}
}
finally {
return xhrObj;
}
}
4.2.2 XHR 注入
通过创建一个 script 的 DOM 元素,然后把 XMLHttpRequest 的响应注入 script 中来执行 JavaScript。
var xhrObj = getXHRObject();
xhrObj.onreadystatechange =
function() {
if(xhrObj.readyState == 4) {
var scriptElem = document.createElement('script');
document.getElementByTagName('head')[0].appendChild(scriptElem);
scriptElem.text = xhrObj.responseText;
}
};
xhrObj.open('GET', 'A.js', true); // 必须和主页面在同一个域中
xhrObj.send('');
4.2.3 Script in Iframe
<iframe src = 'A.html' width=0 height=0 frameborder=0 id=frame1></iframe> // 要求 iframe URL 和主页面同域
其中是 A.html 而不是 A.js,因为 iframe 认为其返回的是 HTML 文档,需要再 HTML 文档中把外部脚本转换成行内脚本。
// 使用 frames 访问主页面上的 iframe
window.frames[0].createNewDiv();


// 使用 getElementById 访问主页面上的 iframe
document.getElementById('frame1').contentWindow.createNewDiv();


// 在 iframe 中使用 parent 访问主页面
function createNewDiv() {
var newDiv = parent.document.createElement('div');
parent.document.body.appendChild(newDiv);
}
4.2.4 Script DOM Element
var scriptElem = document.createElement('script');
scriptElem.src = 'http://anydomin.com/A.js'; // 允许跨域获取脚本
document.getElementsByTagName('head')[0].appendChild(scriptElem);
4.2.5 Script Defer
<script defer src='A.js'></script> // defer属性是非常简单的防止脚本阻塞的方法,但只有 IE 和一些新浏览器支持
4.2.6 document.write Script Tag
document.write("<script type='text/javascript' src='A.js'><\/script>"); // 只有 IE 支持
4.3 浏览器忙指示器
通常 onload 事件要等到所有资源下载完成时才会触发。
4.4 确保(或避免)按顺序执行
使用常见的 script src 技术保证脚本按它们在页面中排列的顺序下载和执行。然而使用前面描述的某些高级的下载技术并不能确保这点。因为脚本是并行下载的,所以它们会按到达的顺序


执行-这会导致竞争状态,进而导致未定义标识符错误。
4.5 汇总结果
不推荐 document.write Script Tag 技术,Script Defer 技术也只在部分浏览器中实现了并行下载。
4.6 最佳方案
最佳脚本加载技术的决策树


第5章  整合异步脚本
5.1 示例代码:menu.js
5.2 竞争状态
5.3 异步加载脚本时保持执行顺序
5.3.1 硬编码回调
让外部脚本调用行内代码里的函数。
5.3.2 Window Onload
通过监听 onload 事件来触发行内代码的执行,只要确保外部脚本在 window.onload 之前下载执行就能保持执行顺序。
5.3.3 定时器
5.3.4 Script Onload
这是整合异步加载外部脚本和行内脚本的首选
<script type="text/javascript">
var aExamples = [['couple-normal.php', 'Normal Script Src'],...];


function init() {
EFWS.Menu.createMenu('examplesbtn', aExamples);
}


var domscript = document.createElement('script');
domscript.src = "menu.js";
domscript.onloadDone = false; // Opera 对 onreadystatechange 和 onload 都有效,故需做标记处理
domscript.onload = function() { // onload 在除 IE 外其他浏览器中有效
domscript.onloadDone = true;
init();
};
domscript.onreadystatechange = function() { // onreadystatechange 在 IE 中有效
if(("loaded" === domscript.readyState||"complete" === domscript.readyState) && !domscript.onloadDone) {
domscript.onloadDone = true;
}
}
document.getElementsByTagName('head')[0].appendChild(domscript);
</script>
5.3.5 降级使用 script 标签
5.4 多个外部脚本
5.4.1 Managed XHR
5.4.2 DOM Element 和 Doc Write
5.5 综合解决方案
5.5.1 单个脚本
异步加载单个脚本的最佳技术是 Script DOM Element。
Script Onload 模式是用于整合行内代码和单个外部脚本的最好选择。
EFWS.Script.loadScriptDomElement 实现了以上两个技术。
5.6 现实互联网中的异步加载
5.6.1 Google 分析和 Dojo
5.6.2 YUI Loader


第6章  布置行内脚本
6.1 行内脚本阻塞并行下载
除了阻塞并行下载,行内脚本还阻塞渲染。
6.1.1 把行内脚本移至底部
虽然该技术避免了阻塞下载,但它依旧阻塞渲染。
6.1.2 异步启动执行脚本
如果行内脚本执行时间很短,那么使用延迟值为0毫秒的 setTimeout。如果脚本执行时间很长,更好的选择是使用 onload。
6.1.3 使用 script 的 defer 属性
defer 是较简单的实现并行下载解决方案,但任然阻塞逐步渲染。
6.2 保持 CSS 和 JavaScript 的执行顺序
不管 HTTP 响应和接收的顺序如何,它们都是按照指定顺序应用的。
6.3 风险:把行内脚本放置在样式表之后
6.3.1 大部分下载都不阻塞行内脚本
6.3.2 样式表阻塞行内脚本
在样式表后面的行内脚本会阻塞所有后续资源的下载。
方案是调整行内脚本的位置,使其不出现在样式表和任何其他资源之间。


第7章  编写高效的 JavaScript
7.1 管理作用域
7.1.1 使用局部变量
一个好的经验是任何非局部变量在函数中的使用超过一次时,都应该将其存储为局部变量。
7.1.2 增长作用域链
with 语句会增长作用域链,会减慢标识符的存取,建议避免使用。
try-catch 语句块中 catch 从句也会增长作用域链,但其影响较小,注意不要在 catch 从句中执行过多的代码。
7.2 高效的数据存取
在数据存取时,将函数中使用超过一次的对象属性或数组元素存储为局部变量是一种好方法。
var divs = document.getElementsByTagName("div");
for(var i = 0; i < divs.length; i++) { // 避免!因为每次循环要读取 divs 两次。
var div = divs[i];
process(div);
}
var divs = document.getElementsByTagName("div");
for(var i = 0, len = divs.length; i < len; i++) { // 更好的方式,每次循环只需读取 divs 一次。
var div = divs[i];
process(div);
}
7.3 流控制
7.3.1 快速条件判断
将最常见的情况放在 if 语句的顶部。
将条件拆分成几个分支,使用二分查找逐步找出有效的条件。
当两个以上条件且条件比较简单(不是进行范围判断)时,switch 语句往往更快。
数组查询
// 定义数组 results
var results = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10];
// 返回正确的结果
return results[value];
最快的条件判断:
使用 if 语句的情况:
两个之内离散值需要判断。
大量的值能容易地分到不同的区间范围中。
使用 switch 语句的情况:
超过两个而少于10个离散值需要判断。
条件值是非线性的,无法分离出区间范围。
使用数组查询的情况:
超过10个值需要判断。
条件对应的结果是单一值,而不是一系列操作。
7.3.2 快速循环
使用局部变量 length 代替 values.length 进行条件比较。
将循环变量递减到0,而不是递增到总长度。// 可以比原来节约多达50%的执行时间。
小心使用数组原生的 indexOf 方法,这个方法遍历数组成员的耗时可能比普通的循环还长。
避免 for-in 循环。for-in 循环通常比其他循环慢,因为它需要从一个特定的对象中解析每个可枚举的属性。
for(var prop in object) {
if(object.hasOwnProperty(prop)) { // 确保只处理实例自身的属性
process(object[prop]);
}
}
改为:
// 要遍历的已知属性
var props = ["name", "age", "title"];


// while 循环
var i = props.length;
while (i--) {
process(object[props[i]]);
}
展开循环
var i = values.length;
while(i--) {
process(value[i])
}
// 如果 values 数组中只有5项,则展开循环
process(values[0]);
process(values[1]);
process(values[2]);
process(values[3]);
process(values[4]);
Duff 策略
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;


if(leftover > 0) {
do {
process(values[i++]);
} while (--leftover > 0)
}


do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);
Duff 策略主要用于处理大数组,仅当你注意到性能的瓶颈是由循环处理大量元素项所引起的时,才应该尝试使用。
7.4 字符串优化
7.4.1 字符串连接
数组技术
var buffer = [], i = 0;
buffer[i++] = "Hello";
buffer[i++] = " ";
buffer[i++] = "World!";
var text = buffer.join("");
7.4.2 裁剪字符串
function trim(text) {
text = text.replace(/^\s+/, "");
for(var i = text.length - 1; i >= 0; i--) {
if(/\S/.test(text.charAt(i))) {
text = text.substring(0, i + 1);
break;
}
}
return text;
}
7.5 避免运行时间过长的脚本
常见的脚本执行时间过长的原因包括:
过多的 DOM 交互
过多的循环
过多的递归
7.5.1 使用定时器挂起
window.onload = function() {


// 页面加载完成


// 创建第一个定时器
setTimeout(function() {


// 被延迟的脚本1


setTimeout(function() {


// 被延迟的脚本2
}, 100);


// 被延迟的脚本1,继续执行


}, 100);
};
7.5.2 用于挂起的定时器模式
function chunk(array, process, context) {
setTimeout(function() {
var item = array.shift();
process.call(context, item);


if(array.length > 0) {
setTimeout(arguments.callee, 100);
}
}, 100);
}
var names = ["Nicholas", "Steves", "Doug", "Bill", "Ben", "Dion"],
todo = names.concat(); // 复制数组


chunk(todo, function(item) {
console.log(item);
});


function sort(array, onComplete) {


var pos = 0;


(function() {


var j, value;


for(j = array.length; j > pos; j--) {
if(array[j] < array[j - 1]) {
value = data[j];
data[j] = data[j - 1];
data[j - 1] = value;
}
}


pos++;


if(pos < array.length) {
setTimeout(arguments.callee, 10);
} else {
onComplete();
}
})();
}
sort(values, function() {
alert("Done!");
});


第8章  可伸缩的 Comet
8.1 Comet 工作原理
8.2 传输技术
8.2.1 轮询
setTimeout(function(){xhrRequest({"foo":"bar"})}, 2000);


function xhrRequest(data) {
var xhr = new XMLHttpRequest();
xhr.open("get", "http://localhost/foo.php", true);
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
// 处理服务器返回的请求
}
};
xhr.send(null);
}
8.2.2 长轮询
function longPoll(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
// 发送另一个请求,重新连接服务端
callback(xhr.responseText);
xhr.open('GET', url, true);
xhr.send(null);
}
}
xhr.open('POST', url, true);
xhr.send(null);
}
8.2.3 永久帧
function foreverFrame(url, callback) {
var iframe = body.appendChild(document.createElement("iframe"));
iframe.style.display = "none";
iframe.src = url + "?callback=parent.foreverFrame.callback";
this.callback = callback;
}
8.2.4 XHR 流
function xhrStreaming(url, callback) {
xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
var lastSize;
xhr.onreadystatechange = function() {
var newTextReceived;
if(xhr.readyState > 2) {
// 获取最新的响应正文
newTextReceived = xhr.responseText.substring(lastSize);
lastSize = xhr.responseText.length;
callback(newTextReceived);
}
if(xhr.readyState == 4) {
// 如果响应结束,马上创建一个新的请求
xhrStreaming(url, callback);
}
}
xhr.send(null);
}
8.2.5 WebSocket
8.3 跨域
function callbackPolling(url, callback) {
// 建立一个 script 元素,在这个元素中将加载服务器的响应
var script = document.createElement("script");
script.type = "text/javascript";
script.src = url + "callback=callbackPolling.callback";
callbackPolling.callback = function(data) {
// 发送一个新的请求等待服务器下次发送消息
callbackPolling(url, callback);
// 调用回调函数
callback(data);
};
// 添加这个元素,开始执行这段脚本
document.getElementsByTagName("head")[0].appendChild(script);
}


第9章  超越 Gzip 压缩
大概有15%的访问者没有声明支持 Gzip 压缩。
9.2 问题的根源
9.2.2 罪魁祸首
最酷祸首主要有两类:Web 代理和 PC 安全软件
9.3 如何帮助这些用户
9.3.1 最小化未压缩文件的尺寸
使用事件委托
<div class="menu_content" onclick="return intl_set_cookie_locale(event)">
...
<a href="http://es-la.facebook.com/" class="es_LA">Espanol</a>
...
</div>
<script>
function intl_set_cookie_locale(e) {
e = e || window.event; // 获取 event 对象
var targetElement = e.target || e.srcElement; // 获取触发事件的元素
var newLocale = targetElement.class; // 获取新的地区值
...
// 使用 newLocale 变量去设置 Cookie
...
return false; // 阻止链接的默认行为
}
</script>
使用相对 URL
移除空白
移除属性的引号
避免行内样式
为 JavaScript 变量名设置别名
9.3.2 引导用户
9.3.3 对 Gzip 的支持进行直接探测


第10章  图像优化
10.6.2 优化生成的图像
相比 GIF,优先选择 PNG 是非常明智的,PNG8 是最佳的选择。
在服务器保存之前别忘了使用 pngcrush 对图像进行优化。
10.6.3 Favicons
最佳的做法是只保留一个16*16图像。
可以使用 Pixelformer 进行优化。
10.6.4 Apple 触摸图标
一个 Apple 触摸图标就是一个位于 web 服务器根目录的 PNG 文件,尺寸是57*57像素。
10.7 总结
用 JPEG 保存照片,用 GIF 保存动画,其他所有图像都用 PNG 来保存,并且尽量使用 PNG8。
对大小超过10KB的图像,采用渐进 JPEG 编码。
不要在 HTML 中对图像进行缩放。


第11章  划分主域
11.3 降级到 HTTP/1.0
如果你选择把静态资源分配到多个域上,那请遵循只划分两个域的准则。


第12章  尽早刷新文档的输出
12.1 刷新文档头部的输出
<?php
flush();
long_slow_function();
?>
刷新输出的最大好处是可以提前加载页面资源。
12.2 输出缓存
12.3 块编码


第13章  少用 iframe
13.1 开销最高的 DOM 元素
创建 iframe 的开销比创建其他类型的 DOM 元素要高1-2个数量级。
13.2 iframe 阻塞 onload 事件


第14章  简化 CSS 选择符
14.2 高效 CSS 选择符的关键
事实上,CSS 选择符是从右向左进行匹配的。
14.2.2 编写高效的 CSS 选择符
避免使用通配规则
不要限定 ID 选择符
不要限定类选择符,而是根据情况对类名进行扩展,(例如,把LI.chapter 改成 .li-chapter 或 .list-chapter)
让规则越具体越好
避免使用后代选择符
避免使用标签-子选择符
质疑子选择符的所有用途
依靠继承(了解哪些属性可以通过继承而来,然后避免对这些属性重复指定规则)
0 0