前端开发中导致页面卡顿的因素

之前在做实习的一个项目的时候碰到了一个让我抓狂的BUG,需求是让我实现一个实时刷新获取消息列表的功能,但是后台返回的接口数据是成百上千的,页面卡的不得了,笔记本的风扇都呼呼的,所以这篇博客是为了总结一下导致页面卡顿的原因。


一.页面卡顿的原因大体上可以分为两种类型

1.渲染不及时,页面掉帧

  • 长时间占用js线程
  • 页面回流和重绘较多
  • 资源加载堵塞

我觉得这就是我上次碰到的哪个问题的原因,每一次请求时间差不多有2s左右,js长时间占用线程,而且加载的数据很多,页面回流与重绘严重,上一次请求还没结束,这一次请求又发起了,也造成了资源加载堵塞,多种原因导致我的页面卡崩溃了。
2.网页内存占用过高,运行卡顿

内存泄漏导致内存过大

  • 意外的全局变量引起的内存泄漏
  • 闭包引起的内存泄漏
  • 被遗忘的定时器
  • 循环引用
  • DOM 删除时没有解绑事件
  • 没有清理的DOM元素引用

dom节点或事件占用内存过大

二.详细解读-渲染不及时的问题

1.长时间占用js线程

浏览器包括js线程和GUI线程,而且二者是互斥的,当长时间占用js线程时,会导致渲染不及时,出现页面卡顿

  • 1.1同步方式获取数据,会导致gui线程挂起,数据返回后再执行渲染
$.ajax({
   url: '',
   async: false
})
  • 1.2计算时间过长导致页面渲染不及时

很明显的例子就是我遇到的哪个BUG,2S的响应时间,不卡才怪。
渲染不及时的原因:
浏览器的渲染频率一般是60HZ,即要求1帧的时间为1s / 60 = 16.67ms,浏览器显示页面的时候,要处理js逻辑,还要做渲染,每个执行片段不能超过16.67ms。实际上,浏览器内核自身支撑体系运行也需要消耗一些时间,所以留给我们的时间差不多只有10ms。
常见的优化方式:

  • 使用requestIdleCallback和requestAnimationFrame,任务分片
function Task(){
    this.tasks = [];
}
//添加一个任务
Task.prototype.addTask = function(task){
    this.tasks.push(task);
};
//每次重绘前取一个task执行
Task.prototype.draw = function(){
    var that = this;
    window.requestAnimationFrame(function(){
        var tasks = that.tasks;        if(tasks.length){
            var task = tasks.shift();
            task();
        }
        window.requestAnimationFrame(function(){that.draw.call(that)});
    });
};

2.页面回流和重绘较多

  • 尽量减少layout
    获取scrollTop、clentWidth等维度属性时都会触发layout以获取实时的值,所以在for循环里面应该把这些值缓存一下
  • 简化DOM结构
    当DOM结构越复杂时,需要重绘的元素也就越多。所以dom应该保持简单,特别是那些要做动画的,或者要监听scroll/mousemove事件的。另外使用flex比使用float在重绘方面会有优势

3.资源加载阻塞

  • js资源放在body之前
  • 行内script阻塞
  • css加载会阻塞DOM树渲染(css并不会阻塞DOM树的解析)
  • 资源过大阻塞

三.详细解读-内存过大导致的页面卡顿

1.内存泄漏导致内存过大

浏览器有自己的一套垃圾回收机制,主流垃圾回收机制是标记清除,不过在ie中访问原生dom会采用引用计数方式机制,而如果闲置内存得不到及时回收,就会导致内存泄漏。

简单介绍下两种垃圾回收机制(GC Garbage Collection)

标记清除

定义和用法:
当变量进入环境时,将变量标记为进入环境,当变量离开环境时,标记为离开环境。某一时刻,垃圾回收器会过滤掉环境种的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。
到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
流程:

  • 浏览器在运行的时候会给存储再内存中的所有变量都加上标记
  • 去掉环境中的变量以及被环境中引用的变量的标记
  • 如果还有变量有标记,就会被视为准备删除的变量
  • 垃圾回收机制完成内存的清除工作,销毁那些带标记的变量,并回收他们所占用的内存空间

引用计数

定义和用法:引用计数是跟踪记录每个值被引用的次数。
基本原理:就是变量的引用次数,被引用一次则加1,当这个引用计数为0时,被视为准备回收的对象。
流程:

  • 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值引用次数就是1
  • 同一个值又被赋值另一个变量,这个引用类型的值引用次数加1
  • 当包含这个引用类型值得变量又被赋值另一个值了,那么这个引用类型的值的引用次数减1
  • 当引用次数变成0时, 说明这个值需要解除引用
  • 当垃圾回收机制下次运行时,它就会释放引用次数为0 的值所占用的内存

