[关闭]
@yacent 2016-07-22T10:00:31.000000Z 字数 7191 阅读 868

高性能网站建设进阶指南

性能优化

author: Steve Souders

本书架构:
    1-7章 JavaScript 性能
    8-12章 网络性能
    13-14 浏览器性能(HTML CSS等)

Ajax

通过ajax来实现页面当中局部DOM的修改,不用重新渲染整个页面,节省流量,并且提高整个网站的性能和用户体验


复杂计算

当页面当中的脚本需要进行长时间计算的时候,如果写成同步的函数,那么处理过程会非常久,页面可能会卡顿很久,当在做交互的时候,亦或者由于长时间的计算处理而导致整个网页都失去了响应,那么,我们可以通过 "多线程" 的思想来尝试解决这种问题,有如下几种方法:

1. 使用setTimeout来进行长时间的计算,因为浏览器常驻线程当中,有一个是 setTimeout事件触发线程,故可以用别的线程来进行处理
2. 使用web worker,思想是让外部的js文件来进行数据处理,通过postMessage的方法来将数据返回,虽然web worker挺强大的,但是还是要遵循js脚本语言单线程的特点,就是web worker不能处理dom操作,最好只用来做额外的计算
3. 当web worker不可用的时候,使用gears插件(google的 最初版 web worker)

拆分初始化负载

打开一个网站的时候,研究表明,如果加载时间在1s内,用户还是能接受的,如果延迟在1-10s内,用户可能会离开,超过10s,用户肯定直接关掉页面,骂一句 "草"。所以,在第一次打开页面的时候,响应速度还是很重要的,应该要尽快渲染出网页的关键,即要尽快将用户第一眼会看到的内容渲染出来,即首屏内容,那么思想就是拆分脚本, 一部分是渲染初始页面必须的,剩下的作为另外一部分,什么意思,就是初始化的时候只加载必须的js,而其余的js稍后再加载也可以,最容易的做法就是 将以后才会用到的 js脚本放在 onload事件当中,具体无阻塞的加载脚本的方式在下一节当中说


无阻塞加载脚本

首先说一下,为什么脚本的运行会阻塞页面其他资源的加载,对于HTML页面来说,因为js文件的解析和执行有可能会修改到页面的内容,所以在js脚本执行完之前,页面的加载可能是没有意义的,所以阻塞,而不同脚本之间会阻塞时因为,脚本与脚本之间存在依赖关系,后面的js脚本必须等待前面的脚本执行完毕了,再执行才不会出错。

那么,要是脚本不阻塞页面其他DOM元素的加载,有如下的方法

1. 将script标签放置在body的底部
2. 使用js脚本动态地插入新的script标签,请求外部的js文件,该方法不会阻塞页面当中其他组件的加载,异步的,可以与其他资源进行并行下载

整合异步脚本

如上,通过script dom的方式来实现无阻塞加载脚本,虽然能够并行的加载脚本,但是有一个很严重的问题,就是代码异步执行时可能会出现竞争状态,由于竞争状态而导致出现未定义标识符的错误,即由于js文件之间的依赖关系而导致的。
当异步加载的外部脚本与其他脚本之间存在依赖关系时,必须保证其执行的顺序来整合脚本,但由于之前的无阻塞脚本加载的方式,无法保证执行顺序,加载完之后就立即执行(即先加载完就先执行),就会导致上面说的错误。

当外部脚本按常规方式加载时,即将script标签放置在head标签当中,会阻塞行内代码的执行,竞争状态不会发生
  1. <script type="text/javascript">
  2. var domscript = document.createElement("script");
  3. domscript.src = "menu.js";
  4. document.getElementsByTagName("head")[0].appendChild(domscript);
  5. </script>
  6. <script type="text/javascript">
  7. function init() {
  8. // 需要用到 menu.js当中的变量
  9. }
  10. </script>

如上代码,如果使用dom script的方式,在行内 script当中的函数init有可能会因为未识别标识符而出现错误,缘由menu.js还没有加载进来。

解决方法:

硬编码回调
这种方式在我们平常的代码当中应该是经常用到的,就是通过将init函数写到menu.js 脚本当中去执行,这样就不会有竞争问题了,但是这样不好维护,就是需要改变回调接口时,需要修改外部的脚本,当外部脚本还是第三方库时,更麻烦

window onload
这种方式就是监听window.onload事件来触发行内代码的执行,但是这种方式的缺点是,必须保证外部脚本在onload事件之前就已经加载完成了

定时器
定时器技术通过 轮询 的方法来保证在行内的代码执行之前所依赖的外部脚本已经加载好了,即通过监听 外部脚本当中所拥有的变量是否为 undefined 来判断脚本是否已经加载完成,但是这种做法有缺点,首先是时间的设置,太短,增加开销,太长,又产生较大延迟,另外,如果脚本加载不成功,那么轮询将会一直持续下去

  1. function initTimer() {
  2. if ("undefined" == typeof(***)) {
  3. setTimeout(initTimer, 300);
  4. } else {
  5. init();
  6. }
  7. }

