理解ajax
- 时间、质量和成本,三选二,一切都是权衡
- 优化的目的是希望降低程序的整体开销,更应该把重点放在程序整体开销影响最大的那部分
- ajax的实现方式是:发送数据包到服务端(json),然后服务端返回另一个数据包(json)来响应,最后JavaScript程序使用这些数据来更新浏览器的显示
- ajax应用常见错误是把所有应用数据都发送给浏览器,这会再次引入ajax本应避免的延迟问题,同事也会增加浏览器需要处理的数据量
- DOM API非常低效,运行程序最大的开销往往是DOM而非JavaScript,理解DOM的奥秘并减少它的影响比试图给脚本提速效果更好
- 误用酷炫特性也会导致不必要的DOM操作,从而带来令人吃惊的巨大开销
- 大部分JavaScript引擎的优化是为了抢占市场而不是性能
创建快速响应的Web应用
浏览器的简单工作原理:当用户和浏览器交互时,操作系统接收到计算机的输入后,打包为单独时间并放置到该应用的事件队列中,浏览器按队列顺序(先进先出)完成其队列中单独时间的处理,然后决定如何处理这个事件。并且这个处理过程实质上是单线程的,任意一个任务都能阻止其他任务的执行(具体细节会有所不同)。
足够快
Jakob Nielsen(Web可用性领域专家)对“足够快”的论述:
基于Web应用响应时间准则和所有其他应用一样,37年来这些准则毫无变化,所以它们也不太可能因新技术的出现而发生改变
0.1秒:用户直接操作UI中的对象的感觉极限。比如,从用户选择表格中一列到该列高亮或向用户反馈已被选择的时间间隔。理想情况下,它也是对列进行排序的响应时间–这种情况下用户会感知到它们正在给表格排序
1秒:用户随意在计算机指令空间进行操作而无需过度等待的感觉极限。0.2~1.0秒的延迟意味着会被用户注意到,因此感觉到计算机处于对指令的“处理中”,这有别于直接响应用户行为的指令
10秒:用户专注于任务的极限,超过10秒的任何操作都需要一个百分比完成指示器,以及一个方便用户中断操作且有清晰标识的方法。
测量延迟时间
手动代码检测(记录,在函数处添加计时器)
自动代码检测(性能分析,如firebug)
别把运行时间可能很长的低性能代码引入到网页中
线程处理
- 传统的解决方案是使用多线程来把开销很大的代码从与用户交互的线程中剥离开来,然而JavaScript不支持多线程
Brendan Eich(JavaScript的创立者):多线程在各方面违反了抽象概念,主要是产生了竞争状态,死锁的风险和悲观锁定开销,并且它们无法横向扩展区处理未来超级内核的亿万次计算能力。
所以我对于诸如“你将在什么时候为JavaScript添加多线程”之类的问题的默认回答是:“等你死了之后!”
确保响应速度
Google著名浏览器插件Gears实现了:WorkerPool API,它允许浏览器的主JavaScript线程创建后台的“Worker”,而这些Worker启用时从浏览器线程中接受到一些简单“信息”(如独立状态,不是共享变量),并且在完成时返回一条信息
- Web Worker API
- Gears Worker API
在浏览器的主线程上实现运行长时间的计算并保持快速响应的界面是可行的,但使用Worker会更加容易且高效
XMLHttpRequest有两个基本的执行模式:
异步:实质上是拥有一个专用API的Web Worker
同步:行为像在浏览器的主线程中执行它所有的工作一样,这回导致用户界面的持续延迟时间与发送它的请求并解析来自服务器端响应的整体耗时一样长,千万不要使用同步模式
创建快速响应网页的另一个关键方面是:内存管理
JavaScript像许多现代的高级语言一样,把低级的内存管理抽象出来,绝大部分的运行环境都实现了垃圾回收,然而自动内存管理是有开销的
JavaScript运行环境暂无成熟的工具监控运行环境中的GC进程的性能,或者观察堆中当前的对象集以便能诊断GC相关的问题
如果网页的内存需求增长到足够大,可能会迫使操作系统开始内存分页,一个极慢的进程凭借迫使其他进程放弃其真正的内存来给浏览器不断增长的需求腾出空间(内存页从屋里内存转移到虚拟内存),会导致性能降低
拆分初始化负载
如果把JavaScript分为两个部分:一部分是渲染初始化页面必须的,剩下的作为另一部分。与其给用户带来响应停顿的第一印象,倒不如在初始化时只加载必要的JavaScript,其余的JavaScript稍后再加载
- 大部分的JavaScript是可以延迟下载的
- 虽然一些函数未被使用,但仍然是必须的,比如错误处理和一些条件判断代码,函数的作用于和eval使这个挑战更加复杂
Doloto是微软开发的自动拆分JavaScript代码的系统,它可以把代码拆分到不同的组。第一组包含初始化网页所必须的函数,剩下的则是这些代码需要执行时按需加载它们,或者等到初始化的那些JavaScript代码加载完毕时再加载
Doloto决定什么位置拆分代码是基于训练阶段的,它会把JavaScript拆分成多个文件下载,对于大多数Web应用程序来说,最好把onload事件之前执行的JavaScript代码拆分成一个单独的文件,下载完成之后剩余的JavaScript采用无阻塞下载技术立即下载
未定义标识符和竞争状态
拆分JavaScript代码的一个难点是要避免出现未定义标识符错误,如果在JavaScript执行时引用到了一个被降级到延迟加载的标志符时,就会出现这种问题。应该建立一个下载JavaScript和触发执行行为之间的竞争状态,但仍无法完全避免
在延迟加载代码与用户界面元素相关联的情况下,可以通过改变元素的展现来解决此问题(如展示加载中…),另一个选择是在延迟加载的代码里绑定界面元素的事件处理程序(如初始化成一个静态文本,点击不会执行任何JavaScript)
在延迟加载代码不与界面元素关联的情况下,可以使用桩(stub)函数解决这个问题,桩函数是一个与原函数名称相同但是函数体为空,或者是用一些临时代码代替原有内容的函数。
更简单的方法是给每一个被引用但又被降级为延迟下载的函数创建一个桩函数。
ps: 拆分CSS样式表也是有益的,相对于拆分JavaScript,后者节省的资源要少一些,因为样式表的整体大小通常比JavaScript小,而且下载CSS并不会像JavaScript那样具有阻塞特性
无阻塞加载脚本
浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或JavaScript的名字空间,它们会对后续内容造成影响
很显然脚本必须按顺序执行,但没有必要按顺序下载
以下列出的技术既拥有外部脚本的好处,又能避免因阻塞导致的减速影响:
- XHR Eval
- XHR 注入(XHR Injection)
- Script in Iframe
- Script DOM Element(首选,但需要异步整合技术保证执行顺序)
- Script Defer
- document.write Script Tag
XHR Eval
通过XMLHttpRequest从服务端获取脚本,当响应完成时通过eval命令执行内容,XMLHttpRequest没有阻塞页面中其他组件
主要缺陷:通过XMLHttpRequest获取的脚本必须部署在和主页相同的域名中
XHR 注入
类似 XHR Eval,XHR注入也是通过XMLHttpRequest来获取JavaScript,但该机制是通过创建一个script的DOM元素,然后把响应注入script中来执行JavaScript,在某些情况下使用eval可能会比这种机制慢
Scirpt in Iframe
主页中的iframe和其他组件是并行加载的,利用iframe无阻塞加载JavaScript,该技术要求iframe URL和主页面同域,另外我们需要修改JavaScript来创建它们之间的关联
Iframe还存在自身的消耗,实际上iframe是开销最高的DOM元素,至少比普通DOM元素高出一个数量级
Script DOM Element
使用JavaScript动态创建script DOM元素并设置其src属性,下载过程中用这种方式创建脚本不会阻塞其他组件,该技术允许跨域获取脚本
Script Defer
IE 支持script的defer属性,可以让浏览器不必立即记载脚本,当脚本不包含document.write调用和其他脚本对其的依赖时,使用这个属性是安全的,defer是HTML4规范的一部分,只有IE和一些新浏览器支持它
document.write Script Tag
使用document.write把HTML标签script写入页面中,在IE中可以并行加载脚本,虽然可以并行下载脚本,但仍会阻塞其他类型的资源,因此不推荐该技术
浏览器忙指示器
- 状态栏
- 进度条
- 标签页图标
- 光标
- 阻塞渲染:对用户体验来说是非常不好的
- 阻塞onload时间:通过页面onload时间要等所有资源下载完成时才会触发,如果状态栏等待更长时间才显示完成,并且延迟默认输入框获取焦点,会影响用户体验
不同的延迟加载技术会触发不同的浏览器忙指示器
确保顺序执行
上面描述的某些高级下载技术并不能确保脚本按顺序执行,因为脚本时并行下载的,所以它们会按到达的顺序执行,而不是它们的排列顺序,这会导致竞争状态,进而导致未定义标识符错误
选用哪种技术取决于脚本的执行顺序是否与下载顺序有关,没有独立的最佳方案
为了避免下载太多不必要的JavaScript,通过服务端程序后端程序来实现是最有效的,只调用相关函数就把合适的技术插入到HTML文档响应中
整合异步脚本
当异步加载的外部脚本与行内脚本之间存在代码依赖时,我们必须通过一种保证执行顺序的方法来整合两个脚本
新款浏览器采用常规script标签方式并行下载,同时保持执行顺序,但我们需要的是一种异步加载脚本并且保持执行顺序的跨浏览器的解决方案
整合技术:
- 硬编码回调(Hardcoded Callback)
- Window Onload
- 定时器(Timer)
- Script Onload(可能是最好的选择)
- Degrading Script Tags
硬编码回调(Hardcoded Callback)
一种简单的整合技术是,让外部脚本调用行内代码里的函数,但是往往不太可能把回调嵌入第三方的JavaScript模块中,也不是很灵活,改变回调接口时需要调整外部脚本
Window Onload
通过监听window的onload事件来触发行内代码的执行,这使得只要确保外部脚本在window.onload之前下载执行就能保持执行顺序,部分异步加载技术能在部分浏览器中确保这点
定时器(Timer)
定时器技术指使用轮休方法来保证在行内代码执行之前所依赖的外部脚本已经加载,可能会增加页面开销或者存在延迟,存在极端问题:js加载失败后,轮询会无限进行下去
Script Onload
通过监听脚本的onload事件解决了上述技术的脆弱性,延迟和开销
Script Onload技术是整合异步加载脚本和行内脚本的首选
降级使用script标签
这个想法是让行内代码在外部脚本加载成功之后执行
优点:
- 更干净
- 更清晰
- 更安全
缺点:需要修改外部脚本
多个外部脚本
异步加载多个外部脚本,同时保持外部脚本和行内脚本的执行顺序,Managed XHR是一个选择,但是它的限制在于脚本必须和主页面同域。DOM Element和 Doc Write虽然可以跨域,但是在不同的浏览器中会有不同的表现,并且两种技术不能跨浏览器实现异步加载所有类型的资源。
综合解决方案
- 单个脚本:script dom element + script onload
- 多个脚本:EFWS.Script.loadScripts
现实中的异步加载
- Google分析
- Dojo
- YUI Loader
布置行内脚本
虽然行内脚本不会产生额外的HTTP请求,但是阻塞页面上资源的并行下载,还会阻塞逐步渲染
下面提供几个有效的解决方案:
- 把行内脚本移至底部
- 使用异步回调启动JavaScript的执行(setTimeout,onload事件)
- 使用script的defer属性(该属性也允许浏览器继续解析和渲染页面的同时延迟执行行业脚本)
保持CSS和JavaScript的执行顺序
鉴于样式的级联特性,按照不同的顺序加载它们可能会产生意想不到的结果,浏览器是按照样式表在页面中列出的顺序应用它们的,而与下载的顺序无关,CSS的应用规则同时适用于样式表和行内样式
风险:将行内脚本防止在样式表之后
浏览器也按顺序应用CSS和JavaScript,而当行内脚本防止在样式表之后时,该行为会明显地延迟资源的下载
浏览器要在样式表完全下载之后才开始执行行内脚本,因为行内脚本可能含有依赖于样式表中样式的代码
结论:在样式表后面的行内脚本会阻塞所有后续资源的下载
编写高效的JavaScript
管理作用域
当执行JavaScript代码时,JavaScript引擎会创建一个执行上下文,执行上下文(作用域)设定了代码执行时所处的环境。引擎会在页面加载后创建一个全局的执行上下文,每执行一个函数时都会创建一个对应的执行上下文,最终建立一个执行上下文的堆栈,当前起作用的执行上下文在堆栈的最顶部。
每个执行上下文都有一个与之关联的作用域链,用来解析标识符。(此处不摘录执行上下文和作用域链的关系)
使用局部变量
局部变量是JavaScript中读写最快的标识符
增长作用域链
在代码执行过程中,执行上下文对应的作用域链通常保持不变,然而有两个语句会临时增长执行上下文的作用域链:with、try-catch语句块中的catch从句
高效数据存取
数据在脚本中存储的位置直接影响脚本执行的总耗时,一般有4种地方可以存取数据:
- 字面量值
- 变量
- 数组元素
- 对象属性
从字面量读取值和从局部变量中读取值开销差异很少,可以忽略不计,真正的差异在于从数组和对象中读取数据
流控制
流控制或许是提升JavaScript性能最重要的一环
快速条件判断
使用switch语句还是一连串的if-else语句?
if-else语句的优化:
- 将条件按频率降序排列
- 降条件拆分成几个分支(尽量减少条件的判断)
switch避免了创建复杂的嵌套条件,允许“贯穿”条件,但JavaScript引擎下swithc语句的性能参差不齐
同样可以按出现频率的降序排列条件来提高switch语句的性能
在JavaScript中,当仅判断一两个条件时,if语句通常比switch语句更快
另一种选择:数据查询,使用数据查询少量的结果是不合适的,因为数组查询往往比少量的条件判断语句慢
快速循环
在JavaScript中循环是导致性能问题的常见起因,编写循环的方式能彻底改变它的执行时间
JavaScript开发者无法依赖编译器去优化循环的速度,无需去管源码的样子,所以了解各种编写循环的方式及它们对性能的影响,就显得尤为重要
循环:
- for
- do-while
- while
- for-in(避免)
展开循环:展开小循环以提高性能。这一做法的基础是通过限制循环的次数来减少循环的开销,这种方式降低了维护性
字符串优化
传统上,字符串连接一直是JavaScript中性能最低的操作之一,后来使用Array对象和join进行了补救’
在决定如何连接字符串时要考虑两个因素:
- 被连接字符串大小
- 数量
当字符串相对较少(少于20个字符)且连接的数量也较少时(少于1000个),所有的浏览器中使用假发运算符都能在不到1毫秒之内轻松完成连接
裁剪字符串
JavaScript中没有用于移除字符串头尾空白的原声修剪方法,最常见的是实现是正则表达式匹配字符串,然而这个方式有个基于正则表达式的性能问题
裁剪字符串的速度只有在整个执行过程中裁剪的频率足够大时才重要
避免运行时间过长的脚本
JavaScript代码执行时,所有用户的交互必然被中断,这是浏览器的一个重要特性,因为JavaScript在执行过程中可以改变页面的底层结构,所以有可能取消或改变用户交互的响应结果
常见的脚本执行过长的原因包括:
- 过多的DOM交互
- 过多的循环
- 过多的递归
使用定时器挂起
有必要在长时间执行的JavaScript代码中加入中断,最简单的方法是使用定时器
定时器是在六七中拆分执行JavaScript代码的惯用方式,每当脚本需要花太长时间来完成执行的时候,就让部分延迟执行
一般而言,延迟50~100毫秒比较合适,足够让浏览器有时间执行必要的页面更新
用于挂起的定时器模式
处理数组是引起脚本长时间运行的常见原因之一,利用定时器拆分执行时一个很好的备选方案,另一个流行的模式是利用定时器执行较大型运算中小而有序的部分
可伸缩的Comet
Comet是通用属于描述技术,协议和为浏览器提供可行且可扩展的低延迟数据传输实现的集合
Comet的目标包括随时从服务端向客户端推送数据,提升传统Ajax的速度和可扩展性
Comet工作原理
Comet利用HTTP规范中不常用的特性来工作,通过更智能的长连接管理和减少每个连接占用的服务器端资源,比传统Web服务器更易于提供更多的同步连接,使数据传输更快
Comet服务器通常会根据操作系统改进事件库:libevent,epoll,kqueue
传输技术
4种实现低延迟数据传输的方法,它们是Comet的基础:
- 轮询:简单轮询是效率最低但最简单的Comet技术
- 长轮询:服务器端只在有可用的新数据时才响应,要支持长轮询,服务端要完全保持一个所有未响应请求和它们对应连接的大集合,服务器通过返回Transfer-Encoding: chunked 或Connection:close响应来保持这些请求连接
- 永久帧:打开一个隐藏的iframe,请求一个基于HTTP1.1块编码的文档
- XHR流:与服务器通信的最简化API是通过XMLHttpRequest来实现的,它将是目前浏览器上最高效的Comet传输方式,如果流的响应连接时间太长,浏览器会占用过多的内存
HTML5正在完成WebSocket,它将提供一个Web安全的TCP套接字,使得从客户端到服务端的通讯方式大大简化
跨域
如果浏览器不支持跨域XHR,长轮询也同样不支持跨域请求,可以通过其他的一些临时方案来实现跨子域XHR
- Abe Fetting
- 支持跨域XHR的现代浏览器调用
- HTM5中引用postMessage
- 回调轮询/JSONP轮询
支持跨域的Comet非常重要:
- 对不同域名进行请求可以突破浏览器最多同时打开两个连接的限制
- 可以用于从第三方服务中检索数据
- Comet和HTTP可以运行在不同的服务器上,这样可以针对Comet和HTTP服务器进行不同的优化
在应用程序上的执行效果
客户端Comet的性能优化目的:
- 减少数据传输的延迟
- HTTP连接的保存和管理
- 远程消息和处理跨域问题
服务器端的性能优化目的:
- 保存和共享HTTP的连接数
- 尽量减少每个连接所消耗的内存、CPU、I/O和带宽
超越Gzip压缩
是否应该关注这些压缩功能失效的用户?如果只看到平均影响,并不能反映实际情况
为什么压缩功能失效
- 旧的浏览器:Accept-Encoding头损坏/根本没有Accept-Encoding头
- Web代理
- PC安全软件
如何帮助这些用户
发送更少的响应会使页面加载速度更快
- 使用事件委托:当多个元素都需要响应某个事件时,我们把这个事件的处理程序绑定到它们的父元素上,这项技术通常叫事件委托
- 使用相对URL
- 移除空白
- 移除属性的引号,注意两种不应该移除引号的情况:
- 网页依据XHMTL规则编写
- 避免属性值从不包含引号到包含引号这种意外情况所带来的bug,HTML的属性值应该始终被包含在引号内
- 避免行内样式
- 为JavaScript变量设置别名
引导用户升级浏览器
对gzip的支持进行直接探测‘
图像优化
在典型的网页中,图像的大小要占一半以上,最重要的是,如果想在不删减功能的前提下提升性能,图像优化是最易实施的方案
两步实现简单图像哟花
- 首先要确定图像的颜色数、分辨率和清晰度,对这几个参数的修改有损优化,同样也会影响图像的质量
- 利用无损压缩技术尽可能滴削减图像大小
图像格式
Web上的3种图像格式:JPEG、PNG和GIF
- 就图像格式而言,GIF通常用来显示图形,而JPEG更适合显示照片,PNG两者都适合
- 像素是图像中最小的信息单元, 可以使用不同的颜色模型来描述像素,RGB颜色模型是最常见的一种
- 将图像中各种不同的颜色提取出来建立一个表,这个表通常叫做调色板
- RGBA:在RGB基础上扩展alpha透明
- 隔行扫描:部分图形格式支持对那些连续采样的图形进行隔行扫描
GIF:图形交换格式(Graphics Interchange Format),一种调色板图像格式
- 支持透明度
- 支持动画
- 无损
- 支持逐行扫描
- 支持隔行扫描
JPEG:联合图像专家小组(Joint Photographic Experts Group),是照片存储的实际标准
- 有损
- 不支持透明和动画
- 渐进JPEG支持隔行扫描
PNG: 便携式网络图片(Portable Network Graphics)
- 真彩色和调色板PNG格式
- 支持alpha透明
- 还没有跨浏览器动画解决方案
- 无损
- 逐行扫描
- 隔行扫描
自动无损图像优化
PNG格式将图像信息保存在块中,对于Web显示来说,大部分的块并非必要,可以安全地将它们删除,还有一点好处,将叫做gamma的块删除后,实际上会提升跨浏览器的显示效果
工具:
- Pngcrush
- PNGOUT
- OptiPNG
- PngOptimizer
JPEG文件包含如下元数据:
- 注释
- 应用程序定义的内部信息
- EXIF信息
这些元数据不影响图像显示,可以被安全地移除
工具: - jpegtran
- ExifTool
将GIF转换成PNG:ImageMagick
优化GIF动画:Gifsicle
在线的图像优化工具:Smush.it
使用渐进JPEG格式来存储大图形
Alpha透明:避免使用AlphalmageLoader
实现跨浏览器的alpha透明效果比想象的要难很多
略
优化Sprite
CSS Sprite:将多个背景图片合并到一个较大的图片中
Sprite的困惑:页面数量、优化、维护,三者只能选择两项
超级Sprite VS 模块化Sprite
页面很少:所有图像放到一个超级Sprite中
很多页面:模块化Sprite
高度优化的CSS Sprite
- 按照颜色合并
- 避免不必要的空白
- 将元素水平排列
- 将颜色限制在256以内
- 先优化单独的图像、再优化Sprite
- 控制大小和对齐减少反锯齿像素的数量
- 避免使用对角线渐变
- 避免在IE6种使用alpha透明图像
- 每2~3个像素改变渐变的颜色
- 处理Logo的时候要小心
其他图形优化
- 避免对图形进行缩放
- 优化生成的图像
- Favicons
- Apple触摸图标
划分主语
当从单个域下载资源成为瓶颈时,可将资源分配到多个域上,通过增加并行的下载数来提高页面速度
尽早刷新文档的输出
大多数情况下,浏览器会在HTML文档加载完毕后才开始渲染页面,同时下载页面上的资源
刷新文档头部的输出:PHP可以通过调用flush函数加快头部资源的下载(总会找到一种方法来刷新STDOUT)
块编码: HTTP/1.1引入了Transfer-Encoding:chunked响应头,HTML文档被分成多个数据库返回,每个响应的数据库都以标识其大小的指示符为开头,这样浏览器在下载数据包后马上开始解析,使得页面的加载速度更快;通过块编码,服务器可以尽早发送响应,因为只需要知道每个发送快的大小即可
启用压缩功能后,缓冲输出机制将会阻止刷新
少用iframe
iframe也叫内嵌frame,它允许把一个HTML文档嵌入另一个中。iframe常用于整合诸如广告这样的HTML内容,它们一般来自和主页不同的站点
缺点:性能较低
- 开销最高的DOM
- 阻塞onload时间
- 某些情况下主页面会阻塞iframe中资源的下载
简化CSS选择符
设计CSS编写相关内容,此处省略
- 避免使用统配推责
- 不要限定ID选择符
- 不要限定类选择符
- 让规则越具体越好
- 避免使用后台选择符
- 避免使用标签–子选择符
- 质疑子选择符的所有用途
- 依靠继承
性能工具
数据包嗅探器
- HttpWatch
- Firebug网络控制面板
- AOL Pagetest(IE的插件)
- VRTA
- IBM Page Detailer
- Web Inspector资源控制面板(Safari)
- Fiddler
- Charles(类似fiddler)
- WireShark
Web开发工具
- Firebug
- Web Inspector
- IE Developer Toolbar
性能分析器
不同的分析器分析性能的最佳实践列表会有差异
- YSlow
- AOL Pagetest
- VRTA
- neXpert
杂项
- Hammerhead
- Smush.it
- Cuzillion
- UA Profiler