JavaScript 性能优化

85次阅读

共计 2110 个字符,预计需要花费 6 分钟才能阅读完成。

更多文章
加载与执行

将 <script> 标签放在 </body> 前面,不要放在 <head> 中,防止造成堵塞
尽量减少请求,单个 100KB 的文件比 4 个 25KB 的文件更快,也就是说减少页面中外链的文件会改善性能

数据存取

使用局部变量和字面量比使用数组和对象有更少的读写消耗
尽可能使用局部变量代替全局变量
如无必要,不要使用闭包;闭包引用着其他作用域的变量,会造成更大的内存开销
原型链不要过深、对象嵌套不要太多
对于多次访问的嵌套对象,应该用变量缓存起来

DOM 编程

不要频繁修改 DOM,因为修改 DOM 样式会导致重绘 (repaint) 和重排(reflow)
如果要修改 DOM 的多个样式可以用 cssText 一次性将要改的样式写入,或将样式写到 class 里,再修改 DOM 的 class 名称

const el = document.querySelector(‘.myDiv’)
el.style.borderLeft = ‘1px’
el.style.borderRight = ‘2px’
el.style.padding = ‘5px’
可以使用如下语句代替
const el = document.querySelector(‘.myDiv’)
el.style.cssText = ‘border-left: 1px; border-right: 2px; padding: 5px;’
cssText 会覆盖已存在的样式,如果不想覆盖已有样式,可以这样
el.style.cssText += ‘;border-left: 1px; border-right: 2px; padding: 5px;’

避免大量使用:hover

使用事件委托

<ul>
<li> 苹果 </li>
<li> 香蕉 </li>
<li> 凤梨 </li>
</ul>

// good
document.querySelector(‘ul’).onclick = (event) => {
let target = event.target
if (target.nodeName === ‘LI’) {
console.log(target.innerHTML)
}
}

// bad
document.querySelectorAll(‘li’).forEach((e) => {
e.onclick = function() {
console.log(this.innerHTML)
}
})
批量修改 DOM
当你需要批量修改 DOM 时,可以通过以下步骤减少重绘和重排次数:

使元素脱离文档流
对其应用多重改变
把元素带回文档中

该过程会触发两次重排——第一步和第三步。如果你忽略这两个步骤,那么在第二步所产生的任何修改都会触发一次重排。有三种方法可以使 DOM 脱离文档:

隐藏元素,应用修改,重新显示
使用文档片断(document.fragment)在当前 DOM 之外构建一个子树,再把它拷回文档
将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

算法和流程控制

改善性能最佳的方式是减少每次迭代的运算量和减少循环迭代次数
JavaScript 四种循环中 for while do-while for-in,只有 for-in 循环比其他其中明显要慢,因为 for-in 循环要搜索原型属性
限制循环中耗时操作的数量
基于函数的迭代 forEach 比一般的循环要慢,如果对运行速度要求很严格,不要使用

if-else switch,条件数量越大,越倾向于使用 switch

在判断条件多时,可以使用查找表来代替 if-else switch,速度更快

switch(value) {
case 0:
return result0
break
case 1:
return result1
break
case 2:
return result2
break
case 3:
return result3
break

}

// 可以使用查找表代替
const results = [result0, result1, result2, result3]
如果遇到栈溢出错误,可以使用迭代来代替递归
字符串
str += ‘one’ + ‘two’
此代码运行时,会经历四个步骤:

在内存中创建一个临时字符串
连接后的字符串 onetwo 被赋值给该临时字符串
临时字符串与 str 当前的值连接
结果赋值给 str

str += ‘one’
str += ‘two’
第二种方式比第一种方式要更快,因为它避免了临时字符串的产生
你也可以用一个语句就能达到同样的性能提升
str = str + ‘one’ + ‘two’
快速响应用户界面

对于执行时间过长的大段代码,可以使用 setTimeout 和 setInterval 来对代码进行分割,避免对页面造成堵塞
对于数据处理工作可以交由 Web Workers 来处理,因为 Web Workers 不占用浏览器 UI 线程的时间

编程实践
使用 Object/Array 字面量
const obj = new Object()
const newObj = {}

const arry = new Array()
const newArry = []
使用字面量会运行得更快,并且节省代码量
位操作在 JavaScript 中性能非常快,可以使用位运算来代替纯数学操作
x =* x
// 用位运算代替
x <<= 1
如无必要,不要重写原生方法,因为原生方法底层是用 C /C++ 实现的,速度更快
参考资料
高性能 JavaScript