script onload
这种方式主要是通过判断 script脚本的状态来进行判断是否已经加载完毕

  1. function init() {
  2. // do something
  3. }
  4. function loadScript(url, callback) {
  5. var script = document.createElement('script');
  6. script.type = 'text/javascript';
  7. if (script.readyState) { // IE
  8. script.onreadystatechange = function() {
  9. if (script.readyState == "loaded" || script.readyState == "complete") {
  10. script.onreadystatechange = null;
  11. callback();
  12. }
  13. };
  14. } else { // others
  15. script.onload = function() {
  16. callback();
  17. };
  18. }
  19. script.src = url;
  20. document.getElementsByTagName('head')[0].appendChild(script);
  21. }

这种方式,可以保证脚本加载完成之后,才执行回调函数,这样行内代码与外部脚本之间的依赖关系就可以顺利获得

以上的方法都是考虑的如下情况,单个外部脚本和行内脚本的依赖关系的解决,但是,一般情况下,网站是不止一个外部脚本的,那么当有多个外部脚本时,都采取异步加载的方式,那么如何保证其依赖关系的正确呢?传统的做法,是用队列来存取脚本,以保证他们按顺序执行,但是工程化的今天,实际更多的是使用异步加载工具有 YUI loader AMD CMD等来确保脚本之间的依赖关系并且确保其正确执行

多个外部脚本异步加载,并且确保其按顺序加载
前提:外部脚本与主页面是同域的

response: XHR响应
onload: 脚本加载后触发的函数
bOrder: 如果该脚本需要依赖其他脚本按顺序执行,则设为true
  1. EFWS.Script = {
  2. queuedScripts: [],
  3. loadScriptXhrInjection: function(url, onload, bOrder) {
  4. var iQ = EFWS.Script.queuedScripts.length;
  5. if (bOrder) {
  6. var qScript = {response: null, onload: onload, done: false};
  7. EFWS.Script.queuedScripts[iQ] = qScript;
  8. }
  9. var xhrObj = EFWS.Script.getXHRObject();
  10. xhrObj.onreadystatechange = function() {
  11. if (xhrObj.readyState == 4) {
  12. if (bOrder) {
  13. EFWS.Script.queuedScripts[iQ].response = xhrObj.responseText;
  14. EFWS.Script.injectScripts();
  15. } else {
  16. eval(xhrObj.responseText);
  17. if (onload) {
  18. onload();
  19. }
  20. }
  21. }
  22. };
  23. xhrObj.open('GET', url, true);
  24. xhrObj.send('');
  25. },
  26. injectScripts: function() {
  27. var len = EFWS.Script.queuedScripts.length;
  28. for (var i = 0; i < len; i++) {
  29. var qScript = EFWS.Script.queuedScripts[i];
  30. if (!qScript.done) {
  31. if (!qScript.response) {
  32. break;
  33. } else {
  34. eval(qScript.response);
  35. if (qScript.onload) {
  36. qScript.onload();
  37. }
  38. qScript.done = true;
  39. }
  40. }
  41. }
  42. },
  43. getXHRObject: function() {
  44. var xhrObj = false;
  45. try {
  46. xhrObj = new XMLHttpRequest();
  47. }
  48. catch(e) {
  49. var aTypes = ["Msxm12.XMLHTTP6.0",
  50. "Msxm12.XMLHTTP3.0",
  51. "Msxm12.XMLHTTP",
  52. "Microsoft.XMLHTTP"];
  53. var len = aTypes.length;
  54. for (var i = 0; i < len; i++) {
  55. try {
  56. xhrObj = new ActiveXObject(aTypes[i]);
  57. }
  58. catch(e) {
  59. continue;
  60. }
  61. break;
  62. }
  63. }
  64. finally {
  65. return xhrObj;
  66. }
  67. }
  68. };

布置行内脚本

虽然行内脚本不会产生额外的HTTP请求,但是会阻塞页面上资源的并行下载,还会阻塞逐步渲染,我们应该避免行内脚本阻塞到页面的渲染,有如下的几种方式

1. 将行内脚本移至底部,并且将行内代码放在 onload事件当中进行触发
2. 使用异步回调启动js的执行
3. 使用script的defer属性(不推荐,兼容性不太好)

CSS样式表的顺序也是类似的,CSS样式表以再页面当中列出的顺序为准,而与加载先后无关.

行内脚本放在样式表之后,会导致页面当中其他资源的阻塞


编写高效的JavaScript


可伸缩的Comet