常见的造成内存泄漏的原因:

  • 意外的全局变量引起的内存泄漏
    解决:使用严格模式避免
    实例:
<button onclick="createNode()">添加节点</button>
  <button onclick="removeNode()">删除节点</button>
  <div id="wrapper"></div>
  <script>
  var text = [];
  function createNode() { 
      text.push(new Array(1000000).join('x'));  
      var textNode = document.createTextNode("新节点"),
          div = document.createElement('div');
      div.appendChild(textNode);
      document.getElementById("wrapper").appendChild(div);  
  }
  
  function removeNode() {
      var wrapper = document.getElementById("wrapper"),
          len = wrapper.childNodes.length;
      if (len > 0) {
          wrapper.removeChild(wrapper.childNodes[len - 1]);  
      }
  }
  </script>

text变量在createNode中引用,导致text不能被回收

  • 闭包引起的内存泄漏
    实例:
 <button onclick="replaceThing()">第二次点我就有泄漏</button>
  <script>
  var theThing = null;
  var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
          if (originalThing) {
              console.log("hi");
          };
      }
      theThing = {
          longStr: new Array(1000000).join('*'),
          someMethod: function someMethod() {
              console.log('someMessage');
          }
      };
  };
  </script>

上面那段代码泄漏的原因在于有两个闭包:unused和someMethod,二者共享父级作用域。
因为后面的 theThing 是全局变量,someMethod是全局变量的属性,它引用的闭包作用域(unused 和somMethod共享)不会释放,由于originalThing在共享的作用域中,造成originalThing不会释放,随着 replaceThing 不断调用,originalThing 指向前一次的 theThing,而新的theThing.someMethod又会引用originalThing ,从而形成一个闭包引用链,而 longStr是一个大字符串,得不到释放,从而造成内存泄漏。
解决方法:在 replaceThing 的最后添加 originalThing = null

  • 被遗忘的定时器
    实例:
var someResource = getData(); 
setInterval(function() { 
    var node = document.getElementById('Node'); 
    if(node) { 
        // 处理 node 和 someResource 
        node.innerHTML = JSON.stringify(someResource)); 
    } 
}, 1000);

计时器回调函数没被回收(计时器停止才会被回收)

  • 循环引用

定义:循环引用就是对象A中包含另一个指向对象B的指针,B中也包含一个指向A的引用。

因为IE中的BOM、DOM的实现使用了COM,而COM对象使用的垃圾收集机制是引用计数策略。所以会存在循环引用的问题

解决方法:手工断开js对象和DOM之间的链接。赋值为null。
实例:

function handle () {
    var element = document.getElementById(“testId”);
    element.onclick = function (){
        alert(element.id)
    }
}

element绑定的事件中引用了element上的属性

onclick事件是一个闭包,闭包可以维持函数内局部变量,使其得不到释放。也就是说element变量得不到释放,每调用一次element都会得不到释放,最终内存泄漏
解决问题:

function handle () {
    var element = document.getElementById(“testId”);
    element.onclick = function (){
        alert(element.id)
    }
    element = null
}
  • DOM删除是没有解绑事件
  • 没有清除DOM元素的引用

2.dom节点或事件占用内存过大

实例:

function addDom(){
        let d = document.createDocumentFragment();
       
        for(var i = 0;i<30;i++){
            let li = document.createElement('li')
            li.addEventListener('click', function(e) {
                let _this = e.target;
                let dom = e.target.tagName.toLowerCase();
                _this.style.color = 'red';
            })
        li.innerHTML = `</div>
            <h4>
                测试图片 
            </h4>
            <img style = "height:20px;width:100px" src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1591105123139&di=90a63b4962d0b4405ce65c2096a676c2&imgtype=0&src=http%3A%2F%2Fimg0.imgtn.bdimg.com%2Fit%2Fu%3D3769023270%2C3433174748%26fm%3D214%26gp%3D0.jpg"/>
            </div>`
            d.appendChild(li)
        }
        document.getElementById('app').appendChild(d)
    }

上面的代码是下拉加载,每次都会添加dom,最终导致内存过大
解决办法:采用虚拟列表和事件委托


总结:页面卡顿在实际开发过程中有很多场景,可以使用内存泄漏检测工具(sIEve,针对IE)进行检测,也可以使用chrome提供的timeline和profiles,或者performance,这里不再详细介绍。


查阅:
https://blog.csdn.net/ycf74514/article/details/51123263?locationNum=3&fps=1
https://blog.csdn.net/c11073138/article/details/84728132
https://www.cnblogs.com/yanglongbo/articles/9762359.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容