正文完
 0

javascript性能优化

86次阅读

共计 4440 个字符,预计需要花费 12 分钟才能阅读完成。

本文主要是在我读《高性能 Javascript》之后,想要记录下一些有用的优化方案,并且就我本身的一些经验,来大家一起分享下,
Javascript 的加载与执行
大家都知道,浏览器在解析 DOM 树的时候,当解析到 script 标签的时候,会阻塞其他的所有任务,直到该 js 文件下载、解析执行完成后,才会继续往下执行。因此,这个时候浏览器就会被阻塞在这里,如果将 script 标签放在 head 里的话,那么在该 js 文件加载执行前,用户只能看到空白的页面,这样的用户体验肯定是特别烂。对此,常用的方法有以下:

将所有的 script 标签都放到 body 最底部,这样可以保证 js 文件是最后加载并执行的,可以先将页面展现给用户。但是,你首先得清楚,页面的首屏渲染是否依赖于你的部分 js 文件,如果是的话,则需要将这一部分 js 文件放到 head 上。
使用 defer, 比如下面这种写法。使用 defer 这种写法时,虽然浏览器解析到该标签的时候,也会下载对应的 js 文件,不过它并不会马上执行,而是会等到 DOM 解析完后(DomContentLoader 之前)才会执行这些 js 文件。因此,就不会阻塞到浏览器。

<script src=”test.js” type=”text/javascript” defer></script>
动态加载 js 文件, 通过这种方式,可以在页面加载完成后,再去加载所需要的代码,也可以通过这种方式实现 js 文件懒加载 / 按需加载,比如现在比较常见的,就是 webpack 结合 vue-router/react-router 实现按需加载,只有访问到具体路由的时候,才加载相应的代码。具体的方法如下:
1. 动态的插入 script 标签来加载脚本, 比如通过以下代码
function loadScript(url, callback) {
const script = document.createElement(‘script’);
script.type = ‘text/javascript’;
// 处理 IE
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState === ‘loaded’ || script.readyState === ‘complete’) {
script.onreadystatechange = null;
callback();
}
}
} else {
// 处理其他浏览器的情况
script.onload = function () {
callback();
}
}
script.src = url;
document.body.append(script);
}

// 动态加载 js
loadScript(‘file.js’, function () {
console.log(‘ 加载完成 ’);
})
2. 通过 xhr 方式加载 js 文件,不过通过这种方式的话,就可能会面临着跨域的问题。例子如下:
const xhr = new XMLHttpRequest();
xhr.open(‘get’, ‘file.js’);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
const script = document.createElement(‘script’);
script.type = ‘text/javascript’;
script.text = xhr.responseText;
document.body.append(script);
}
}
}
3. 将多个 js 文件合并为同一个,并且进行压缩。原因:目前浏览器大多已经支持并行下载 js 文件了,但是并发下载还是有一定的数量限制了(基于浏览器,一部分浏览器只能下载 4 个),并且,每一个 js 文件都需要建立一次额外的 http 连接,加载 4 个 25KB 的文件比起加载一个 100KB 的文件消耗的时间要大。因此,我们最好就是将多个 js 文件合并为同一个,并且进行代码压缩。
javascript 作用域
当一个函数执行的时候,会生成一个执行上下文,这个执行上下文定义了函数执行时的环境。当函数执行完毕后,这个执行上下文就会被销毁。因此,多次调用同一个函数会导致创建多个执行上下文。每隔执行上下文都有自己的作用域链。相信大家应该早就知道了作用域这个东西,对于一个函数而言,其第一个作用域就是它函数内部的变量。在函数执行过程中,每遇到一个变量,都会搜索函数的作用域链找到第一个匹配的变量,首先查找函数内部的变量,之后再沿着作用域链逐层寻找。因此,若我们要访问最外层的变量(全局变量),则相比直接访问内部的变量而言,会带来比较大的性能损耗。因此,我们可以将经常使用的全局变量引用储存在一个局部变量里。
const a = 5;
function outter () {
const a = 2;
function inner () {
const b = 2;
console.log(b); // 2
console.log(a); // 2
}
inner();
}
对象的读取
javascript 中,主要分为字面量、局部变量、数组元素和对象这四种。访问字面量和局部变量的速度最快,而访问数组元素和对象成员相对较慢。而访问对象成员的时候,就和作用域链一样,是在原型链 (prototype) 上进行查找。因此,若查找的成员在原型链位置太深,则访问速度越慢。因此,我们应该尽可能的减少对象成员的查找次数和嵌套深度。比如以下代码
// 进行两次对象成员查找
function hasEitherClass(element, className1, className2) {
return element.className === className1 || element.className === className2;
}
// 优化,如果该变量不会改变,则可以使用局部变量保存查找的内容
function hasEitherClass(element, className1, className2) {
const currentClassName = element.className;
return currentClassName === className1 || currentClassName === className2;
}
DOM 操作优化
最小化 DOM 的操作次数,尽可能的用 javascript 来处理,并且尽可能的使用局部变量储存 DOM 节点。比如以下的代码:
// 优化前,在每次循环的时候,都要获取 id 为 t 的节点,并且设置它的 innerHTML
function innerHTMLLoop () {
for (let count = 0; count < 15000; count++) {
document.getElementById(‘t’).innerHTML += ‘a’;
}
}
// 优化后,
function innerHTMLLoop () {
const tNode = document.getElemenById(‘t’);
const insertHtml = ”;
for (let count = 0; count < 15000; count++) {
insertHtml += ‘a’;
}
tNode.innerHtml += insertHtml;
}
尽可能的减少重排和重绘, 重排和重汇可能会代价非常昂贵,因此,为了减少重排重汇的发生次数,我们可以做以下的优化
1. 当我们要对 Dom 的样式进行修改的时候,我们应该尽可能的合并所有的修改并且一次处理,减少重排和重汇的次数。
// 优化前
const el = document.getElementById(‘test’);
el.style.borderLeft = ‘1px’;
el.style.borderRight = ‘2px’;
el.style.padding = ‘5px’;

