JS 性能优化
性能优化不可避免。任何一种提高运行效率,降低运行开销的行为都可以看作是一种优化操作。
一、内存优化
内存泄漏案例:
function fn () {
arrList = [];
arrList[100000] = 'yueluo is a coder';
}
fn();
function fn () {
arrList = [];
arrList[100000] = 'yueluo is a coder';
}
fn();
内存管理
内存:由读写单元组成,表示一片可操作空间
管理:人为的去操作一片空间的申请、使用和释放
内存管理:开发者主动地申请空间、使用空间、释放空间
管理流程:申请 - 使用 - 释放
JavaScript 内存管理
申请内存空间
使用内存空间
释放内存空间
// 申请空间
let obj = {};
// 使用空间
obj.name = 'yueluo';
// 释放空间
obj = null;
// 申请空间
let obj = {};
// 使用空间
obj.name = 'yueluo';
// 释放空间
obj = null;
垃圾回收
- JavaScript 中内存管理是自动的
- 对象不再被引用时被视为垃圾
- 对象不能访问到时被视为垃圾
可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript 中的根可以理解为是全局变量对象
引用与可达
let obj = { name: 'yueluo' };
let obj2 = obj;
obj = null;
console.log(obj); // { name: 'yueluo' };
let obj = { name: 'yueluo' };
let obj2 = obj;
obj = null;
console.log(obj); // { name: 'yueluo' };
function group (obj1, obj2) {
obj1.next = obj2;
obj2.prev = obj1;
return {
o1: obj1,
o2: obj2
}
}
let obj = group({ name: 'obj1' }, { name: 'obj2' });
console.log(obj); // {o1: {…}, o2: {…}}
function group (obj1, obj2) {
obj1.next = obj2;
obj2.prev = obj1;
return {
o1: obj1,
o2: obj2
}
}
let obj = group({ name: 'obj1' }, { name: 'obj2' });
console.log(obj); // {o1: {…}, o2: {…}}

GC 算法
GC 定义与作用
GC 就是垃圾回收机制的简写,GC 可以找到内存中的垃圾、并释放和回收空间。
GC 中的垃圾
程序中不再需要使用的对象、程序中不能再访问到的对象
GC 算法
GC 是一种机制,垃圾回收器完成具体的工作
工作的内容就是查找垃圾释放空间、回收空间
算法就是工作时查找和回收所遵循的规则
常见 GC 算法
- 引用计数
- 标记清除
- 标记整理
- 分代回收
引用计数算法
核心思想:设置引用数,判断当前引用数是否为 0
引用关系改变时,引用计数器修改引用数字。
const user1 = { age: 11 };
const user2 = { age: 12 };
const user3 = { age: 13 };
const nameList = [user1.age, user2.age, user3.age];
function fn () {
num1 = 1;
num2 = 2;
}
fn();
const user1 = { age: 11 };
const user2 = { age: 12 };
const user3 = { age: 13 };
const nameList = [user1.age, user2.age, user3.age];
function fn () {
num1 = 1;
num2 = 2;
}
fn();
优点:
- 发现垃圾时立即回收
- 最大限度减少程序暂停
缺点:
- 无法回收循环引用的对象
- 时间开销大
function fn () {
const obj1 = {};
const obj2 = {};
obj1.name = obj2;
obj2.name = obj1;
return 'yueluo is a coder';
}
fn();
function fn () {
const obj1 = {};
const obj2 = {};
obj1.name = obj2;
obj2.name = obj1;
return 'yueluo is a coder';
}
fn();
存在互相引用关系,使用引用计数算法无法进行垃圾回收。
标记清除算法
核心思想:分标记和清除二个阶段完成
遍历所有的对象找标记活动对象,遍历所有对象清除没有标记对象,回收相应空间。

第一个阶段中找到所有可达对象,如果涉及到引用层次关系会递归查找进行标记。
第二个阶段会进行清除,找到没有标记的对象,进行垃圾回收,同时将第一次的标记进行清除。这样就完成一次垃圾回收。
最终会把回收的空间放到空闲列表上面,方便后续程序申请空间使用。
优点:
- 相对于标记清除算法,可以解决对象循环引用问题;
缺点:
- 易产生空间碎片化,垃圾回收后导致地址不连续,不能让空间得到最大化使用
标记整理算法
标记整理算法可以看作是标记清除算法的增强。
标记阶段的操作和标记清除一致,清除阶段会先执行整理,移动对象位置,让地址产生连续,最大化的利用空间。



