前端性能优化(JavaScript 篇)

1.优化循环

如果现在有一个 data[] 数组,需要对其进行遍历,应当怎么做?最简单的代码是:

for(let i = 0; i < data.length; i++){
}

这里每次循环开始都需要判断 i 是否小于 data.length, JavaScript 并不会对 data.length 进行缓存,而是每次比较都会进行一次取值,如我们所说,JavaScript 数组其实是一个对象,里面有一个 length 属性,所以这里实际上就是取得对象的属性,如果直接使用变量的话就会少一次索引对象,如果数组的元素很多,效率提升还是很可观的,所以我们通常将代码改成如下所示:

for(let i = 0, m = data.length; i < m; i++){
}

这里多加了一个变量 m 用于存放 data.length 属性,这样就可以在每次循环时,减少一次索引对象,但是代价是增加了一个变量空间,如果遍历不要求顺序,我们甚至可以不用 m 这个变量存储长度,在不要求顺序的时候可以使用如下代码:

for(let i = data.length; i--; ){
}
// 当然 也可以用 while 来代替
let i = data.length;
while(i--){
}

这样就只使用一个变量了。

2.运算结果缓存

由于JavaScript 中的函数也是对象(JavaScript中一切都是对象),所以我们可以给函数添加任意属性,这也就为我们提供符合备忘录模式的缓存运算结果的功能,如果我们有一个需要大量运算才能得到结果的函数如下:

function calculator(params){
	// 大量的耗时的计算
	return result;
}

如果其中不涉及随机,参数一样时所返回的结果一致,我们就可以将运算结果进行缓存,从而避免重复的计算:

function calculator(params){
	let cacheKey = JSON.stringify(params);
	let cache = calculator.cache = calculator.cache || {};
	if(typeof cache[cacheKey] !== 'undefined'){
		return cache[cacheKey];
	}
	// 大量的计算
	cache[cacheKey] = result;
	return result;
}

这里将参数转化为 JSON 字符串作为 key,如果这个参数已经被计算过,那么就直接返回,否则进行计算,计算完毕后再添加 cache 中,如果需要,可以直接查看 cache 的内容:calculator.cache
这是一种很典型的空间换时间的方式,由于浏览器的页面存活时间一般不会很长,占用的内存会很快被释放(当然也有例外,比如一些 WEB 应用),所以可以通过这种空间换时间的方式来减少响应时间,提升用户体验,这种方式不适合如下场合:

  • 相同的参数可能 产生不同的结果(包含随机数之类的)
  • 运算结果占用特别多的内存的情况

3.不要在循环中创建函数

这个很好理解,每创建一个函数对象是需要大批量空间的,所以在一个循环中创建函数是不明智的,尽量将函数移动到循环之外创建,比如如下代码:

for(let i = 0, m = data.length; i < m; i++){
	handlerData(data[i],function(data){
	  // do something
	})
}
// 这个可以 修改为:
let handler = function(data){
	// do something
}
for(let i = 0, m = data.length; i < m ; i ++){
	handlerData(data[i], handler)
}

4.让垃圾回收器回收那些不再需要的对象

如果我们长时间保存对象,老生代中占用的空间将增大,每次在老生代中的垃圾回收过程将会相当漫长,而垃圾回收器判断一个对象为活对象还是死对象,是按照是否有活对象或根对象含有对它的引用来判定的,如果有根对象或者活对象引用了这个对象,它将被判断为活对象,所以我们需要通过手动消除这些引用来让垃圾回收器来回收这些对象。

delete
一种方式是通过 delete 方式来消除对象中的键值对,从而消除引用,但是这种方式并不提倡,它会改变对象的结构,可能导致引擎中对对象的的存储方式变更,降级为字典方式进行存储,不利于 JavaScript 引擎的优化,所以尽量减少使用。

全局对象
另外需要注意的是,垃圾回收器认为根对象永远是活对象,永远不会对其进行垃圾回收,而全局对象就是根对象,所以全局作用域中的变量将会一直存在。

事件处理器的回收
在平常写代码的时候,我们经常会给一个 DOM 节点绑定事件处理器,但有时候我们不需要这些事件处理器后,就不管它们了,它们默默的在内存中保存着,所以在某些 DOM 节点绑定的事件处理器不需要后,我们应当销毁它们,同时绑定的时候也尽量使用事件代理的方式进行绑定,以免造成多次重复的绑定导致内存空间的浪费。
闭包导致的内存泄漏
JavaScript 的闭包可以说即是“天使”又是“魔鬼”,它天使的一面是我们可以通过它突破作用域的限制,而其魔鬼的一面就是容易导致内存泄漏,比如如下情况:

let result = (function(){
	let small = {};
	let big = new Array(10000000);
	return function(){
		if(big.indexOf('someValue') !== -1){
			return null;
		}else{
			return small;
		}
	}
})()

这里,创建了一个闭包,使得返回的函数存储在 result 中,而 result 函数能够访问其作用域内的 small 对象和 big 对象。由于 big 对象和 small 对象都可能被访问,所以垃圾回收器不会去碰这两个对象,它们不会被回收,我们上述代码改为下述形式:

let result = (function(){
	let small = {};
	let big = new Array(10000000);
	let hasSomeValue;
	hasSomeValue = big.indexOf('someValue') !== -1;
	return function(){
		if(hasSomeValue){
			return null;
		}else{
			return small;
		}
	}
})

这样,函数内部只能够访问到 hasSomeValue 变量和 small 变量了,big 没有办法通过任何形式被访问到,垃圾回收器将会对齐进行回收,节省了大量的内存。

