页面性能优化 - Draft 1
如何让网页在最快的时间内呈现给用户, 不停的在loading是让人烦躁的一件事情
主要先研究前台的优化
- JS代码本身优化
- 统一入口异步调用
- 实时加载
JS代码优化
如何写出高效的JS不是一句两句话能说完的事情, 比如若果在使用jQuery那么ID选择符前不要加多余的tag或者class, 因为jQuery对selector的第一个正则表达式的判断就是判断是否是ID或html tag等等
统一入口异步调用
整个站点所有JS函数调用只留一个入口, 这个入口要做的健壮而强大 - 参考msn.com的做法
设计:
页面inline的JS实现async这个统一入口, 可以看到msn.com的页面上有这样一段被压缩过的JS
没有压缩后的代码应该大致是这样的
(function($) { var defaults = { timeout: 50; }; // key: url being downloaded // value: array of asyncItem objects var pending = {}; var pollList = []; var waitList = []; // timer id for the poll timeout var timerId; // shortcuts var $isString = $.isString; var $isFunction = $.isFunction; var w = window; // main entry point function async(test, action, url) { var canary; //$isString(a, b)为自扩展方法用于判断a为string名且长度大于等于1 if ($isString(test, 1) && (canary = this[test])) { // $isFunction(action)也是一样, 此条件满足为最理想状态 // 测试条件为true说明JS文件内方法已经能访问到, 能访问到又说明JS已经download了 // 所以要做的就是执行方法 // 用法1: $.async(test, action) if ($isFunction(action)) { // just call the function, passing this as the object applyCallback(action, this); } else if ($isFunction(canary)) { // 第二个参数是个数组, 那么我们默认这个数组就是第一个参数执行时所需要的参数, 所以有用法2如下 // 用法2: $.async(既是test条件也是要执行的function, 前面这个函数执行需要的参数) if ($.isArray(action)) { applyCallback(canary, this, action); } // 没有参数, 用法3: $.async(既是test条件也是要执行的function) else if (!$.isDefined(action)) { applyCallback(canary, this); } } } // undefined? 那是因为JS文件没有下载 // 加入pending队列发送Ajax请求文件并重头开始判断测试条件并决定是否执行 else if ($isString(url)) { // 我们会把所有已经在pending列表里和第一次请求下载文件(也就是暂时没有在pending列表内)的所有async请求循环 // 然后重新调用async, 重新判断测试条件并且决定是否执行函数或者再来下载JS. var testQueue = pending[url]; // 如果在这个全局的pending列表里面已经有了我们要下载的url对象 // 但是为什么仍旧走到了这一步呢? 当然是因为测试条件不满足 // 也就是实际上我们的JS文件还没有下载完, 这个情况的出现有很多因素 // 最大的因素就是我们下载JS文件都是用的Ajax方式下载 // 这显然不能保证你一掉用Ajax请求下载JS我们就能让这个JS文件下载完 if (testQueue) { testQueue.push(new asyncItem(test, action, this)); } else { pending[url] = [new asyncItem(test, action, this)]; [Ryan] 从来没有遇到过要下载这个JS Url的请求, 为避免重复发送Ajax请求和便于管理pending列表, 我们会在每一次遇到新的(第一次请求)JS下载请求时, 为pending列表创建一个key为这次请求的Url, 然后再新建一个asyncItem的对象(包含test, action, this的信息)赋值给pending列表中刚新建的那个项 $.ajax({ url: url, dataType: "script", cache: 1, success: function() { // 将这个新建的赋值给局部变量testQueue // 因为我们不期待在这一次独立的单独的特殊的具有惟一性的请求中去循环pending列表 // 因为我们每一次Async请求都有一个单独的周期 // 1. 测试条件通过, 执行需要的方法 // 2. 没通过, 发送Ajax下载请求, 并且调用retryTest() – 重新从头来过, 继续判断测试条件然后开始执行 // 3. 循环1和2 testQueue = pending[url]; for (var item, ndx = 0; (item = testQueue[ndx]); ++ndx) { // 这个函数你在下面可以看到就是重新拿传入的item – new asyncItem(test, action, this) retryTest(item); } } }); } } // 通过上面2次的判断知道到这边的情况下, 测试条件都是false的并且第三个参数不是string也就是可以理解为没有文件要下载 else if ($isString(test, 1)) { // poolList是一个全局的等待列表, 用于存放那些判断条件仍然为false, 并且没有提供第三个Url的参数的请求 pollList.push(new asyncItem(test, action, this)); if (!timerId) { // 根据提供的timeout参数间隔的执行pollCallback, 至于这个函数干了些什么能在下面他定义的地方看到. timerId = w.setTimeout(pollCallback, defaults.timeout); } } } function applyCallback(callback, callee, action) { if (callee.selector && callee.size() == 0) { waitList.push(new asyncItem(callback, action, callee)); return; } if (callee.selector && $.isArray(action) && action[0].asyncp) { callee = callee.filter(filterPersistantAsyncCalls); waitList.push(new asyncItem(callback, action, callee.end())); } if (action) { callback.apply(callee, action); } else { callback.apply(callee); } } function filterPersistantAsyncCalls() { var $this = $(this); if ($this.data('asyncfilter')) { return 0; } $this.data('asyncfilter', 1); return 1; } function checkWaitList() { var list = waitList; waitList = []; for (var item, callee, ndx = 0; (item = list[ndx]); ++ndx) { callee = $(item.callee.selector, item.callee.context); applyCallback(item.func, callee, item.action); } } function pollCallback() { // 必须重新定义一个, 防止死循环 var list = pollList; pollList = []; for (var item, ndx = 0; (item = list[ndx]); ++ndx) { // 跟上面需要下载文件的时候一样, 我们重新回到Async入口. retryTest(item); } // 重新赋值, 如果pollList列表已经空了, 那么不需要重新setTimeout了 // 如果不是, 那么继续定时的返回这个函数的入口, 去重新调用Async timerId = (pollList.length == 0 ? 0 : w.setTimeout(pollCallback, defaults.timeout)); } function retryTest(item) { async.call(item.callee, item.func, item.action); } function asyncItem(func, action, callee) { this.func = func; this.action = action; this.callee = callee; } // 扩展出各种接口供使用者, 其中w.async就是给window的, $.fn.async是供各种jQuery方法的 $.async = $.fn.async = w.async = async; $.async.defaults = defaults; $.async.delayed = checkWaitList; })(jQuery);
于是页面调用所以JS方法时于是有了统一的入口
一个简单的流程图
实时加载
先在firebug中看一下是那些东西导致我们的页面请求变慢
注意看颜色的注释, 很多方面作为程序员无能为力, 比如域名解析, 建立连接, 发送请求, 等待响应.. 这些包含了太多的外界因素, 并不是我们能控制的
但是看到接受数据占据了很多时间, 于是是否可以减少数据接收量来提高性能?
显然是可以的, 很多网站都在做这样的事情, 比如人人网, 往下拉才会加重更多新鲜事, 再比如分页, 其实这些都是在减少一个页面的数据量
那么我们是否可以通过判断html element在document中的height和window的height加上scrollbar的height来判断这个html element是否已经需要显示在页面上了?
显然是可以的, 比如图片的下载, 以前见人写过土豆就是这样做的
但是也许页面上不完全是图片信息会导致全部加载后变的无法接受
这里提供一个方法让JS延迟执行, 根据element是否已经显示在页面上来判断是否执行
(function ($) { var $window = $(window); var baseline; var currentHeight; var currentScroll; var documentHeight; var scrollQueue = []; // events strings var eventScroll = "scroll"; var eventResize = "resize"; var bothEvents = eventScroll + " " + eventResize; $.fn.scrollBind = function (action, optionsArr) { if (!action) { return; } if (!baseline) { syncBaseline(); } if (!documentHeight) { documentHeight = $(document).height(); } return this.each(applyBehavior); function applyBehavior() { var $this = $(this); var top = $this.offset().top; // !!!!!!! if (!top) { top = $this.closet(":visible").offset().top; } // load or enqueue (top < baseline) ? loadNow($this, action, optionsArr) : addToQueue({ top: top, item: $this, action: action, opts: optionsArr }); } } function syncBaseline(event) { if (!event) { currentScroll = $window.scrollTop(); currentHeight = $window.height(); } else { // on resize find the window height if (event.type == eventResize) { currentHeight = $window.height(); } // on scroll find where the window is scrolled to else if (event.type == eventScroll) { currentScroll = $window.scrollTop(); } } baseline = currentHeight + currentScroll; } function loadNow($item, action, opts) { action.call($item, opts); } function addToQueue(obj) { // the first time if (!scrollQueue.length) { attachEvents(); } scrollQueue.push(obj); } function attachEvents() { $window.bind(bothEvents, scrollHandler); } function scrollHandler(e) { syncBaseline(e); // load now if scrollQueue.top <= baseline for (var i = scrollQueue.length - 1; i >= 0 && scrollQueue[i].top <= baseline; i--) { var item = scrollQueue[i]; loadNow(item.item, item.action, item.opts); scrollQueue.splice(i, 1); } if (!scrollQueue.length) { $window.unbind(bothEvents, scrollHandler); } } })(jQuery);
也是对JS方法的调用套了一个入口, 未显示在用户眼前的element的JS代码不执行, 当用户下啦滚动条直至呈现时才执行
当然有个不算缺点的缺点: 必须保证你的页面布局离开JS不会混乱, 但似乎这是废话~~ 表现为什么要交给JS? 可实际上很多网站都在交给JS...
延迟加载JS的事例: 下载 别用IE打开这个事例~ 为了简单处理我用了min-height而IE是不支持这东西的~