之前写过一篇文章前端网络高级篇(六)网站性能优化,里面提到过13个性能优化的点:
在具体编程方面,再补充几个点。
用JS操作DOM,是比较慢的。为什么呢?首先,补充一下浏览器相关的知识。下图为浏览器结构:
image
我们只要关心渲染引擎(Rendering engine)和JS引擎(JavaScript Interpreter)也可称为JS解析器。如图所示,当用JS引擎和渲染引擎是独立实现的,两者通过桥接接口通信。而DOM由渲染引擎绘制,所以,当JS改变DOM结构时,必须通过Bridge通知给渲染引擎,然后进行重排或者重绘。这个通信是有开销的。
重排:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。
重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。
重排的开销要远大于重绘
所以,我们的优化点就是:
看看下面的例子吧。
container
元素里面添加10000个“hello”。不好的行为(JS多次读取DOM元素):
for(var count=0;count<10000;count++){
document.getElementById('container').innerHTML+='<span>hello</span>';
}
改造(JS只读取一次DOM元素,但是,依旧多次更改DOM元素):
// 只获取一次container
let container = document.getElementById('container');
for(let count=0;count<10000;count++){
container.innerHTML += '<span>hello</span>';
}
再改造(JS只读取一次DOM元素,只操作DOM元素):
let container = document.getElementById('container');
let content = '';
for(let count=0;count<10000;count++){
// 拼接内容
content += '<span>hello</span>';
}
// 最后更改DOM
container.innerHTML = content;
其实,JS里面用+
号拼接String开销也略大,一般会建议创建数组,然后通过array.join('')
将数组转为String,如:
let array = [];
for(let count=0;count<10000;count++){
// 拼接内容
array.push('<span>hello</span>');
}
container.innerHTML = array.join('');
不过,DOM提供了更好的内置容器来帮助做内容拼接 - DOM Fragment。最后,用 DOM Fragment 改写总结版:
let container = document.getElementById('container');
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment();
for(let count=0;count<10000;count++){
// 通过DOM API创建span
let spanElt = document.createElement("span");
spanElt.innerHTML = 'hello';
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(spanElt);
}
// 最后更改DOM
container.appendChild(content)
不好的行为(逐条更改样式):
const container = document.getElementById('container')
container.style.width = '100px';
container.style.height = '200px';
container.style.color = 'red';
改造(利用class,只改动一次样式):
//style.css
.basic_style {
width: 100px;
height: 200px;
color: red;
}
//app.js
const container = document.getElementById('container');
container.classList.add('basic_style');
当DOM离线时(display: none),无论怎么操作,浏览器都不会绘制它,也就不会引发重排或者重绘。所以,利用这个特性再改造一版:
let container = document.getElementById('container');
container.style.display = 'none';
container.style.width = '100px';
container.style.height = '200px';
container.style.color = 'red';
container.style.display = 'block'
最后提醒一下,下面的属性慎用。因为这些属性都需要实时计算得到,所以,浏览器为了取得正确的值,会进行重排!
offsetTop、offsetLeft、 offsetWidth、offsetHeight、 scrollTop、scrollLeft、scrollWidth、scrollHeight、 clientTop、clientLeft、clientWidth、clientHeight
比如窗口的scroll和resize事件,一旦激活,会频繁触发相应的事件函数。频繁触发回掉函数导致的大量计算有可能引发页面抖动甚至卡顿。为了规避这些风险,我们会采用事件节流或者防抖,来降低函数的触发频率。
节流:当事件第一次被触发时,在指定时间内,无论再次触发多少次,都会被忽略。也就是说,以第一次事件为准。 防抖:事件触发后,会延迟执行,在延迟时间内,如果事件再次被触发,上一次的事件被取消,以当次为准,重新延迟执行。也就是说,以最后一次事件为准。
示例代码如下:
// 节流 1
let canRun = true;
$(window).scroll(() => {
if(!canRun){
// 判断是否已空闲,如果在执行中,则直接return
return;
}
canRun = false;
setTimeout(() => {
canRun = true;
}, 300);
});
// 节流 2
let interval = 300;
let last = 0;
$(window).scroll(() => {
let now = +new Date()
if (now - last >= interval) {
// 如果时间间隔大于设定的时间间隔阈值,则执行回调
last = now;
....
}
});
// 防抖
let timer;
$(window).scroll(() => {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(() => {
// 延时 200ms,处理滚动逻辑
}, 200)
})
一般在浏览器scroll和resize事件应用节流,在远程搜索场景下,应用防抖。
CSS选择器是从右向左解析的,所以,尽可能直接用class作为选择器,减少查询时间。
// 推荐
.top {...}
// 不推荐
// 浏览器会先查找所有的a标签,然后再找这些a标签中哪些有span父标签...
div span a {...}