5.慎用 eval 和 with

Douglas Crockford 将 eval 比作魔鬼,确实在很多方面我们可以找到更好的替代方式,使用它时需要在运行时调用解释引擎对 eval() 函数内部的字符串进行解释运行,这需要大量的时间,像 Function、setInterval、setTimeout 也是类似的

Douglas Crockford也不建议使用 with, with 会降低性能,通过 with 包裹的代码块,作用域将会额外增加一层,降低索引效率。

6.对象的优化

缓存需要被使用的对象
JavaScript 获取数据的性能如下(从快到慢): 变量获取 > 数组下标获取(对象的整数索引获取)> 对象属性获取(对象非整数索引获取)。我们可以通过最快的方式代替最慢的方式:

let body = document.body;
let maxLength = someArray.length;

需要考虑,作用域链和原型链中的对象索引,如果作用域和原型链较长,也需要对所需要的变量继续缓存,否则沿着作用域链和原型链向上查找时也会消耗额外的时间。

缓存正则表达式对象
需要注意,正则表达式对象的创建非常消耗时间,尽量不要在循环中创建正则表达式,尽可能多的对正则表达式对象进行复用。

考虑对象和数组
在 JavaScript 中我们可以使用两种存放数据:对象和数组,由于 JavaScript 数组可以存放任意类型数据这样的灵活性,导致我们经常需要考虑何时使用数组,何时使用对象,我们应当在如下情况作出考虑:

  1. 存储一串相同类型的对象,应当使用数组
  2. 存储一对键值对,值得类型多样性,应当使用对象
  3. 所有值都是通过整数索引,应当使用数组

数组使用时的优化

  1. 往数组中插入混合类型很容易降低数组使用的效率,尽量保持数组中元素的类型一致。
  2. 如果使用稀疏数组,它的元素访问将远慢于满数组的元素访问,因为 V8 为了节省空间,会将对稀疏数组通过字典方式保存在内存中,节约了空间,但增加了访问时间。

对象的拷贝
需要注意的是,JavaScript 遍历对象和数组时,使用的是 for…in 的效率相当低,所以在拷贝对象时,如果已知需要被拷贝的对象的属性,通过直接赋值的方法比使用 for…in 方式要来得快,我们可以通过设定一个拷贝构造函数来实现,如下:

function copy(source){
	let result = {};
	let item;
	for(item in source){
		result[item] = source[item];
	}
	return result;
}
let backup = copy(source);
// 可修改为 
function copy(source){
	this.property1 = source.property1;
	this.property2 = source.property2;
	this.property3 = source.property3;
	// ...
}
let backup2 = new Copy(source)

字面量代替构造函数
JavaScript 可以通过字面量来构造对象,比如通过 [] 构造一个数组, {} 构造一个对象, /regexp/ 构造一个正则表达式,我们应当尽力使用字面量来构造对象,因为字面量是引擎直接解释执行的,而如果使用构造函数的话,需要调用一个内部构造器,所以字面量略微要快一点点。

缓存Ajax

(1)函数缓存
我们可以使用前面缓存复杂计算函数接的方式进行缓存,通过在函数对象上构造 cache 对象,原理一样,这里略过,这种方式是精确到函数,而不是精确到请求。
(2)本地缓存
HTML5 提供了本地缓存 sessionStroage 和 localStorage,区别就在于前者在浏览器关闭后会自动释放,而后者则是永久的,不会被释放,它提供的缓存大小是以 MB 为单位的,比 cookie 要大的多,所以我们可以提供 Ajax 数据缓存的存活时间来判断存放在 sessionStorage 还是 localStorage 当中,在这里以存储到 sessionStorage 中为例:

function(data, url, type, callback){
	let storage = window.sessionStorage;
	let key = JSON.stringify({
		url:url,
		type:type,
		data:data
	});
	let result = storage.getItem(key);
	let xhr;
	if(result){
		callback.call(null, result);
	}else{
		xhr.onreadystatechange = function(){
			if(xhr.readyState === 4){
				if(xhr.status === 200){
					storage.setItem(key,xhr.responseText);
					callback.call(null,xhr.responseText);
				}else{}
			}
		}
		xhr.open(type, url, async);
		xhr.send(data);
	}
}

使用布尔表达式的短路
使用原生方法
在 JavaScript 中,大多数原生方法是使用 C++ 编写的,比 JS写的方法要快得多,所以尽量使用原生对象和方法。

字符串拼接
在 IE 和 FF 下,使用直接 += 的方式或是 + 的方式进行字符串拼接,将会很慢,我们可以通过 Array 的 join() 方法进行字符串拼接,不过并不是所有的浏览器都是这样,现在很多浏览器使用 += 比 join() 方法还要快。

4.JavaScript 文件的优化

使用 CDN
在编写 JavaScirpt 代码中,我们会经常使用库(jQuery等等),这些 JS 库通常不会对其进行更改,我们可以将这些库文件放在 CDN (内容分发网络上),这样能大大减少响应时间。

压缩与合并 JavaScript 文件
在网络只不过传输 JS 文件,文件越长,需要的时间越多,所以在上线前,通常都会对 JS 文件进行压缩,去掉其中的注释、回车、不必要的空格等多余内容,如果通过 uglify 的算法,还可以缩减变量名和函数名,从而将 JS 代码压缩,节约传输带宽,另外经常也会将 JavaScript 代码合并,使所有代码在一个文件之中,这样就能够减少 HTTP 请求次数,合并的原理和 sprite 技术相同

使用 Application Cache 缓存

你可能感兴趣的:(前端,JavaScript)