查看原文
其他

【第280期】奇趣百科性能优化

xinran 前端早读课 2019-10-28

来自早读君:

这篇文章很给力,以实际的案例讲解工具的使用。这篇内容比较长,考验耐心的时候到了,看完你一定会回头点赞的~

正文从这开始~

奇趣百科年后进行了一次大改版, 不论是内容还是程序架构上, 改版使用 Vue.js 的MVVM 的理念令开发加速了不少. 但是改版后却出现了明显的性能问题, 出现了比较明显的页面卡顿, 因此我们专门做了一次性能优化...

1. 组件粒度加粗

首先最先想到的就是用Timeline看一下:


Frames情况还算正常, 但是注意到内存占用已经快17MB了, 相对于改版前的12MB是明显偏高的(由于篇幅关系就不上图了), 那么我们继续来追查内存相关的,使用Chrome开发者工具的Profiles看一下当前的内存占用情况:

打开Chrome开发者工具 -> 点击 Profiles 控制板 -> 选中 Take Heap Snapshot -> 点击 Take Snapshot


接下来我们来研究一下snapshot表格中相应的列分别代表什么.

一个对象有两种形式来持有内存:

1)直接拥有

2)间接引用

分别对应 snapshot 中的Shallow Size和Retained Size

Shallow Size

Shallow Size代表了对象直接持有的内存大小。一个标准的JS对象通常会持有用于描述自身逻辑和存储直接值(属性值)的内存。 通常情况下应该只有字符串和数组类型可能拥有一个较大的Shallow Size。

Retained Size

Retained Size代表了当前对象所引用的其他对象占用的内存大小. 当当前对象被销毁时, 这一部分的内存会被释放.

查看了一下, 发现listItemHead, listItemImg , listItemMeta 和 listTuwen 组件对象分别都各有9个或者10个(9个是因为业务逻辑问题).

Vue.js支持组件系统, 因此, 为了提高复用性, 我把一个卡片定义为一个组件,而这个组件又由若干个组件组成.


卡片本身是listTuwen组件, 该组件包括了三个组件: listItemHead, listItemImg 和 listItemMeta.

奇趣百科的首页触底加载一共可以加载30次一共300张卡片,我们加载30次完毕后与刚进入首页的情况进行对比:


内存占用已经飙升到了70MB了, 我们切换到 Comparison 视图(红框), 并选择 Snapshot1(红框)

#New 一列说明了三个组件的对象都增加了290个(289的是因为业务逻辑), Size Delta 一列说明了三个组件的对象各自增加了快7M的内存, 加起来就是20+MB了.

因此我们可以得出这样一个结论: 同一页面中大量被重用的组件尽量不要嵌套其他组件, 不然内存占用会随着组件的增多而快速上升.


可以看到一个Vue组件对象内部引用了大量的其他对象, 包括directives, watchers 等, 还有一些系列的 getter 和setter 方法.

解决办法就是卡片内部不使用组件, 一个卡片就只有自身这个组件, 采用其他方法来提高代码的复用性.


最后我们来对比优化后的结果:


触底加载完毕, 300张卡片占用内存40M左右, 虽然listTuwen组件的对象占用的内用大了很多,但是总体下降了40%,优化效果很明显.

最后, Vue 的作者 @尤小右 在这个微博下给出了答复:


关于这个编译函数产生的垃圾后来我又用堆快照粗略找了一下, 没有找到... 也没仔细去看过 Vue 的源码,有兴趣的同学可以去再仔细研究研究...

移除视窗外的不可见的DOM

页面上DOM的数目越少,占用内存就越少,性能也就越好. 这是很容易得出来的结论.


参照手机淘宝搜索结果页的做法, 我们可以把视窗外的不可见的卡片移除掉, 当这些卡片滚动回到窗口内(或者滚动位置接近到窗口的某个像素值)后再插入显示.列表的卡片数目保持在一个恒定的值,而不是直线增长下去.


奇趣百科线上的代码可以看到, 我们的卡片数目保持在30个, 也就是 DOM 的数据是恒定的,不会随着页面越滚动到下面越多.

接下来我们看一下内存情况验证我们这样做的效果:

优化前后的 Timeline 工具的内存曲线:


内存曲线都成锯齿形状, 有触发垃圾回收, 但是优化后的曲线上升的斜率比优化前少了2°, 即内存上升速度慢了.

另外是优化前后的 Profiles 对比('DOM'作为关键字进行筛选):


