一、基础知识
1、不可避免
随着软件开发行业的不断发展,性能优化已经是一个不可避免的话题。从初、中级工程师,到高级、资深工程师,以及技术专家,可以说每个人都在谈性能优化。
2、什么是性能优化
任何一种能够提高运行效率,降低运行开销的行为都可以看做是优化操作
3、前端性能优化
请求资源时所用到的网络以及数据的传输方式,开发过程中所用到的框架等等
二、代码优化方法1
使用工具精准测试 JavaScript 性能
本质上就是采集大量的执行样本进行数学统计和分析
1、慎用全局变量
为什么要慎用
- 全局变量定义在全局执行上下文,是所有作用域链的顶端,导致变量查找消耗时间过长
- 全局执行上下文一直存在于上下文执行栈,知道程序退出,对于GC工作也是不利的
- 如果某个局部作用域出现了同名变量则会遮蔽或污染全局变量
// 使用全局变量
var i, str = ''
for (i = 0; i < 100; i++) {
str += i
}
// 使用局部变量
for (let i = 0; i < 100; i++) {
let str = ''
str += i
}
运行后,通过对比很容易看出使用局部变量执行效率更高。
2、缓存全局变量
将使用中无法避免的全局变量缓存到局部。比如说在查找DOM元素的时候就不可避免的用到document。如果需要大量的查找元素,这个时候缓存document到局部就会提升我们的执行效率。
// 假如存在大量的按钮
// 执行效率低
function getBtn () {
let btn1 = document.getElementById('btn1')
let btn2 = document.getElementById('btn2')
let btn3 = document.getElementById('btn3')
let btn4 = document.getElementById('btn4')
...
}
// 执行效率高
function getBtn () {
let doc = document
let btn1 = doc.getElementById('btn1')
let btn2 = doc.getElementById('btn2')
let btn3 = doc.getElementById('btn3')
let btn4 = doc.getElementById('btn4')
...
}
3、通过原型对象添加附加方法
// 在原型对象上新增实例对象需要的方法
// 执行效率低
var fn1 = function() {
this.foo = function() {
console.log(111)
}
}
let f1 = new fn1()
// 执行效率高
var fn2 = function() {}
fn2.prototype.foo = function() {
console.log(111)
}
let f2 = new fn2()
4、避开闭包陷阱
// 假设有一个id为btn的按钮
function foo() {
val el = document.getElementById('btn')
el.onclick = function() {
console.log(el.id)
}
el = null // 解决闭包产生的内存泄漏
}
foo()
5、避免属性访问方法使用
// JS不需属性访问方法,所有属性都是外部可见的
// 使用属性访问方法只会增加一层重定义,没有访问的控制力
// 执行效率低
function Person () {
this.name = 'zhagnsan'
this.age = 20
this.getAge = function () {
return this.age
}
}
const p1 = new Person()
const a = p1.getAge
// 执行效率高
function Person () {
this.name = 'zhagnsan'
this.age = 20
}
const p2 = new Person()
const b = p2.age
6、for循环优化
将循环体中经常使用又不变动的数据放到循环体的外部,做一个缓存,这样代码在执行过程中少做一些事情。
let arr = Array(10).fill('123')
// 执行效率低
for (let i = 0; i < arr.length; i++) {
console.log(i)
}
// 执行效率高
for (let i = 0, len = arr.length; i < len; i++) {
console.log(i)
}
7、选择最优循环方法
let arr = Array(10).fill('123')
// 执行效率最高,如果不做什么操作,可优选forEach循环
arr.forEach(item => {
console.log(item)
})
// 执行效率高
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i])
}
// 执行效率低
for (let i in arr) {
console.log(arr[i])
}
8、文档碎片优化节点添加
// 节点的添加操作必然会有回流和重绘
// 执行效率低
for (let i = 0; i < 10; i++) {
var oP = document.createElement('p')
oP.innerHtml = i
document.body.appendChild(oP)
}
// 执行效率高
const fragEle = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
var oP = document.createElement('p')
oP.innerHtml = i
fragEle.appendChild(oP)
}
document.body.appendChild(fragEle)
9、克隆优化节点操作
// 假如已经有一个p标签 <p id="p1">old</p>
// 执行效率低
for (let i = 0; i < 3; i++) {
var oP = document.createElement('p')
oP.innerHtml = i
document.body.appendChild(oP)
}
// 执行效率高
var oldP = document.getElementById('p1')
for (let i = 0; i < 3; i++) {
var newP = oldP.cloneNode(false)
newP.innerHtml = i
document.body.appendChild(newP)
}
10、直接量替换 new Object
// 执行效率高
var a = [1, 2, 3]
// 执行效率低
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
三、代码优化方法2
1、堆栈中的JS执行过程
2、减少判断层级
在编写代码的时候,很可能出现判断条件嵌套的场景,可以通过提前 return 掉那些不通过的条件,来优化执行效率。
需求:
- 当前有一些视频学习课程,分为多个视频类型
- 每个视频类型下面又分为免费和收费模块
- 假设前5个小节是免费的,后面是收费的
// 执行效率低
function fn(part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (part) {
if (parts.includes(part)) {
console.log('属于当前课程')
if (chapter > 5) {
console.log('需要付费观看')
}
}
} else {
console.log('请确认模块信息')
}
}
fn('ES2016', 6)
// 执行效率高
function fn(part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (!part) {
console.log('请确认模块信息')
return
}
if (!parts.includes(part)) {
console.log('不属于当前课程')
return
}
console.log('属于当前课程')
if (chapter > 5) {
console.log('需要付费观看')
}
}
fn('ES2016', 6)
3、减少作用域链查找层级
// 执行效率低
var name = 'zhangsan'
function fn () {
name = 'lisi' // name 属于全局作用域,修改了全局作用域的 name 值
function fn2() {
var age = 20 // 属于当前fn2作用域
// 当前作用域存在age,直接输出age
console.log(age)
// 当前作用域不存在name,向上查找fn作用域,也不存在name,
// 再向上查找全局作用域找到name,输出name
console.log(name)
}
fn2()
}
fn()
// 执行效率高
var name = 'zhangsan'
function fn () {
var name = 'lisi' // name 属于当前fn作用域
function fn2() {
var age = 20 // 属于当前fn2作用域
// 当前作用域存在age,直接输出age
console.log(age)
// 当前作用域不存在name,向上查找fn作用域找到name,输出name
console.log(name)
}
fn2()
}
fn()
4、减少数据读取次数
// 假设存在这样一个DOM节点 <div id="skip" class="skip"></div>
// 执行效率低
var box = document.getElementById("skip")
function hasEle(ele, cls) {
// 这个地方ele是一个对象,所用到的属性可能很多,嵌套层级可能很深
// 可能在下面也会用到很多次,每次都会去读取数据
return ele.className == cls
}
console.log(hasEle(box, 'skip'))
// 执行效率高
var box = document.getElementById("skip")
function hasEle(ele, cls) {
// 将对象的属性缓存起来,下面使用的时候就不会多次去读取数据
const className = ele.className
return className == cls
}
console.log(hasEle(box, 'skip'))
5、字面量与构造式
// 对于引用类型
// 执行效率低
const test = () => {
// new Object() 可以看做在调用一个函数,做的事情更多一些
let obj = new Object()
obj.name = 'zhangsan'
obj.age = 20
obj.slogan = '好好学习,天天长胖'
return obj
}
console.log(test())
// 执行效率高
const test = () => {
// 使用字面量的方式,直接开辟空间存入数据
let obj = {
name: 'zhangsan',
age: 20,
slogan: '好好学习,天天长胖'
}
return obj
}
console.log(test())
// 对于基本数据类型
// 执行效率高
const str1 = '好好学习,天天长胖'
console.log(str1)
// 执行效率低
const str2 = new String('好好学习,天天长胖')
console.log(str2)
// 总结:字面量创建执行效率高,构造函数执行效率低
6、减少循环体中的活动
// 执行效率低
const test = () => {
const arr = ['zhangsan', '20', '好好学习,天天长胖']
for(let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
}
test()
// 执行效率高
const test = () => {
const arr = ['zhangsan', '20', '好好学习,天天长胖']
for(let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i])
}
}
test()
// 执行效率最高
const test = () => {
const arr = ['zhangsan', '20', '好好学习,天天长胖']
let len = arr.length
while(len--) {
console.log(arr[len])
}
}
test()
7、减少声明及语句
// 假设有一个DOM节点 <div id="box" style="width: 100px; height: 100px;"></div>
// 执行效率低
var box = document.getElementById('box')
const test = () => {
const w = box.offsetWidth
const h = box.offsetHeight
return w * h
}
test()
// 执行效率高
var box = document.getElementById('box')
const test = () => {
return box.offsetWidth * box.offsetHeight
}
test()
// 可能会有人认为这个和 缓存变量 优化执行效率冲突
// 具体情况要具体分析:
// 如果变量要经常使用,那么缓存可以提高执行效率
// 如果变量只会使用一次或者很少的几次,那么减少声明则会提升效率
// 执行效率低
const test = () => {
const name = 'zhangsan'
const age = 20
const slogan = '好好学习,天天长胖'
return name + age + slogan
}
// 执行效率高
const test = () => {
const name = 'zhangsan', age = 20, slogan = '好好学习,天天长胖'
return name + age + slogan
}
// 减少声明语句数可以提升执行效率
8、采用事件委托
- 如果通过for循环,给每个子节点绑定点击事件
- 当子节点数量特别庞大的时候,就特别耗性能
- 这个时候使用事件委托的话,就可以只给父元素绑定点击事件,提升执行效率
9.尊重对象的所有权
因为JavaScript可以在任何时候修改任意对象,这样就可以以不可预计的方式覆写默认的行为,所以如果你不负责维护某个对象,它的对象或者它的方法,那么你就不要对它进行修改,具体一点就是说:
- 不要为实例或原型添加属性
- 不要为实例或者原型添加方法
- 不要重定义已经存在的方法 不要重复定义其它团队成员已经实现的方法,永远不要修改不是由你所有的对象,你可以通过以下方式为对象创建新的功能:
- 创建包含所需功能的新对象,并用它与相关对象进行交互
- 创建自定义类型,继承需要进行修改的类型,然后可以为自定义类型添加额外功能
10.注意NodeList
最小化访问NodeList的次数可以极大的改进脚本的性能
var images = document.getElementsByTagName('img');for (var i = 0, len = images.length; i < len; i++) {}
编写JavaScript的时候一定要知道何时返回NodeList对象,这样可以最小化对它们的访问
- 进行了对getElementsByTagName()的调用
- 获取了元素的childNodes属性
- 获取了元素的attributes属性
- 访问了特殊的集合,如document.forms、document.images等等 要了解了当使用NodeList对象时,合理使用会极大的提升代码执行速度
11.避免与null进行比较
由于JavaScript是弱类型的,所以它不会做任何的自动类型检查,所以如果看到与null进行比较的代码,尝试使用以下技术替换:
- 如果值应为一个引用类型,使用instanceof操作符检查其构造函数
- 如果值应为一个基本类型,作用typeof检查其类型
- 如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上
12.通过 XHR 对象加载 JavaScript 脚本:
var xhr = new XMLHttpRequest();
xhr.open("get", "script1.js", true);
xhr.onreadystatechange = function(){if (xhr.readyState == 4){if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){varscript = document.createElement ("script");script.type = "text/javascript";script.text = xhr.responseText;document.body.appendChild(script);}}
};
xhr.send(null);
此代码向服务器发送一个获取 script1.js 文件的 GET 请求。onreadystatechange 事件处理函数检查readyState 是不是 4,然后检查 HTTP 状态码是不是有效(2XX 表示有效的回应,304 表示一个缓存响应)。如果收到了一个有效的响应,那么就创建一个新的script元素,将它的文本属性设置为从服务器接收到的 responseText 字符串。这样做实际上会创建一个带有内联代码的script元素。一旦新script元素被添加到文档,代码将被执行,并准备使用。
这种方法的主要优点是,您可以下载不立即执行的 JavaScript 代码。由于代码返回在script标签之外(换句话说不受script标签约束),它下载后不会自动执行,这使得您可以推迟执行,直到一切都准备好了。另一个优点是,同样的代码在所有现代浏览器中都不会引发异常。
此方法最主要的限制是:JavaScript 文件必须与页面放置在同一个域内,不能从 CDN 下载(CDN 指”内容投递网络(Content Delivery Network)”,所以大型网页通常不采用 XHR 脚本注入技术。
13.部署
- 用JSLint运行JavaScript验证器来确保没有语法错误或者是代码没有潜在的问题。
- 部署之前推荐使用压缩工具将JS文件压缩。
- 文件编码统一用UTF-8。 JavaScript 程序应该尽量放在 .js 的文件中,需要调用的时候在 HTML 中以 script src=”filename.js”的形式包含进来。JavaScript 代码若不是此 HTML 文件所专用的,则应尽量避免在 HTML 文件中直接编写 JavaScript 代码。因为这样会大大增加 HTML 文件的大小,无益于代码的压缩和缓存的使用。另外,script src=”filename.js”标签应尽量放在文件的后面,最好是放在/body标签前。这样会降低因加载 JavaScript 代码而影响页面中其它组件的加载时间。
总结:
永远不要忽略代码优化工作,重构是一项从项目开始到结束需要持续的工作,只有不断的优化代码才能让代码的执行效率越来越好。