GC 算法总结
- 引用计数
- 内部通过引用计数器维护每个对象的引用数值,通过数值是否为 0, 判断是否是垃圾对象
- 可以及时回收垃圾对象、最大限度减少程序卡顿时间
- 无法回收循环引用的对象、资源消耗较大(维护引用计数器,需要频繁操作)
- 标记清除
- 遍历所有对象,将当前活动对象进行标记、遍历所有对象,将没有被标记的对象释放掉
- 可以回收循环引用的对象
- 容易产生碎片化空间,浪费空间、不会立即回收垃圾对象(清除时存在全停顿现象)
- 标记整理
- 和标记清除类似,不过要在清除前先整理当前内存空间
- 减少碎片化空间
- 不会立即回收垃圾对象
V8
介绍
V8 是一款主流的 JavaScript 执行引擎。
- 采用即时编译
- 内存设有上限
- windows 64位操作系统不超过 1.5 GB,32 位操作系统不超过 800 MB
- V8 本身是为浏览器制造的,现有内存大小对于网页应用来说是足够使用的
- V8 内部使用的垃圾回收机制也决定它采用这种方式
垃圾回收策略
采用分代回收的思想,把内存空间按照一定规则分成两类,新生代和老生代。
针对不同对象采用不同算法。
V8 中常用 GC 算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
回收新生代对象
V8 内存空间一分为二,小空间用于存储新生代对象(64 位 32 M,32 位 16 M )。
新生代指的是存活时间较短的对象。

主要采用复制算法和标记整理算法
- 将新生代内存区分为二个等大小空间,使用空间为 From 状态、空闲空间为 To 状态
- 活动对象存储于 From 空间,标记整理后将活动对象拷贝至 To
- 拷贝过程存在晋升现象,将新生代对象移动至老生代
- 晋升条件:一轮 GC 还存活的新生代需要晋升,To 空间的使用率超过 25%
- From 与 To 交换空间完成内存释放
回收老生代对象
64 位操作系统 1.4 G,32 位操作系统 700 M。
老生代对象就是指存活时间较长的对象。
主要采用标记清除、标记整理、增量标记算法
- 使用标记清除完成垃圾空间的回收
- 新生代对象向老生代区域移动,并且老生代区域不足以存放新生代区域对象,采用标记整理进行空间优化
- 采用增量标记进行效率优化
新老生代垃圾回收对比
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法
- 空间一分为二,浪费空间
- 存放数据较多,复制过程消耗时间较多
标记增量如何优化垃圾回收
垃圾回收工作时会阻塞当前 JavaScript 程序执行,存在空档期(全停顿)。
标记增量就是将一整段的垃圾回收操作拆分成多个小步,组合完成整个回收,不需要一次完成整个垃圾回收过程。标记增量可以让垃圾回收和程序执行交替工作。

