一个老生常谈的问题,算是一项任重而道远的工程,总的来看优化范围很广,但我们可以从单个点逐步切入,养成自动代入优化思维才是目的,这里只做简单记录一下。
问题方案
切入主题,综合资料总的来看,一般直接造成页面卡顿的原因主要有 大批量dom操作、无限制监听函数、大量源站请求(懒加载、CDN、雪碧图、字体图标、浏览器缓存等)都可造成页面可视范围延迟卡顿,大部分问题网上早有解决方案,主要还是需要改写部分代码的习惯,简单聊下优化范围(部分参考资料来自互联网整合,来源均标注于文尾),详细优化方案可参考:让你的网页更丝滑(全)
- dom操作重排/绘(排版布局动画实现)
- 函数监听机制(函数执行次数限制)
- Promise / Web Worker、Time Slicing(延迟执行,队列任务,线程阻塞)
DOM操作
一般来说dom操作对页面卡顿影响虽不是最大但肯定是最常见的,习惯使用 jquery 的小伙伴肯定不陌生,包括原生 js 也是一样,错误的操作会导致页面重排造成非常大的性能消耗!所以首先要优化操作,网上已经有很多详细优化方案
页面渲染流程
DOM 操作会导致最重要的,也是我们最需要的问题就是导致用户阻塞的重构 (reflow) 和重绘 (repaint) 比较通俗的一句话就是你在页面上的任何操作都是有代价的,有些大有些小,如果我们的操作比较频繁或者波及范围较大,那么就要讲究方式和技巧 reflow 和 repaint 就是我们在改变页面或者说操作 DOM 时,会带来的两种后果 reflow 意味着结构的改变,比如一堆元素堆叠,改变其中一个的宽高,那么相应的所有元素的位置都要改变.repaint 意味着样式的改变比如 div 调整了背景色等,但是位置不变,只改变我们操作的元素。所以通常来看 repaint 的代价要远小于 reflow, 速度也更快
在 CSS 中可使用 transform 和 opacity 属性更改来实现动画,这两个属性更改不会触发重排与重绘,它们是可以由合成器(composite)单独处理的属性。
- tips1:CSS使用 translate替代position位移,使用 transform:translateZ(0) 或 will-change: ***; 来新建渲染图层,强制开启GPU加速(注意:不要创建过多的渲染层,这意味着新的内存分配和更复杂的层管理)
- tips2:JS使用 requestAnimationFrame(fn) 替代传统 setTimeout/setInterval 定时器动画
DOM渲染顺序(选择性渲染Layout及Paint)tips:gif图片会持续触发 Paint
能放到 DOM 操作之外的操作就放到外面,DOM 操作要尽量少
DOM 操作优化中这一观点在网上已经很普及了,很多例子都有比如遍历一个数组然后逐渐把内容添加到 DOM 上,这里就推荐先遍历完数组,然后一次性在 DOM 上操作。大家可以看代码:
代码语言:javascript复制// 不好
for (var i=0; i < items.length; i ){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " i);
list.appendChild(item);
}
// 好 使用容器存放临时变更, 最后再一次性更新DOM
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i ){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " i);
fragment.appendChild(item);
}
list.appendChild(fragment);
大范围操作先把容器隐藏,在其中操作完成后,再显示
这是一个我刚接触前端时遇到的一个优化办法,当时很不理解为什么 display=none 之后操作就算是性能优化了。但是数据证明如此渲染确实快了很多,这个的原理要涉及到浏览器加载和渲染的原理,简单说就是隐藏的元素其中不会产生 reflow. 这个例子我就不写了,很简单.
样式操作不要注意修改属性,直接替换 class
这个还是比较容易理解的,你逐一修改要访问很多次,而替换 class 就相当于批量操作了,访问一次 DOM 就可以了,当然性能提高了.
用变量保存 DOM 对象而不是多次获取,同时减少操作 DOM 属性的次数
代码语言:javascript复制//不好
function addAnchor(parentElement, anchorText, anchorClass) {
var element = document.createElement('a');
parentElement.appendChild(element);
// 下方额外多了两次渲染
element.innerHTML = anchorText;
element.className = anchorClass;
}
//好
function addAnchor(parentElement, anchorText, anchorClass) {
var element = document.createElement('a');
element.innerHTML = anchorText;
element.className = anchorClass;
parentElement.appendChild(element);
}
函数控制
主要涉及到优化有定时器动画、函数防抖节流、闭包、减少判断层级、减少循环体活动、事件绑定、事件队列等,在监听事件时控制函数触发间隔(如滚动对页面性能造成的影响,如可视区懒加载)控制,通过 Promise异步处理 大批量拥有前置条件(可能阻塞页面其他脚本执行序列)的函数操作。应用到实现不限于
- 可视区懒加载
- 异步请求回调
// 父元素绑定事件,自动向上遍历直到指定 CLASS 类的子元素生效(避免在循环中大量绑定子元素事件)
function bindEventClick(parent){
parent.onclick=(e)=>{
e = e || window.event;
let t = e.target || e.srcElement;
if(!t) return;
while(t!=parent){
if(t.id=="id"){
// target id..
break;
}else if(t.classList.contains("class")){
// target class..
break;
}else if(t.nodeName.toLowerCase()=="body"){
// target node..
break;
}else{
t = t.parentNode;
}
}
}
}
// 闭包节流器
// 使用被节流的匿名函数作为回调函数
// window.addEventListener("scroll", closure_throttle((e)=>{
// console.log(e);
// }, 1200));
function closure_throttle(callback=false, delay=200){
let closure_variable = true; //default running
return function(){
if(!closure_variable) return; //now running..
closure_variable = false; //stop running
setTimeout(()=>{
callback.apply(this, arguments);
closure_variable = true; //reset running
}, delay);
};
}
// 指定被节流函数,可用于移除事件处理器
const scrollFunc= closure_throttle((e)=>{
console.log(e);
}, 1200);
// 使用被节流的指定函数作为回调函数
window.addEventListener("scroll", scrollFunc);
// 移除事件(若 scrollFunc 为匿名函数则无法正常移除)
// window.removeEventListener('scroll', scrollFunc);
性能监测
Chromium 内核浏览器可通过谷歌 Lighthouse 评估页面测试。
通过F12控制台中的 Rendering
选项卡可检测页面FPS,LFC页面性能参数等。通过 Performance
选项卡可录制查看有红色标记的主线程丢帧情况(元素、原因、时间),具体可参考:浏览器Performance性能监控使用详解、