【第280期】奇趣百科性能优化
来自早读君:
这篇文章很给力,以实际的案例讲解工具的使用。这篇内容比较长,考验耐心的时候到了,看完你一定会回头点赞的~
正文从这开始~
奇趣百科年后进行了一次大改版, 不论是内容还是程序架构上, 改版使用 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人了~哈
长按图片识别图中二维码