垃圾回收总结
V8 是一款主流的 JavaScript 执行引擎,V8 内存设置上限。
V8 采用分代回收的思想实现垃圾回收,将内存分为新生代和老生代,分别使用不同的 GC 算法。
Performance
工具介绍
GC 的目的是为了实现内存空间的良性循环。
良性循环的基石是合理使用。需要时刻关注内存变化。
Performance 提供多种监控方式。
通过使用 Performance 时刻监控内存。
使用步骤
- 打开浏览器输入目标网址
- 打开开发人员工具面板,选择性能
- 开启录制功能,访问具体页面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的体现
分析内存问题的前提条件是网络环境正常。
外在表现
- 页面出现延迟加载或经常性暂停(,可能出现频繁的垃圾回收
- 页面持续性出现糟糕的性能,可能存在内存膨胀
- 页面的性能随时间加长越来越差
监控内存的方式
界定内存问题的标准
- 内存泄漏:内存使用持续升高
- 内存膨胀:多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的几种方式
- 浏览器任务管理器
- Timeline 时序图记录
- 堆快照查找分析 DOM
- 判断是否存在频繁的垃圾回收
任务管理器监控内存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="J-btn">Button</button>
<script>
const oBtn = document.getElementId('J-btn');
oBtn.onclick = function () {
let arrList = new Array(1000000);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="J-btn">Button</button>
<script>
const oBtn = document.getElementId('J-btn');
oBtn.onclick = function () {
let arrList = new Array(1000000);
}
</script>
</body>
</html>
shift + esc 打开浏览器任务管理器。任务管理器只能说明存在问题,但是不能很准确的定位问题。

Timeline 记录内存
<button id="J-btn">Add</button>
<script>
const oBtn = document.getElementById('J-btn');
oBtn.onclick = function () {
let arrList = new Array(1000000);
}
</script>
<button id="J-btn">Add</button>
<script>
const oBtn = document.getElementById('J-btn');
oBtn.onclick = function () {
let arrList = new Array(1000000);
}
</script>

堆快照查找分离 DOM
分离 DOM
- 界面元素存活在 DOM 树上
- 垃圾对象时的 DOM 节点
- 分离状态的 DOM 节点(界面已经不存在,但是存在 JS 引用)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="J-btn">Add</button>
<script>
const oBtn = document.getElementById('J-btn');
let tempElem;
function test () {
for (let i = 0; i < 100000; i++) {
const ul = document.createElement('ul');
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
ul.appendChild(ul);
}
tempElem = ul;
}
}
oBtn.addEventListener('click', test);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="J-btn">Add</button>
<script>
const oBtn = document.getElementById('J-btn');
let tempElem;
function test () {
for (let i = 0; i < 100000; i++) {
const ul = document.createElement('ul');
for (let i = 0; i < 10; i++) {
const li = document.createElement('li');
ul.appendChild(ul);
}
tempElem = ul;
}
}
oBtn.addEventListener('click', test);
</script>
</body>
</html>

判断是否存在频繁 GC
频繁垃圾回收现象
- GC 工作时应用程序是停止的
- 频繁且过长的 GC 会导致应用假死
- 用户使用中感知应用卡顿
如何确定频繁地垃圾回收
- Timeline 中频繁的上升下降
- 任务管理器中数据频繁的增加或者减小
Performance 总结
Performance 使用
- Performance 使用流程
- 内存问题的相关分析
- Performance 时序图监控内存变化
- 任务管理器监控内存变化
二、代码优化
精准测试 JavaScript 性能
- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于 Benchmark.js 的 https://jsperf.com/ 完成,在线脚本性能测试
Jsperf 使用流程
- GitHub 账号登录
- 填写个人信息(非必须)
- 填写详细的测试用例信息(title、slug)
- 填写准备代码(DOM 操作经常使用)
- 填写必要的 setup 和 teardown 代码
- 填写代码测试片段
慎用全局变量
程序运行过程中,如果需要对某些数据进行存储,需要尽可能的放到局部作用域中,使用局部变量。
- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出
- 如果某个局部作用域出现同名变量则会遮蔽或污染全局
缓存全局变量
将使用中无法避免的全局变量缓存到局部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="button" value="btn" id="J-btn1" />
<input type="button" value="btn" id="J-btn2" />
<input type="button" value="btn" id="J-btn3" />
<input type="button" value="btn" id="J-btn4" />
<p>1111</p>
<input type="button" value="btn" id="J-btn5" />
<input type="button" value="btn" id="J-btn6" />
<p>2222</p>
<input type="button" value="btn" id="J-btn7" />
<input type="button" value="btn" id="J-btn8" />
<p>3333</p>
<input type="button" value="btn" id="J-btn9" />
<input type="button" value="btn" id="J-btn10" />
<script>
function getBtn () {
let oBtn1 = document.getElementById('J-btn1');
let oBtn3 = document.getElementById('J-btn3');
let oBtn5 = document.getElementById('J-btn5');
let oBtn7 = document.getElementById('J-btn7');
let oBtn9 = document.getElementById('J-btn9');
}
function getBtn2 () {
const _doc = document;
let oBtn1 = _doc.getElementById('J-btn1');
let oBtn3 = _doc.getElementById('J-btn3');
let oBtn5 = _doc.getElementById('J-btn5');
let oBtn7 = _doc.getElementById('J-btn7');
let oBtn9 = _doc.getElementById('J-btn9');
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="button" value="btn" id="J-btn1" />
<input type="button" value="btn" id="J-btn2" />
<input type="button" value="btn" id="J-btn3" />
<input type="button" value="btn" id="J-btn4" />
<p>1111</p>
<input type="button" value="btn" id="J-btn5" />
<input type="button" value="btn" id="J-btn6" />
<p>2222</p>
<input type="button" value="btn" id="J-btn7" />
<input type="button" value="btn" id="J-btn8" />
<p>3333</p>
<input type="button" value="btn" id="J-btn9" />
<input type="button" value="btn" id="J-btn10" />
<script>
function getBtn () {
let oBtn1 = document.getElementById('J-btn1');
let oBtn3 = document.getElementById('J-btn3');
let oBtn5 = document.getElementById('J-btn5');
let oBtn7 = document.getElementById('J-btn7');
let oBtn9 = document.getElementById('J-btn9');
}
function getBtn2 () {
const _doc = document;
let oBtn1 = _doc.getElementById('J-btn1');
let oBtn3 = _doc.getElementById('J-btn3');
let oBtn5 = _doc.getElementById('J-btn5');
let oBtn7 = _doc.getElementById('J-btn7');
let oBtn9 = _doc.getElementById('J-btn9');
}
</script>
</body>
</html>
通过原型添加方法
在原型对象上新增实例对象需要的方法 。
var Fn1 = function () {
this.foo = function () {
console.log(1111);
}
}
let f1 = new Fn1();
var Fn1 = function () {
this.foo = function () {
console.log(1111);
}
}
let f1 = new Fn1();
var Fn2 = function () {}
fn2.prototype.foo = function () {
console.log(1111);
}
let f2 = new Fn2();
var Fn2 = function () {}
fn2.prototype.foo = function () {
console.log(1111);
}
let f2 = new Fn2();
避开闭包陷阱
闭包特点
- 外部具有指向内部的引用
- 在 “外” 部作用域访问 “内” 部作用域的数据
关于闭包
- 闭包是一种强大的语法。
- 闭包使用不当很容易出现内存泄漏
- 不要为了闭包而闭包
function foo () {
var name = 'yueluo';
function fn () {
console.log(name);
}
return fn;
}
var a = foo();
a();
function foo () {
var name = 'yueluo';
function fn () {
console.log(name);
}
return fn;
}
var a = foo();
a();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="button" id="J-btn" />
<script>
function foo () {
var el = document.getElementById('J-btn');
el.onclick = function () {
console.log(el.id);
}
}
foo();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="button" id="J-btn" />
<script>
function foo () {
var el = document.getElementById('J-btn');
el.onclick = function () {
console.log(el.id);
}
}
foo();
</script>
</body>
</html>
存在内存泄漏。
function foo () {
var el = document.getElementById('J-btn');
el.onclick = function () {
console.log(el.id);
}
el = null;
}
foo();
function foo () {
var el = document.getElementById('J-btn');
el.onclick = function () {
console.log(el.id);
}
el = null;
}
foo();
避免属性访问方法使用
JavaScript 中的面向对象
- JS 不需属性的访问方法,所有属性都是外部可见的
- 使用属性访问方法只会增加一层重定义,没有访问的控制力
function Person () {
this.name = 'coder';
this.age = 23;
this.getAge = function () {
return this.age;
}
}
const p = new Person();
const age = p.getAge();
function Person () {
this.name = 'coder';
this.age = 23;
this.getAge = function () {
return this.age;
}
}
const p = new Person();
const age = p.getAge();
function Person () {
this.name = 'coder';
this.age = 23;
this.getAge = function () {
return this.age;
}
}
const p = new Person();
const age = p.age;
function Person () {
this.name = 'coder';
this.age = 23;
this.getAge = function () {
return this.age;
}
}
const p = new Person();
const age = p.age;
提供成员方法在执行效率上比直接访问效率低。
For 循环优化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<script>
var oBtns = document.getElementsByClassName('btn');
for (var i = 0; i < oBtns.length; i++) {
console.log(i);
}
for (var i = 0, len = oBtns.length; i < len; i++) {
console.log(i);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<script>
var oBtns = document.getElementsByClassName('btn');
for (var i = 0; i < oBtns.length; i++) {
console.log(i);
}
for (var i = 0, len = oBtns.length; i < len; i++) {
console.log(i);
}
</script>
</body>
</html>
选择最优的循环方法
var arr = new Array(1, 2, 3, 4, 5);
console.time();
arr.forEach(function (item) {
console.log(item);
});
console.timeEnd();
console.time();
arr.map(function (item) {
console.log(item);
});
console.timeEnd();
console.time();
for (var i = arr.length; i; i--) {
console.log(arr[i]);
}
console.timeEnd();
console.time();
for (var i in arr) {
console.log(arr[i]);
}
console.timeEnd();
var arr = new Array(1, 2, 3, 4, 5);
console.time();
arr.forEach(function (item) {
console.log(item);
});
console.timeEnd();
console.time();
arr.map(function (item) {
console.log(item);
});
console.timeEnd();
console.time();
for (var i = arr.length; i; i--) {
console.log(arr[i]);
}
console.timeEnd();
console.time();
for (var i in arr) {
console.log(arr[i]);
}
console.timeEnd();
效率对比: for > for in > map > forEach。
浏览器可能对 forEach 有优化操作,所以差距也不明显。
文档碎片优化
节点的添加操作必然会有回流和重绘,建议使用 DocumentFragment。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
<script>
const oFrag = document.createDocumentFragment();
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
oFrag.appendChild(oP);
}
document.body.appendChild(oFrag);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
<script>
const oFrag = document.createDocumentFragment();
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
oFrag.appendChild(oP);
}
document.body.appendChild(oFrag);
</script>
</body>
</html>
克隆优化节点操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="J-box">old</p>
<script>
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
<script>
const oldP = document.getElementById('J-box');
for (var i = 0; i < 10; i++) {
var oP = oldP.cloneNode(false);
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="J-box">old</p>
<script>
for (var i = 0; i < 10; i++) {
var oP = document.createElement('p');
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
<script>
const oldP = document.getElementById('J-box');
for (var i = 0; i < 10; i++) {
var oP = oldP.cloneNode(false);
oP.innerHTML = i;
document.body.appendChild(oP);
}
</script>
</body>
</html>
直接量替换 new Object
var arr = [1, 2, 3];
var arr2 = new Array(1, 2, 3);
var arr = [1, 2, 3];
var arr2 = new Array(1, 2, 3);
。。。