内存优化后少了近10M,并且DOM的数据减少了10倍.内存使用下降25%.

图片懒加载

进行这一个优化点之前, 我们先来科普一下Timeline这个控制板.

最好在浏览器隐身模式下使用, 禁用一切无关插件,因为插件也会占用内存, 影响测试结果

如果需要记录网络请求的话, 最好把浏览器缓存也禁用掉

网上盗的一张图(出处):


有三种模式可以切换关注点:

1)Events: 显示所有事件的记录

2)Frames: 显示页面渲染的帧数

3)Memory: 显示页面的内存情况

这里我们重点关注 Frames 模式.

页面的每一帧内容都是GPU绘制出来的,它的最高绘制频率受限于显示器的刷新频率,大多数情况下最高的绘制平率只能是每秒60帧(frames per second, 即fps),对应于显示器的60Hz.因此在页面性能的测试中, 60fps是一个非常重要的指标,越接近越好.

这里说到了一个常量 -- 屏幕刷新频率60Hz.

60Hz和60fps有什么关系?没有任何关系。fps代表GPU渲染画面的频率,Hz代表显示器刷新屏幕的频率。一幅静态图片,你可以说这副图片的fps是0帧/秒,但绝对不能说此时屏幕的刷新率是0Hz,也就是说刷新率不随图像内容的变化而变化。游戏也好浏览器也好,我们谈到掉帧,是指GPU渲染画面频率降低。比如跌落到30fps甚至20fps,但因为视觉暂留原理,我们看到的画面仍然是运动和连贯的。

Frames 模式模式中的 Frames 就是"帧". "一帧"(Frames模式下的一条柱子)代表了显示器为了在一帧()内展现内容所要完成的工作,包括执行JavaScript,处理事件,更新DOM,改变样式和布局还有绘制页面.


在Frame视图中有两条贯穿该视图的横线,分别标识出60FPS和30FPS的基准.


注意到有些柱子有一部分是空白的或者是灰色的,分别代表:

1)空白: 空闲时间

2)灰色: 没有被记录的活动,可以理解成是浏览器内部c++的一些工作,这部分和前端的js以及渲染没什么关系.

现在来看一下奇趣百科的 Frames 情况:


看到超出 60fps 的柱子还是挺多的, 而且都是柱子的大部分颜色都是绿色的.

先来说明一下柱子颜色的含义:


蓝色: 网络和HTML解析

黄色: JavaScript 脚本运行

紫色: 样式重计算和布局 ( Layout , Recaculate Style, Update Layer tree)

绿色: 绘制和合成 ( Paint , Composite Layers)

所以我们大部分时间话费在绘制上了. 我们再选取一些比较高的柱子, 看看都有什么特点:


我们看到有一些空的绿色色块和实心的绿色色块, 是这样的:

1)绘制分两步走: 画和渲染

画: 这包括了一些系列你想要画出来的东西, 而这些是由元素上的CSS而得来的.

渲染: 逐条分析上一步中你想要"画"的东西, 利用 GPU 来组合填充这些东西的实际像素.

这一部分我翻译得很烂,因为我自己也不太懂具体的意思,所以大家可以看看原文 → About thegreen bars

而 Painting 包括了这些事件:


根据我的观察, 我发现比较高的绿色柱子一般都包括多个Image Decode事件, 图片加载回来而触发了这个事件, 进而产生了大量的Rasterize Paint事件, 所以我猜测, 把图片都分开加载, 不要一次性就加载10张图片, 这样就得把事件分散, 高的柱子拆成多个矮的柱子, 这样就能进一步提升流畅度.

图片懒加载是怎么实现的就不细说了, 类似的效果可以参照 淘宝首页

最后我们来看一下优化后的 Timeline :


这个情况已经是达到比较理想的状态了, 实际操作也比较流畅.

但是值得一提的是, 最后 PM 并没有采纳这一步的优化方式, 因为体验过后, PM 认为图片懒加载反而会让用户觉得卡顿, 并不是实际滑动上的卡顿, 而是整体体验上的卡. 所以最后从用户体验的角度出发, 我们的优化方案并没有采用图片懒加载.

一些其他的优化方法

还有很多其他的性能优化手段, 这里就稍微说一下, 就不展开了.

避免使用 border-radius,box-shadow, 渐变等性能杀手

动画尽量使用 opacity 和 transform 完成

来自早读君:

恭喜你,现在有这么耐心的人不多了,已经打败xxxx人了~哈


长按图片识别图中二维码


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存