前端性能优化
一、减少HTTP请求数量
(1)CSS Sprites
(2)内联图片(图片base64)
(3)最大化合并JS、CSS模块
(4)利用浏览器缓存
1.1 CSS Sprites
将多个图片合并成一张图,只向图片发送一次请求的技术。此时可以通过background-position根据位置定位到不同的图片。虽然合并之后的一张图片包含附加的空白区域,会让人觉得比单个图片合并起来的图片更大。实际上,合并后的图片会比分离的图片的总和要小,因为一来将多次请求合并成了一次,二来降低了图片自身的开销(颜色表,格式信息等等)。
举个例子,如果有需要请求四个25k的图片,那么直接请求100k的图片会比发送四次请求要快一些。因为多次http请求会产生性能开销和图片自身的开销。
1.2 内联图片
通过data:URL模式可以在Web页面包含图片但无需任何额外的HTTP请求。data:URL中的URL是经过base64编码的。格式如下:
<img src="data:image/gif;base64......" alt="home" />
由于使用内联图片(图片base64)是内联在HTML中的,因此在跨越页面时不会被缓存。一般情况下,不要将网站的Logo做图片base64的处理,因为编码过的Logo会导致页面变大。可将图片作为背景,放在CSS样式表中,此时CSS可被浏览器缓存
.home{
background-image:url(data:image/gif;base64...)
}
1.3 最大化JS、CSS的合并
考虑到HTTP请求会带来额外的性能开销,因此下载单个100kb的文件比下载4个25kb的文件更快。最大化合并JS、CSS将会改善性能。
1.4 利用浏览器缓存
减少呈现页面时所必需的HTTP请求的数量是加速用户体验的最佳方式。可以通过最大化浏览器缓存组件的能力来实现。
1.4.1 什么是缓存
如果组件(HTML、CSS、JS、图片资源等)被缓存到浏览器中,在下次再次加载的时候有可能从组件中获取缓存,而不是向服务器发送HTTP请求。减少HTTP请求有利于前端性能优化。
1.4.2 浏览器如何缓存
浏览器在下载组件(HTML、CSS、JS、图片资源等),会将他们缓存到浏览器中。如果某个组件确实更新了,但是仍然在缓存中。这时候可以给组件添加版本号的方式(md5)避免读取缓存。
1.4.3 浏览器再次下载组件时,如何确认时缓存的组件
(1)Expires头
可以通过服务器设置,将某个组件的过期时间设置的长一些。比如,公司的Logo不会经常变化等。浏览器在下载组件时,会将其缓存。在后续页面的查看中,如果在指定时间内,表明组件是未过期的,则可以直接读取缓存,而不用走HTTP请求。如果在指定时间外,则表明组件是过期的,此时并不会马上发起一个HTTP请求,而是发起一个条件GET请求。
(2)条件GET请求
如果缓存的组件过期了(或者用户reload,refresh了页面),浏览器在重用它之前必须先检查它是否仍然有效。这称为一个条件GET请求。这个请求是浏览器必须发起的。如果响应头部的Last-Modified(最后修改时间,服务器传回的值)与请求头部的If-Modified-Since(最新修改时间)的值匹配,则会返回304响应(Not-Modified),即直接从浏览器读取缓存,而不是走HTTP请求。
(3)Etag(实体标签)
Etag其实和条件GET请求很像,也是通过检测浏览器缓存中的组件与原始服务器上的组件是否匹配。如果响应头部的Etag与请求头部的If-None-Match的值互相匹配,则会返回304响应。
Etag存在的一些问题:
- 如果只有一台服务器,使用Etag没有什么问题。如果有多台服务器,从不同服务器下载相同的组件返回的Etag会不同,即使内容相同,也不会从缓存中读取,而是发起HTTP请求。
- Etag降低了代理缓存的效率。
- If-None-Match比If-Modified-Since拥有更高的优先级。即使条件GET请求的响应头部和请求头部的两个值相同,在拥有多台服务器的情况下,不是从缓存中读取,而是仍然会发起HTTP请求。
有两种方式可以解决这个问题
- 在服务端配置Etag。
- 在服务端移除Etag。移除Etag可以减少响应和后续HTTP请求头的大小。Last-Modified可以提供完全等价的信息
二、压缩响应主体内容大小
(1)压缩HTTP响应包(Accept-Encoding:gzip,deflate)
(2)压缩HTML、CSS、JS模块
1.组件(HTML、CSS、JS)压缩处理
2.配置请求头部信息:Accept-Encoding:gzip,deflate。此时服务器返回的响应头部中会包含Content-endcoding:gzip的信息,表明http响应包被压缩。
三、DOM方面
(1)离线操作DOM
(2)使用innerHTML进行大量的DHTML操作
(3)使用事件代理
(4)缓存布局信息
(5)移除页面上不存在的事件处理程序
3.1 离线操作DOM
如果需要给页面上某个元素进行某种DOM操作时(如增加某个子节点或者增加某段文件或者删除某个节点),如果直接对在页面上进行更新,此时浏览器需要重新计算页面上所有DOM节点的尺寸、进行重排和重绘。现场进行的DOM更新越多,所花费的时间就越长。重排是指某个DOM节点发生位置变化时(删除、移动、CSS盒模型等),重新绘制渲染🌲的过程。重绘是指将发生位置变化的DOM节点重新绘制到页面上的过程。
var list = document.getElementById("myList"),
item,
i;
for(i=0;i<10;i++){
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item"+i));
}
以上元素进行了20次的现场更新,有10次是将li插入到list元素中,另外10次文本节点。这里就产生了20次DOM的重排和重绘。此时可以采用以下方法,来减少DOM元素的重排和重绘。
采用文档碎片:
var list = document.getElementById("myList"),
item,
i,
frag = document.createDocumentFragment(); // 文档碎片
for ( i = 0 ; i < 10 ; i ++ ) {
item = document.createElement ("li");
frag.appendChild(item);
item.appendChild(document.createTextNode("Item"+i);
}
document.body.appendChild(frag);
3.2 使用innerHTML进行大量的DHTML操作
有两种在页面上创建DOM节点的方法:
使用诸如createElement()和appendChild()这类的DOM方法,以及使用innerHTML。
对于小的DOM更改而言,两种方法效率都差不多。然而,对与大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法时编译好的而非解释执行的,所以执行快得多。
var ul = document.querySelector('ul');
var html='';
for(var i=0;i<10;i++){
html += '<li>'+ i +'</li>;
//避免在for循环中使用innerHTML,因为在循环中使用innerHTML会导致现场更新!
}
ul.innerHTML=html;//循环结束时插入到ul元素中
这段代码构建了一个HTML字符串,然后将其指定到list.innerHTML,便创建了需要的DOM结构。虽然字符串连接上总是有点性能损失,但这种方式还是要比进行多个DOM操作更快。
3.3 使用事件代理
在javascript中,在页面渲染时添加到页面上的事件处理程序数量直接关系到页面的整体运行性能。最直接的影响是页面的事件处理程序越多,访问DOM节点的次数也就越多。另外函数是对象,会占用内存。内存中的对象越多,性能就越差。
事件代理就是解决"过多的事件处理程序"的。事件代理基于事件冒泡机制。因此,可以将同一事件类型的事件都绑定在document对象上,根据事件对象的target属性下的id,class 或者name属性,判断需要给哪个DOM节点绑定事件处理程序。这种事件代理机制在页面渲染时将访问多次DOM节点减少到了一次,因为此时我们只需要访问document对象。如下实现:
document.addEventListener('click',function(e){
switch ( e.target.id){
case 'new':
console.log('new');
break;
case 'name':
console.log('name');
break;
case 'sex':
console.log('sex');
break;
}
},false)
使用事件代理有以下优点:
1.可以再页面声明周期的任何时间点上添加事件处理程序(无需等待DOMContentLoaded和Load事件)。换句话说,只要某个需要添加事件处理程序的元素存在页面上,就可以绑定相应的事件。
2.DOM节点访问次数减少。
3.事件处理程序是函数,而函数是对象。对象会占用内存。事件处理程序减少了,所占用的内存空间就少了,就能够提升整体性能。
3.4 缓存布局信息
当在实际应用中需要获取页面上某个DOM节点的布局信息时,如offset dimension,client dimension或者是样式等,浏览器为了返回最新值,会刷新整个DOM树去获取。最好的做法是缓存布局信息,减少布局信息的获取次数。获取之后将其缓存到局部变量中,然后再操作此局部变量。
如,需要将某个DOM节点沿对角线移动,一次移动一个像素,从100 100移动到500 500。
如果这样做,对于性能优化来说是低效的。
div.style.left=1+div.clientLeft+'px';
div.style.top=1+div.clientTop+'px';
if(div.style.clientLeft>=500 && div.style.clientTop>=500){
//停止累加...
}
下面使用局部变量缓存局部信息,对于性能优化来说是高效的。
let left=div.clientLeft,right=div.clientTop;
div.style.left=1+left+'px';
div.style.right=1+'right'+'px';
if(div.style.clientLeft>=500 && div.style.clientTop>=500){
//停止累加...
}
3.5 移除页面上不存在的事件处理程序
假设有这样一个需求:页面上有一个按钮,在点击时需要替换成某个文本。入股哦直接替换该按钮,由于该按钮的事件处理程序已经存在内存中了,此时移除按钮并没有将事件处理程序一同移除,页面仍然持有对该按钮事件处理程序的引用。一旦这种情况出现多次,那么原来添加到元素中的事件处理程序会占用内存。在事件代理中也谈过,函数是对象,内存中的对象越多,性能就越差。除了文本替换外,还可能出现在移除(removeChild)、替换(replaceChild)带有事件处理程序的DOM节点。
而正确的做法是,在移除该按钮的同时,移除事件处理程序。
<div class="content">
<button class="btn">点击</ button>
</div>
<script>
var btn=document.quertySelector('.btn');
btn.addEventListener('click',function func(e){
btn.removeEventListener('click',func,false); //在替换前,移除该按钮的事件处理程序
document.quertySelector('.content').innerHTML="替换button按钮啦!';
},false)
</script>
四、JavaScript语言本身的优化
(1)减少对象成员及数组项的查找次数
(2)避免使用with语句和eval函数
4.1 减少对象成员及数组项的查找次数
这点主要体现在循环体上。以for循环为例,缓存数组长度,而不是在每次循环中获取。
假设有一个arr数组,长度为50000
//低效的,每次都要获取数组长度
for ( var i = 0; i < arr.length ; i++){
//do something.....
}
// for 循环性能优化;缓存数组长度
for ( var i = 0, len=arr.length ; i<len ; i ++){
//do something.....
}
五、ajax优化
(1)get或者post请求
5.1 get 或者 post请求
这里可以扯一下get和post请求的区别。
对于get请求来说,主要用于获取(查询)数据。get请求的参数需要以query string的方式添加在URL后面的。当我们需要从服务器获取或者查询某数据时,都应该使用get请求。优点在于get请求比post请求要快,同时get请求可以被浏览器缓存。缺点在于get请求的参数大于2048个字符时,超过的字符会被截取,此时需要post请求。
对于post请求来说,主要用于保存(增加值,修改值,删除值)数据。post请求的参数是作为请求的主体提交到服务器。优点在于没有字节的限制。缺点是无法被浏览器缓存。
get和post请求有一个共同点:虽然在请求时,get请求将参数带在url后面,post请求将参数作为请求的主体提交。但是请求参数都是以 name1=value1&name2=value2 的方式发送到服务器的。
所以,扯了那么多。要注意的是,get请求用于查询(获取)数据,post请求用于保存(增删改)数据。
5.2 跨域JSONP
由于同源政策的限制,ajax只能在同域名、同协议、同端口的情况下才可以访问。也就是说,跨域是不行的。但是使用JSONP的方式可以绕过同源政策。
JSONP实现的原理:动态创建script标签。通过src属性添加需要访问的地址,将返回的数据作为参数封装在回调函数中。
JSONP的优点:
1.跨域请求。
2.由于返回的参数是JavaScript代码,而不是作为字符串需要进一步处理。所以速度快
JSONP的缺点:
1.只能以get请求发送。
2.无法为错误、失败事件设置事件处理程序。
3.无法设请求头
六、其他方面的性能优化
(1)使用CDN加载静态资源
(2)CSS样式放在头部
(3)JS脚本放在底部
(4)避免使用CSS表达式
(5)外联JS、CSS
(6)减少DNS查找
(7)避免URL重定向
6.1 将样式表放在顶部
CSS样式表可以放在两个地方,一是文档头部,一是文档底部。位置不同会带来不同的体验。
当样式表放在文档底部时,不同浏览器会出现不同的效果。
IE浏览器在新窗口打开、刷新页面时,浏览器会阻塞内容的逐步呈现,取而代之的是白屏一段时间,等到CSS样式下载完毕之后再将内容和样式渲染到页面上;在点击链接、书签栏、reload时,浏览器会先将内容逐步呈现,等到CSS样式加载完毕之后再重新渲染DOM树,此时会发生无样式内容的闪烁问题。
火狐浏览器不管以什么方式打开浏览器都会将内容逐步呈现,然后等到css样式加载完毕之后再重新渲染DOM树,发生无样式内容的闪烁的问题。
当样式表放在文档顶部时,虽然浏览器需要先加载CSS样式,速度可能比放在底部的慢些,但是由于可以使页面内容逐步呈现,所以对用户来时还是快的。因为有内容呈现了而不是白屏,发生无样式内容的闪烁,用户体验也会友好些。毕竟,有内容比白屏要好很多吧...
将样式放在文档顶部有两种方式。当使用link标签将样式放在head时,浏览器会使内容逐步呈现,但是会发生无样式内容的闪烁问题;当使用@import规则,由于会发生模块(图片、样式、脚本)下载时的无序性,可能会出现白屏的现象。另外,在style标签下可以使用多个import规则,但是必须放置在其他规则之前。link和@import引入样式也存在性能问题,推荐引入样式时都使用link标签。
6.2 将脚本放在底部
脚本放在文档顶部会导致如下问题:
- 脚本会阻塞其后组件的并行下载和执行
- 脚本会阻塞其后页面的逐步呈现
6.3 外联javascript、css
外联javascript、css文件相对于内联有以下优点。外联的方式可以通过script标签或者link标签引入,也可以通过动态方式创建script标签和link标签(动态脚本、动态样式),此时通过动态方式创建的脚本和样式不会阻塞页面其他组件的下载和呈现
1.可以被浏览器缓存
2.作为组件复用
最简单,最常用的方法:
1、将img标签更改为背景图片。
2、将图片合并在一起成为一张大图,利用background-position根据位置定位到不同的图片。
3、使用innerHTML方法创建DOM节点。
4、使用事件代理,将同一事件类型的事件都绑定在document对象上。
5、移除页面上不存在的事件处理程序。
6、减少对象成员及数组项的查找次数。