什么是页面卡顿?如下:

当拖动页面或者滚动的时候页面一卡一卡的,看起来不连贯,我们就说页面卡了,这是一种非常不友好的体验,怎么衡量页面卡顿的情况呢?

1. 失帧和帧率FPS

如果你家里买了电视盒的话,在设置里面应该会有一个输出设置:

上面选中的60Hz就是帧率(frame per second),即一秒钟60帧,换句话说,一秒钟的动画是由60幅静态图片连在一起形成的。60fps是动画播放比较理想、比较基础的要求。当然如果你的显卡要是连这个都支持不了的话那就没办法了。windows系统有个刷新频率也是这个意思。

所以卡了,就是失帧了,或者掉帧了,1秒钟没有60个画面,看起来不连贯了。这可能是因为在渲染某些帧所花的时间比较长,导致停留在这些帧的时间较长,所以画面停顿了。

2. 渲染流程

60fps就要求1帧的时间为1s / 60 = 16.67ms。浏览器显示页面的时候,要处理js逻辑,还要做渲染,每个执行片段不能超过16.67ms。实际上,浏览器内核自身支撑体系运行也需要消耗一些时间,所以留给我们的时间差不多只有10ms。这10ms里面需要做一些什么事情?在Chrome的开发者文档Rendering Performance里面提到这个流程:

首先你用js做了些逻辑,还触发了样式变化,style把应用的样式规则计算好之后,把影响到的页面元素进行重新布局,叫做layout,再把它画到内存的一个画布里面,paint成了像素,最后把这个画布刷到屏幕上去,叫做composite,形成一帧。

这几项的任何一项如果执行时间太长了,就会导致渲染这一帧的时间太长,平均帧率就会掉。假设这一帧花了50ms,那么此时的帧率就为1s / 50ms = 20fps.

当然上面的过程并不一定每一步都会执行,例如:

  1. 你的js只是做一些运算,并没有增删DOM或改变CSS,那么后续几步就不会执行
  2. style只改了颜色等不需要重新layout的属性就不用执行layout这一步
  3. style改了transform属性,在blink和edge浏览器里面不需要layout和paint,如下面css trigger的说明:

发生掉帧的时候,我们可以使用的Chrome的devtools的timeline来观察这个过程。以最开始的例子做说明。

3. 掉帧分析

打开timeline的标签,勾上js profile和paint这两个选项,然后点击左边的记录按钮:

在页面拖动地图,出现卡顿的情况后,点击关闭记录按钮,就会生成这次操作的详细过程,先看最上面的overview图:

最上面一栏是帧率,顶点表示60fps,红色方格表示渲染时间比较长的帧,Chrome把这种情况叫做jank。可以看到上面有3个比较大的低谷,这并不是异常的失帧,这是Chrome检测到页面没有动了,idle空闲了,自动降低帧率。第二栏是CPU,黄色的为script,紫色的是CSS,蓝色是html,可以看到往往script占了比较高的CPU。关于timeline更详细的说明,可以查看chrome的文档。

我们注意到在6s和8s中间CPU占用有一个比较大的峰值,并且失帧得比较厉害:

选中这段区域,进行放大查看:

可以看到有好几帧都超过了16.67ms,其中有一帧甚至达到了81.8ms,所以难怪卡得那么厉害。我们重点看一下这一帧里面发生了什么。

这一帧的FPS只有1s / 81.8ms = 12fps,点击第二个tab展开:

其中js的处理用掉了46.8ms(js里面还要更新dom),排第二的rendering花掉了22.9ms,这个rendering包括上面说的css计算和layout:

最后的Painting,时间还是比较少的,只花了2.5ms:

所以最长的开销是js脚本,并且很可能js里面做了很多dom操作或者改了很多css,导致Rendering的时间也很长。

由于在开始记录之前勾选了js profile的选项,所以可观察这些js执行的具体开销,包括调用的函数栈及每个函数的执行时间:

最上面那个函数是XHR Ready State Change触发的,也就是说这一整段代码都是在一个ajax的success回调函数里面执行的。再往下可以看到回调函数里面调用的最耗时的两个函数:

其一的showMapResut就花费了22.65ms,它又调了removeOldHouses和addNewHouses,这两个各自的时间约为11ms。

而另一个showResult的时间更多:

快40ms,它下面的doShowResut和resizeContainer最为耗时。

所以我们找到4个最为耗时的函数。那接下来怎么办呢?

上面已经提到,每一帧留给我们的时间只有10ms。所以可以考虑把上面那4个函数拆了,分别在4个连续的帧里面执行。这样应该会改善很多。

4. 拆分代码段

我们把代码拆成一个个单元,每个单元就是一个task任务,每一帧执行之前就去取一个task执行。并且控制每个task的执行时间都在10ms以内。这样就可以解决问题。js在渲染每一帧之前会去调requestAnimationFrame(传一个函数的参数给它去执行)。所以用这一个api,并把task传给它。我们建立一个任务队列,为此封装一个Task类:

JavaScript
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)}); }); };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

