1、 前端是庞大的,包括HTML、CSS、Javascript、Image、Flash 等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。那么,前端优化的目的是什么?1. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验。2. 从服务商角度而言,优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源。总之,恰当的优化不仅能够改善站点的用户体验并且能够节省相当的资源利用。前端优化的途径有很多,按粒度大致可以分为两类,第一类是页面级别的优化,例如 HTTP 请求数、脚本的无阻塞加载、内联脚本的位置优化等;第二类则是代码级别
2、的优化,例如 Javascript 中的 DOM 操作优化、CSS 选择符优化、图片优化以及 HTML 结构优化等等。另外,本着提高投入产出比的目的,后文提到的各种优化策略大致按照投入产出比从大到小的顺序排列。一、页面级优化1. 减少 HTTP 请求数这条策略基本上所有前端人都知道,而且也是最重要最有效的。都说要减少 HTTP 请求,那请求多了到底会怎么样呢?首先,每个请求都是有成本的,既包含时间成本也包含资源成本。一个完整的请求都需要经过 DNS 寻址、与服务器建立连接、发送数据、等待服务器响应、接收数据这样一个“漫长” 而复杂的过程。时间成本就是用户需要看到或者“感受”到这个资源是必须要等
3、待这个过程结束的,资源上由于每个请求都需要携带数据,因此每个请求都需要占用带宽。另外,由于浏览器进行并发请求的请求数是有上限的(具体参见此处),因此请求数多了以后,浏览器需要分批进行请求,因此会增加用户的等待时间,会给用户造成站点速度慢这样一个印象,即使可能用户能看到的第一屏的资源都已经请求完了,但是浏览器的进度条会一直存在。减少 HTTP 请求数的主要途径包括:(1). 从设计实现层面简化页面如果你的页面像百度首页一样简单,那么接下来的规则基本上都用不着了。保持页面简洁、减少资源的使用时最直接的。如果不是这样,你的页面需要华丽的皮肤,则继续阅读下面的内容。(2). 合理设置 HTTP 缓存缓
4、存的力量是强大的,恰当的缓存设置可以大大的减少HTTP 请求。以有啊首页为例,当浏览器没有缓存的时候访问一共会发出 78 个请求,共 600 多 K 数据(如图 1.1),而当第二次访问即浏览器已缓存之后访问则仅有 10 个请求,共20 多 K 数据(如图 1.2)。(这里需要说明的是,如果直接 F5 刷新页面的话效果是不一样的,这种情况下请求数还是一样,不过被缓存资源的请求服务器是 304 响应,只有 Header 没有 Body,可以节省带宽)怎样才算合理设置?原则很简单,能缓存越多越好,能缓存越久越好。例如,很少变化的图片资源可以直接通过HTTP Header 中的 Expires 设置
5、一个很长的过期头;变化不频繁而又可能会变的资源可以使用 Last-Modifed 来做请求验证。尽可能的让资源能够在缓存中待得更久。关于 HTTP 缓存的具体设置和原理此处就不再详述了,有兴趣的可以参考下列文章:HTTP1.1 协议中关于缓存策略的描述Fiddler HTTP Performance 中关于缓存的介绍(3). 资源合并与压缩如果可以的话,尽可能的将外部的脚本、样式进行合并,多个合为一个。另外,CSS、Javascript、Image 都可以用相应的工具进行压缩,压缩后往往能省下不少空间。(4). CSS Sprites合并 CSS 图片,减少请求数的又一个好办法。(5). In
6、line Images使用 data: URL scheme 的方式将图片嵌入到页面或 CSS中,如果不考虑资源管理上的问题的话,不失为一个好办法。如果是嵌入页面的话换来的是增大了页面的体积,而且无法利用浏览器缓存。使用在 CSS 中的图片则更为理想一些。(6). Lazy Load Images这条策略实际上并不一定能减少 HTTP 请求数,但是却能在某些条件下或者页面刚加载时减少 HTTP 请求数。对于图片而言,在页面刚加载的时候可以只加载第一屏,当用户继续往后滚屏的时候才加载后续的图片。这样一来,假如用户只对第一屏的内容感兴趣时,那剩余的图片请求就都节省了。有啊首页曾经的做法是在加载的时
7、候把第一屏之后的图片地址缓存在 Textarea 标签中,待用户往下滚屏的时候才“惰性” 加载。2. 将外部脚本置底前文有谈到,浏览器是可以并发请求的,这一特点使得其能够更快的加载资源,然而外链脚本在加载时却会阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验。解决这一问题的方法有很多,在这里有比较详细的介绍(这里是译文和更详细的例子),而最简单可依赖的方法就是将脚本尽可能的往后挪,减少对并发下载的影响。3. 异步执行 inline 脚本inline 脚本对
8、性能的影响与外部脚本相比,是有过之而无不及。首页,与外部脚本一样,inline 脚本在执行的时候一样会阻塞并发请求,除此之外,由于浏览器在页面处理方面是单线程的,当 inline 脚本在页面渲染之前执行时,页面的渲染工作则会被推迟。简而言之,inline 脚本在执行的时候,页面处于空白状态。鉴于以上两点原因,建议将执行时间较长的 inline 脚本异步执行,异步的方式有很多种,例如使用 script 元素的 defer 属性(存在兼容性问题和其他一些问题,例如不能使用 document.write)、使用 setTimeout,此外,在 HTML5 中引入了 Web Workers 的机制,恰
9、恰可以解决此类问题。4. Lazy Load Javascript随着 Javascript 框架的流行,越来越多的站点也使用起了框架。不过,一个框架往往包括了很多的功能实现,这些功能并不是每一个页面都需要的,如果下载了不需要的脚本则算得上是一种资源浪费-既浪费了带宽又浪费了执行花费的时间。目前的做法大概有两种,一种是为那些流量特别大的页面专门定制一个专用的 mini 版框架,另一种则是 Lazy Load。 YUI 则使用了第二种方式,在 YUI 的实现中,最初只加载核心模块,其他模块可以等到需要使用的时候才加载。5. 将 CSS 放在 HEAD 中如果将 CSS 放在其他地方比如 BODY
10、 中,则浏览器有可能还未下载和解析到 CSS 就已经开始渲染页面了,这就导致页面由无 CSS 状态跳转到 CSS 状态,用户体验比较糟糕。除此之外,有些浏览器会在 CSS 下载完成后才开始渲染页面,如果 CSS 放在靠下的位置则会导致浏览器将渲染时间推迟。6. 异步请求 Callback在某些页面中可能存在这样一种需求,需要使用 script标签来异步的请求数据。类似:Javascript: /*Callback 函数*/ function myCallback(info) /do something here HTML: cb 返回的内容: myCallback(Hello world!);
11、像以上这种方式直接在页面上写 对页面的性能也是有影响的,即增加了页面首次加载的负担,推迟了DOMLoaded 和 window.onload 事件的触发时机。如果时效性允许的话,可以考虑在 DOMLoaded 事件触发的时候加载,或者使用 setTimeout 方式来灵活的控制加载的时机。7. 减少不必要的 HTTP 跳转对于以目录形式访问的 HTTP 链接,很多人都会忽略链接最后是否带/,假如你的服务器对此是区别对待的话,那么你也需要注意,这其中很可能隐藏了 301 跳转,增加了多余请求。具体参见下图,其中第一个链接是以无/结尾的方式访问的,于是服务器有了一次跳转。8. 避免重复的资源请求这
12、种情况主要是由于疏忽或页面由多个模块拼接而成,然后每个模块中请求了同样的资源时,会导致资源的重复请求二、代码级优化1. Javascript(1). DOMDOM 操作应该是脚本中最耗性能的一类操作,例如增加、修改、删除 DOM 元素或者对 DOM 集合进行操作。如果脚本中包含了大量的 DOM 操作则需要注意以下几点:a. HTML Collection在脚本中document.images、document.forms、getElementsByTagName()返回的都是 HTMLCollection 类型的集合,在平时使用的时候大多将它作为数组来使用,因为它有 length 属性,也可以
13、使用索引访问每一个元素。不过在访问性能上则比数组要差很多,原因是这个集合并不是一个静态的结果,它表示的仅仅是一个特定的查询,每次访问该集合时都会重新执行这个查询从而更新查询结果。所谓的“访问集合”包括读取集合的length 属性、访问集合中的元素。因此,当你需要遍历 HTML Collection 的时候,尽量将它转为数组后再访问,以提高性能。即使不转换为数组,也请尽可能少的访问它,例如在遍历的时候可以将 length 属性、成员保存到局部变量后再使用局部变量。b. Reflow 代码块的行为实际上是修改了代码块中的执行环境,将 obj 放在了其作用域链的最前端,在 with 代码块中访问非局
14、部变量是都是先从 obj 上开始查找,如果没有再依次按作用域链向上查找,因此使用 with 相当于增加了作用域链长度。而每次查找作用域链都是要消耗时间的,过长的作用域链会导致查找性能下降。因此,除非你能肯定在 with 代码中只访问 obj 中的属性,否则慎用 with,替代的可以使用局部变量缓存需要访问的属性。(3). 避免使用 eval 和 Function每次 eval 或 Function 构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 通常比简单的函数调用慢 100 倍以上。eval 函数效率特别低,由于事先无法知晓传给 eval 的
15、字符串中的内容,eval 在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。Function 构造函数比 eval 略好,因为使用此代码不会影响周围代码;但其速度仍很慢。此外,使用 eval 和 Function 也不利于 Javascript 压缩工具执行压缩。(4). 减少作用域链查找前文谈到了作用域链查找问题,这一点在循环中是尤其需要注意的问题。如果在循环中需要访问非本作用域下的变量时请在遍历之前用局部变量缓存该变量,并在遍历结束后再重写那个变量,这一点对全局变量尤其重要,因为全局变量处于作用域链的最顶端,访问时的查找次数是最
16、多的。低效率的写法: /全局变量 var globalVar = 1; function myCallback(info) for( var i = 100000; i-;) /每次访问 globalVar 都需要查找到作用域链最顶端,本例中需要访问 100000 次 globalVar += i; 更高效的写法: /全局变量 var globalVar = 1; function myCallback(info) /局部变量缓存全局变量 var localVar = globalVar; for( var i = 100000; i-;) /访问局部变量是最快的 localVar += i;
17、 /本例中只需要访问 2 次全局变量 globalVar = localVar; 此外,要减少作用域链查找还应该减少闭包的使用。(5). 数据访问Javascript 中的数据访问包括直接量(字符串、正则表达式)、变量、对象属性以及数组,其中对直接量和局部变量的访问是最快的,对对象属性以及数组的访问需要更大的开销。当出现以下情况时,建议将数据放入局部变量:a. 对任何对象属性的访问超过 1 次b. 对任何数组成员的访问次数超过 1 次另外,还应当尽可能的减少对对象以及数组深度查找。(6). 字符串拼接在 Javascript 中使用 “+“号来拼接字符串效率是比较低的,因为每次运行都会开辟新的
18、内存并生成新的字符串变量,然后将拼接结果赋值给新变量。与之相比更为高效的做法是使用数组的 join 方法,即将需要拼接的字符串放在数组中最后调用其 join 方法得到结果。不过由于使用数组也有一定的开销,因此当需要拼接的字符串较多的时候可以考虑用此方法。关于 Javascript 优化的更详细介绍请参考:Write Efficient Javascript(PPT)Efficient JavaScript2. CSS 选择符在大多数人的观念中,都觉得浏览器对 CSS 选择符的解析式从左往右进行的,例如 #toc A color: #444; 这样一个选择符,如果是从右往左解析则效率会很高,因为
19、第一个 ID 选择基本上就把查找的范围限定了,但实际上浏览器对选择符的解析是从右往左进行的。如上面的选择符,浏览器必须遍历查找每一个 A 标签的祖先节点,效率并不像之前想象的那样高。根据浏览器的这一行为特点,在写选择符的时候需要注意很多事项,有人已经一一列举了,详情参考此处。3. HTML对 HTML 本身的优化现如今也越来越多的受人关注了,详情可以参见这篇总结性文章。4. Image 压缩图片压缩是个技术活,不过现如今这方面的工具也非常多,压缩之后往往能带来不错的效果,具体的压缩原理以及方法在Even Faster Web Sites第 10 章有很详细的介绍,有兴趣的可以去看看。总结本文从
20、页面级以及代码级两个粒度对前端优化的各种方式做了一个总结,这些方法基本上都是前端开发人员在开发的过程中可以借鉴和实践的,除此之外,完整的前端优化还应该包括很多其他的途径,例如 CDN、Gzip、多域名、无Cookie 服务器等等,由于对于开发人员的可操作性并不强大,在此也就不多叙述了,详细的可以参考 Yahoo 和 Google 的这些“金科玉律”。1.Javascript 简介HTML 是纯静态的的页面,而 Javascript 让页面有了动态的效果,比如;OA 中模块的拖拉所有的浏览器都会内置 Javascript 的解释器1992 年 Nombas 公司开发出 C 减减的嵌入式脚本语言。
21、这是最好的 HTML 页面的脚本语言。Netscape 为了扩展其浏览器的功能,开发了一套 LiveScript,并与 1995 年与 SUN 公司联合把其改名为 javascript,它的主要目的是处理一些输入的有效性验证,而之前这个工作是留给perl 之类的服务器端语言完成,在以前使用电话线调制解调器(28.8kb/s)的时代,如此慢的与服务器交互,这绝对是一件很痛苦的事情。Javascript 的出现,解决了这个问题,因为它的验证是基于客户端的。微软公司早期版本的浏览器仅支持自己的 vbscript,但后来不得不加入 javascriptIE3 中搭载 Javascipt 的克隆版本,命
22、名为 jscript在一次技术会议中,sun,microsoft,netscape 公司联合制定了 ECMA-Script 标准在 2005 前,网页上提示框,广告越来越多,把 javascipt 滥用,使 javascript 背上了大量的罪名。2005 年,google 公司的网上产品(google 地图,gmail,google 搜索建议)等使得 ajax 兴起,而 javascript 便是 ajax 最重要的元素之一Javascript 有三个部分组成ECMAScript DOM BOMWEB 标准网页主要有三部分组成(结构 HTML,XHTML,表现 CSS,行为 DOM,ECMA
23、)2.ECMA 脚本Javascript 的语法1. 区分大小写2. 弱类型变量 var age=10 var name=”dd”3. 每行结尾的分号可有可无,但建议还是加上4. 注释与 java 相同变量变量是通过 var 关键字来声明的。 (Variable)变量的命名规则与 java 一致注释有三种:/ /*/ 这个只能注释单行2.1 javascript 的 Hello worlddocument.write()是写在文档的最前面2.2 slice()、 substring()、subtrSlice 和 substring (2,5) 指的是从第 3 为开始,取(5-2)=3 个数,其
24、中 slice 的参数可以为负Substr(2,5)指的是从第 3 为开始,取 5 个数。但ECMAscript 没有对该方法进行标准化,因此尽量少使用该方法2.3 indexOf()和 lastIndexOf(),isNan,typeOfindexOf(”i”) /从前往后,i 在第几位indexOf(”i”,3)可选参数,从第几个字符开始往后找lastIndexOf(”i”) /从后往前,i 在第几位lastIndexOf (”i”,3) /从后往前,i 在第几位如果没找到,则返回-1String 类型的变量,在 Java 中,用“”符号表示字符串,用表示单个字符。而在 javascrip
25、t 中这两种都可以Nan(not a number)Alert(nan =nan)返回 false,因此不推荐使用 nan 本身,推荐函数 isNanAlert(isNanN(“ab”);/返回 falsetypeof 运算符var stmp = “test”;alert(typeof stmp); /输出 stringalert(type of 1);/输出 number此外:还有 boolean,undefined,object(如果是引用类型或者null 值,null 值返回 object,这其实是 ecmascript 的一个错误)当声明的变量未初始化的时候,它的值就是 undefin
26、ed.当没有这个变量的时候,typeof 变返回的值也是 undefined。但是没声明的变量是不能参与计算的。当函数无明确返回值时,返回的也是 undefinedFunction a()Alert(a() = undefined) /返回 trueAlert(null = undefined)/返回 true2.4 数值计算var mynum1 = 23.345;var mynum2 = 45;var mynum3 = -34;var mynum4 = 9e5; /科学计数法 为 9*10 五次方var fNumber = 123.456;alert(fNumber.toExponentia
27、l(1);/保留的小数点数 1.2e+2alert(fNumber.toExponential(2);/1.23e+2.5 布尔值var married = true;alert(“1.“ + typeof(married);/Booleanmarried = “true“;alert(“2.“ + typeof(married);/String.6 类型转换转换成 string 类型有三种方式var a = 3;var b = a + “;var c = a.toString();var d = “student“ + a;toString()var a=11;document.write(
28、a.toString(2) + “);/转成 2 进制document.write(a.toString(3) + “);/转成 3 进制如果不是数值,则转换报错parseInt()document.write(parseInt(“1red6“) + “);/返回 1,后面非数值的将全部忽略document.write(parseInt(“53.5“) + “);/返回 53document.write(parseInt(“0xC“) + “); /直接十进制转换 12document.write(parseInt(““) + “);/NANdocument.write(parseInt(“0
29、11“,8) + “); 返回 9document.write(parseInt(“011“,10) + “); /指定为十进制 返回 11parseFloat()与 parseInt()类似2.7 数组var aMap = new Array(“China“,“USA“,“Britain“);aMap20 = “Korea“;alert(aMap.length + “ “ + aMap10 + “ “ + aMap20);/aMap10返回 undefineddocument.write(aMap.join(“) + “); /用“”来连接var sFruit = “apple,pear,p
30、each,orange“;var aFruit = sFruit.split(“,“); var aFruit = “apple“,“pear“,“peach“,“orange“;alert(aFruit.reverse().toString();var aFruit = “pear“,“apple“,“peach“,“orange“;aFruit.sort();var stack = new Array();stack.push(“red“);stack.push(“green“);stack.push(“blue“);document.write(stack.toString() + “)
31、;var vItem = stack.pop(); / bluedocument.write(vItem + “);document.write(stack.toString(); / red green2.8 if 语句/首先获取用户的一个输入,并用 Number()强制转换为数字var iNumber = Number(prompt(“输入一个 5 到 100 之间的数字“, “);/第二个参数, 用于显示输入框的默认值if(isNaN(iNumber) /判断输入的是否是数字 NaN “Not a Number”document.write(“请确认你的输入正确“);else if(iN
32、umber 100 | iNumber “);document.write(ArgsNum() + “);document.write(ArgsNum(3) + “);从这个例子可以看出,方法可以没有参数,也可以没有返回值,但是照样可以传入参数和返回值。2.12 Date 对象var myDate1 = new Date(); /运行代码前的时间for(var i=0;i -1;var isKHTML = sUserAgent.indexOf(“KHTML“) -1 | sUserAgent.indexOf(“Konqueror“) -1 | sUserAgent.indexOf(“Apple
33、WebKit“) -1;/检测 IE、Mozillavar isIE = sUserAgent.indexOf(“compatible“) -1 var isMoz = sUserAgent.indexOf(“Gecko“) -1 /检测操作系统var isWin = (navigator.platform = “Win32“) | (navigator.platform = “Windows“);var isMac = (navigator.platform = “Mac68K“) | (navigator.platform = “MacPPC“) | (navigator.platform
34、 = “Macintosh“);var isUnix = (navigator.platform = “X11“) if(isOpera) document.write(“Opera “);if(isKHTML) document.write(“KHTML “);if(isIE) document.write(“IE “);if(isMoz) document.write(“Mozilla “);if(isWin) document.write(“Windows“);if(isMac) document.write(“Mac“);if(isUnix) document.write(“Unix“
35、);1.13 Global 对象其实 isNan,paraseInt 等都是 Global 对象的方法EncodeURI.因为有效的 URI 不能包含某些字符,如空格。这个方法就是用于将这个字符转换成 UTF-8 编码,使浏览器可以接受他们。Var suil = “ file/a.html”;Alert(encodeURI(suil);/ 方法Eval(“alert(hi)”);当解析程序发现 eval()时,它将把参数解析成真正的ECMA-script 语句,然后插入该语句所在位置。Global 除了有内置方法外,还有很多内置的属性:如:undefined,nan,Array,String,
36、Number,Date,RegExp 等1.14 Math 对象Max 方法,min 方法,ceil,floor,round,sqrt,randomMax(1,2,3);min(1.2,3.4);想取到 110 的数据Math.floor(Math.random()*10+1)29 的数据Math.floor(Math.random()*9+2);3.1 getElementsByTagNamefunction searchDOM()/放在函数内,页面加载完成后才用的 onload加载,这时如果把 alert 这句改成用 document.write 则会把原内容覆盖掉,因为是后面才执行的va
37、r oLi = document.getElementsByTagName(“li“);/输出长度、标签名称以及某项的文本节点值alert(oLi.length + “ “ +oLi0.tagName + “ “ + oLi3.childNodes0.nodeValue);var oUl = document.getElementsByTagName(“ul“);var oLi2 = oUl1.getElementsByTagName(“li“);alert(oLi2.length + “ “ +oLi20.tagName + “ “ + oLi21.childNodes0.nodeValue
38、);客户端语言HTMLJavaScriptCSS服务器端语言ASP.NETJSPPHP3.2 getElementByIdvar oLi = document.getElementById(“cssLi“);oLi.style .backgroundColor=“yellow“/输出标签名称以及文本节点值alert(oLi.tagName + “ “ + oLi.childNodes0.nodeValue);3.2 getElementsByNamealert(document.getElementsByName(“a“).length);3.3 访问子节点function myDOMInsp
39、ector()var oUl = document.getElementById(“myList“); /获取标记var DOMString = “;if(oUl.hasChildNodes()/判断是否有子节点var oCh = oUl.childNodes;for(var i=0;i糖醋排骨 圆笼粉蒸肉 泡菜鱼 板栗烧鸡麻婆豆腐 访问兄弟节点function myDOMInspector()var myItem = document.getElementById(“myDearFood“);/访问兄弟节点var nextListItem = myItem.nextSibling;var p
40、reListItem = myItem.previousSibling;alert(nextListItem.tagName +“ “+ preListItem.tagName);在 Firefox 中不支持,但是 IE 中却是支持的。3.6 第一个 最后一个 子节点 function nextSib(node)var tempLast = node.parentNode.lastChild;/判断是否是最后一个节点,如果是则返回 nullif(node = tempLast)return null;var tempObj = node.nextSibling;/逐一搜索后面的兄弟节点,直到发
41、现元素节点为止while(tempObj.nodeType!=1 /三目运算符,如果是元素节点则返回节点本身,否则返回 nullreturn (tempObj.nodeType=1)?tempObj:null;function prevSib(node)var tempFirst = node.parentNode.firstChild;/判断是否是第一个节点,如果是则返回 nullif(node = tempFirst)return null;var tempObj = node.previousSibling;/逐一搜索前面的兄弟节点,直到发现元素节点为止while(tempObj.nod
42、eType!=1 return (tempObj.nodeType=1)?tempObj:null;function myDOMInspector()var myItem = document.getElementById(“myDearFood“);/获取后一个元素兄弟节点var nextListItem = nextSib(myItem);/获取前一个元素兄弟节点var preListItem = prevSib(myItem);alert(“后一项:“ + (nextListItem!=null)?nextListItem.firstChild.nodeValue:null) + “ 前
43、一项:“ + (preListItem!=null)?preListItem.firstChild.nodeValue:null) );nodeType元素 element 1属性 attr 2文本 text 3注释 comments 8文档 document 9function showAttr()var btnShowAttr=document.getElementById(“btnShowAttr“); /演示按钮,有很多属性var attrs=btnShowAttr.attributes;for(var i=0;i11111p=document.getElementById(“mypa
44、ra“)pclone = p.cloneNode(true);p.parentNode.appendChild(pclone3.12 文档碎片function insertPs()var aColors = “red“,“green“,“blue“,“magenta“,“yellow“,“chocolate“,“black“,“aquamarine“,“lime“,“fuchsia“,“brass“,“azure“,“brown“,“bronze“,“deeppink“,“aliceblue“,“gray“,“copper“,“coral“,“feldspar“,“orange“,“orchid“,“pink“,“plum“,“quartz“,“purple“;