虽然Ajax和后台的HTTP请求都是可以增强当今web应用程序性能的典型技术,然后浏览器和HTTP中所使用的传统的请求/响应(request/response)模式无法满足诸如 聊天 财经信息 文档协作等要求实时应用的需要,Comet是一种类似与服务器推送事件的模型,那么可以使用 轮询、长轮询、web Socket、XHR流等的方式来实现 低延时数据传输的实时应用

Comet的技术基础: 轮询 长轮询 永久帧 XHR流

轮询
客户端每隔×秒就发送一个请求,看看有没有数据更新,有就接收数据并进行相相对应的处理

  1. setTimeout(function() {xhrRequest({foo: "bar"})}, 2000);
  2. function xhrRequest(data) {
  3. var xhr = new XMLHttpRequest();
  4. // 在发送请求的时候,处理的数据通过参数进行传递
  5. xhr.open("get", "http://localhost/foo.php", true);
  6. xhr.onreadystatechange = function() {
  7. if (xhr.readyState == 4) {
  8. // do something
  9. }
  10. };
  11. xhr.send(null);
  12. }

缺点: 因为客户端不管是否返回数据,都会定时的发送一个请求取进行数据的请求,有可能会导致请求过多,上一个的请求还没响应完,下一个请求又来了,会导致服务器请求连接数过多.虽然轮询的方式简单,但是效率低,资源利用率低

长轮询
长轮询的方式有点类似于轮询的方式,也是通过客户端发送请求,但是与轮询不同的时候,客户端在发出一个长轮询的请求之后,连接会一直保持着,直到服务器端有数据更新时,将数据返回给客户端,客户端接收数据后,又立马创建一个新的长轮询进行数据的请求

长轮询是目前使用最广泛的服务器实时推送方法

  1. function longPoll(url, callback) {
  2. var xhr = new XMLHttpRequest();
  3. xhr.onreadystatechange = function() {
  4. if (xhr.readyState == 4) {
  5. // 发送另一个请求,重新连接服务端
  6. callback(xhr.responseText);
  7. xhr.open('GET', url, true);
  8. xhr.send(null);
  9. }
  10. }
  11. // 连接到服务端以打开一个请求
  12. xhr.open('POST', url, true);
  13. xhr.send(null);
  14. }

永久帧
技术实现大概就是创建一个隐藏的iframe,请求一个基于HTTP1.1块编码的文档.服务端不断地往iframe当中写入数据,可以实现跨域基于iframe的跨域方法

XHR流
这种方式与长轮询类似,只不过是在接收数据的时机不太同,在readyState为3的时候,就开始接收数据,当数据接收完毕,马上创建一个新的请求

  1. function xhrStreaming(url, callback) {
  2. xhr = new XMLHttpRequest();
  3. xhr.open('POST', url, true);
  4. var lastSize;
  5. xhr.onreadystatechange = function() {
  6. var newTextReceived;
  7. if (xhr.readyState > 2) {
  8. // 获取最新的响应正文
  9. newTextReceived = xhr.responseText.subString(lastSize);
  10. lastSize = xhr.responseText.length;
  11. callback(newTextReceived);
  12. }
  13. if (xhr.readyState == 4) {
  14. // 如果响应结束,马上创建一个新的请求
  15. xhrStreaming(url, callback);
  16. }
  17. }
  18. xhr.send(null);
  19. }

超越Gzip压缩

在高性能网站建设指南当中,即雅虎军规14条当中有提到启用Gzip压缩,即在请求的时候加上请求头 Accept-encoding: gzip delfate,那么接收到的请求就是经过Gzip压缩的,其会将html、css、js等的文件进行压缩,大大减小了文件传输的大小,提升用户体验,但是有一部分用户会存在gzip压缩失效,大部分的原因是因为使用了 代理PC安全软件,二者为了能占用更低的cpu资源,劫持了发送请求,并去除Accept-encoding字段,从而导致这部分用户没有发送gzip请求
根本的解决办法,是让代理或者安全软件去修改他们的监听方式,不要去除 accept-encoding字段,但这需要的时间比较长,而且就算商家更新了更改,用户升级到最新版也需要一定的时间

有如下的一些编码习惯可以一定程度上帮助用户能够有更好的体验

事件委托
给相同类型元素绑定同一事件进行处理,如果每一个都进行事件绑定,占用内存会更多,并且会增大文件的大小,在其父元素上绑定事件,利用冒泡机制获得事件事件响应

使用相对URL

移除空白
使用cssmin 或者 jsmin来对代码进行压缩,移除空白,进行变量的替换

避免行内样式
最好通过类来定义一些重复样式而不是通过内联样式来一个个修改,增添了重复代码

为JavaScript变量设置别名
有点类似于编写高效的Javascript代码当中的使用局部变量,对于使用两次以上的DOM,最好能使用局部变量储存起来


图像优化

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注