// 优化后, 一次性修改样式,这样可以将三次重排减少到一次重排
const el = document.getElementById(‘test’);
el.style.cssText += ‘; border-left: 1px ;border-right: 2px; padding: 5px;’
2. 当我们要批量修改 DOM 节点的时候,我们可以将 DOM 节点隐藏掉,然后进行一系列的修改操作,之后再将其设置为可见,这样就可以最多只进行两次重排。具体的方法如下:
// 未优化前
const ele = document.getElementById(‘test’);
// 一系列 dom 修改操作

// 优化方案一,将要修改的节点设置为不显示,之后对它进行修改,修改完成后再显示该节点,从而只需要两次重排
const ele = document.getElementById(‘test’);
ele.style.display = ‘none’;
// 一系列 dom 修改操作
ele.style.display = ‘block’;

// 优化方案二,首先创建一个文档片段(documentFragment), 然后对该片段进行修改,之后将文档片段插入到文档中, 只有最后将文档片段插入文档的时候会引起重排,因此只会触发一次重排。。
const fragment = document.createDocumentFragment();
const ele = document.getElementById(‘test’);
// 一系列 dom 修改操作
ele.appendChild(fragment);
3. 使用事件委托:事件委托就是将目标节点的事件移到父节点来处理,由于浏览器冒泡的特点,当目标节点触发了该事件的时候,父节点也会触发该事件。因此,由父节点来负责监听和处理该事件。
那么,它的优点在哪里呢?假设你有一个列表,里面每一个列表项都需要绑定相同的事件,而这个列表可能会频繁的插入和删除。如果按照平常的方法,你只能给每一个列表项都绑定一个事件处理器,并且,每当插入新的列表项的时候,你也需要为新的列表项注册新的事件处理器。这样的话,如果列表项很大的话,就会导致有特别多的事件处理器,造成极大的性能问题。而通过事件委托,我们只需要在列表项的父节点监听这个事件,由它来统一处理就可以了。这样,对于新增的列表项也不需要做额外的处理。而且事件委托的用法其实也很简单:
function handleClick(target) {
// 点击列表项的处理事件
}
function delegate (e) {
// 判断目标对象是否为列表项
if (e.target.nodeName === ‘LI’) {
handleClick(e.target);
}
}
const parent = document.getElementById(‘parent’);
parent.addEventListener(‘click’, delegate);
本文地址在 -> 本人博客地址, 欢迎给个 start 或 follow

正文完
 0