functionTask(){
    this.tasks=[];
}
//添加一个任务
Task.prototype.addTask=function(task){
    this.tasks.push(task);
};
//每次重绘前取一个task执行
Task.prototype.draw=function(){
    varthat=this;
    window.requestAnimationFrame(function(){
        vartasks=that.tasks;
        if(tasks.length){
            vartask=tasks.shift();
            task();
        }
        window.requestAnimationFrame(function(){that.draw.call(that)});
    });
};

使用的时候先new一个Task,然后调draw函数初始化。有任务的时候调addTask插到队尾,执行任务的时候调shift取出队头元素。

上面的实现其实有一点问题,因为requestAnimationFrame是全局的,每次new一个Task,进行draw的时候,会把上一个传给它的task给覆盖掉。但是这个是可以从代码层面上解决的,这里不展开讨论。

然后再封装一个mapTask的单例,存放map页面的task:

JavaScript
var aTask = null; var mapTask = { get: function(){ if(!aTask){ aTask = new Task(); aTask.draw(); } return aTask; }, add: function(task){ mapTask.get().addTask(task); } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14

varaTask=null;
varmapTask={
    get:function(){
        if(!aTask){
            aTask=newTask();
            aTask.draw();
        }
        returnaTask;
    },
    add:function(task){
        mapTask.get().addTask(task);
    }
};

需要插入一个任务的时候就调一下mapTask.add,把上面4个十分耗时的函数分别当作一个任务插进去,下面是原本的执行逻辑:

JavaScript
updateHouses: function(houses){ var remainMultipleMarkers = null; var housesFilter = null; housesFilter = filterData.filterHouse(houses); remainMultipleMarkers = filterData.removeOldHouses(housesFilter.remainsHouses); housesFilter.newHouses = housesFilter.newHouses.concat(remainMultipleMarkers); filterData.addNewHouses(housesFilter.newHouses); },
1
2
3
4
5
6
7
8

updateHouses:function(houses){
    varremainMultipleMarkers=null;
    varhousesFilter=null;
        housesFilter=filterData.filterHouse(houses);
    remainMultipleMarkers=filterData.removeOldHouses(housesFilter.remainsHouses);
    housesFilter.newHouses=housesFilter.newHouses.concat(remainMultipleMarkers);
    filterData.addNewHouses(housesFilter.newHouses);
},

现在把它改成两个task,并加到任务队列里面:

JavaScript
mapTask.add(function(){ housesFilter = filterData.filterHouse(houses); remainMultipleMarkers = filterData.removeOldHouses(housesFilter.remainsHouses); }); mapTask.add(function(){ housesFilter.newHouses = housesFilter.newHouses.concat(remainMultipleMarkers); filterData.addNewHouses(housesFilter.newHouses); });
1
2
3
4
5
6
7
8

mapTask.add(function(){
    housesFilter=filterData.filterHouse(houses);
    remainMultipleMarkers=filterData.removeOldHouses(housesFilter.remainsHouses);
});
mapTask.add(function(){
    housesFilter.newHouses=housesFilter.newHouses.concat(remainMultipleMarkers);
    filterData.addNewHouses(housesFilter.newHouses);
});

同样地,把另外两个也这样改一下。

然后再拖动地图,查看效果,会发现页面瞬间爽滑了好多:

当把页面拖快的时候还是会有一点卡顿,但是比之前已经好很多。这里还有优化的空间,例如后面两个函数的执行时间还是比较长,可以把这两个函数再继续拆分task。

看一下timeline:

可以看到4个task分别在4帧执行,并且Task3还有很大的优化空间。

除了拆分代码段的方法外,还有其它一些地方要注意:

5. 其它的优化方法

(1)尽量减少layout

获取scrollTop、clentWidth等维度属性时都会触发layout以获取实时的值,所以在for循环里面应该把这些值缓存一下。以下代码:

JavaScript
for(var i = 0; i < childs.length){ childs.style.width = node.offsetWidth + "px"; }
1
2
3

for(vari=0;i<childs.length;i++){
   childs.style.width=node.offsetWidth+"px";
}

应该改成:

JavaScript
var width = node.offsetWidth; for(var i = 0; i < childs.length){ childs.style.width = width + "px"; }
1
2
3
4

varwidth=node.offsetWidth;
for(vari=0;i<childs.length ;i++){
   childs.style.width=width+"px";
}

当循环次数很多的时候,优化版的代码会明显提高性能。

获取一个元素的样式(getComputedStyle)时,也会触发layout

另外,能够使用transform满足要求的就别使用position/width/height做动画。

(2)简化DOM结构

当DOM结构越复杂时,需要重绘的元素也就越多。所以dom应该保持简单,特别是那些要做动画的,或者要监听scroll/mousemove事件的。另外使用flex比使用float在重绘方面会有优势,详见:《Avoid Large, Complex Layouts and Layout Thrashing》

参考:

  1. 淘宝首页性能优化实践
  2. 如何评价页面的性能

扩展阅读:

  1. Effective前端1:能使用html/css解决的问题就不要使用JS
  2. Effective前端2:优化html标签
  3. Effective前端3:用CSS画一个三角形
  4. Effective前端4:尽可能地使用伪元素
  5. Effective前端5:减少前端代码耦合

Effective前端6:避免页面卡顿相关推荐

  1. js初化加载页面时ajax会调用两次的原因_在前端开发中,有哪些因素会导致页面卡顿

    前端开发不像后端那样,很少出现有大量算法的场景,但是前端性能也是需要优化的.好的代码是保证网页平稳高性能运行的基础,结合以往开发中遇到的场景,本文对前端网页卡顿的原因进行了梳理和分析,并给出了对应的解 ...

  2. html中dom多会有影响吗,DOM操作造成的页面卡顿问题及解决

    相关目录 以下为我总结的其他性能优化点,欢迎参考,指点及吐槽 前言 界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现 的.尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很 ...

  3. 页面卡顿的原因及排查

    页面卡顿的原因.排查及解决方案 一.渲染不及时,页面掉帧 1>网络请求太多,请求返回的数据比较慢 接口返回慢的话,后端做些优化:前端适当做些缓存,减少不必要的重复的请求 可以从调试工具中的Net ...

  4. DOM操作造成的页面卡顿问题及解决

    前言 界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现 的.尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,所以前端性 ...

  5. position: relative;导致页面卡顿

    1.现象: vue单页面项目 只有在某个页面切换的时候出现页面卡顿现象 经过长时间排查 确定最终原因是 该模块外层div使用  position: relative 根本原因:待完善 转载于:http ...

  6. 解决el-select后台一次返回大数据量渲染慢导致页面卡顿的问题

    场景一 解决了一次性渲染大量数据问题 业务场景是后台一次返回10万多条数据需要在下拉框中展示,直接渲染会导致页面卡顿且需要很长时间等待,用户体验极差,所以我把这个改造了一下,直接上代码, 里面注释写的 ...

  7. 一文带你了解如何排查内存泄漏导致的页面卡顿现象

    作者 | 零一0101       责编 | 欧阳姝黎 不知道在座的各位有没有被问到过这样一个问题:如果页面卡顿,你觉得可能是什么原因造成的?有什么办法锁定原因并解决吗? 这是一个非常宽泛而又有深度的 ...

  8. el-table 大数据量渲染,页面卡顿的解决方案

    原文: el-table大数据量渲染卡顿的解决方案 描述: 当el-table的数据有成千上万条,且在同一页全部展示,此时页面渲染的dom太多可能造成页面卡顿 原因: 因为数据量过多导致浏览器渲染过多 ...

  9. 解决定时器导致页面卡顿、卡死的问题

    在项目中有时需要使用定时器,去不断刷新数据重载部分页面,长时间使用会导致线程占用和内存泄漏,导致页面卡顿.卡死的现象.网上查找过不少资料,无法根本解决此问题,下面介绍本人实践过的有效方法: var t ...

最新文章

  1. 2022-2028年中国量子点膜行业市场调查分析及未来前景分析报告
  2. 结构体指针struct stu *p;和结构体变量struct stu p;结构体为什么要用指针引用而不用变量引用
  3. EPOLL 事件之 EPOLLRDHUP
  4. Python 中的序列类型支持哪些公共操作
  5. 【转载】java读取.properties配置文件的几种方法
  6. 【caffe-matlab】目标检测R-FCN算法于Windows下配置
  7. 可持久化数据结构、可并堆
  8. nagios配置文件说明
  9. java可重复的集合_Java中是否存在任何无序,可重复的Collection类?
  10. linux 文件夹大小_技能“慧”|初识Linux(二)
  11. php替换局部大小写字母,php替换字符串中的一些字符(区分大小写)的函数str_replace()...
  12. 分布式系统唯一ID设计
  13. linux内核知识图谱
  14. springboot快速搭建图书管理系统
  15. 徐松亮硬件教学-微波天线设计-基于HFSS软件的天线设计流程
  16. selenium录屏python_Selenium实现录屏的一种方法
  17. BEA-090403 Authentication for user admin denied
  18. python(64位)安装超详细
  19. 舍不得卸载的5款宝藏APP,每款都是精品中的精品
  20. 三维空间中绘制点、线、面、UV贴图,万能的BufferGeometry(three.js实战4)

热门文章

  1. 如何将电脑中的视频进行剪辑?电脑视频剪辑工具哪个好
  2. Win32API:CreateDialog、DialogBox、DialogProc
  3. 杭州裸辞女孩打脸多少领导:对不起,90后不好骗
  4. php获取必应,php获取必应美图api
  5. 进程同步问题——生产者—消费者问题
  6. 诺顿ghost使用教程
  7. matlab中的带弯的x怎么输入,数学中的X怎么打出来呀,是那种弯的
  8. 通用oa系统_OA办公系统可能带来的风险
  9. Javascript中Window.open参数详解
  10. SpringBoot+Vue实现前后端分离OA办公管理系统