我们还需要一些关键变量来记录一些可能实时变化的属性:
// 当前展示的图片index
currentIndex: 0,
// 记录偏移量
displacement: {
x: 0,
y: 0
},
// 位置信息
position: {
start: { x: 0, y: 0 },
end: { x: 0, y: 0 },
direction: 1, // 滑动方向,左是-1,右是1
swipping: false // 是否在拖动交换过程中
},
// 记录每一个丢出去的方向
directionArr: [],
// 显示图片的堆叠数量
visible: 3,
// 视口宽度
winWidth: 0,
// 滑动阈值
slideWidth: 70,
// 超过阈值时的自动偏移量
offsetWidth: 120,
再给 style 绑上 2 个初始化的方法。 cardTransform 用来初始化每张卡片的样式,indexTransform 用来初始化第一张卡片的样式。
// 初始化每张卡片的样式
cardTransform (index) {
let style = {}
//卡片自动位移距离(飞出屏幕多远)
let offset = 0
if (this.directionArr[index] === 1) {
offset = 800
} else if (this.directionArr[index] === -1) {
offset = -800
}
style['z-index'] = this.currentIndex - index + this.visible
style['transform'] = `translate3d(0,0,${(this.currentIndex - index) * 60}px)`
//让藏在后面的卡片缩小样式堆叠在一起并透明不显示。一旦飞走一张,下一张卡片会自动过渡动画往前推进
if (index - this.currentIndex <0) {
style['opacity'] = 0
style['transform'] = `translate3d(${this.position.end.x + offset}px,${this.position.end.y}px,${(this.currentIndex - index) * 60}px) rotate(${this.position.direction * -65}deg)`
}
// 非手势滑动状态才添加过渡动画
if (!this.position.swipping) {
style['transitionTimingFunction'] = 'ease'
style['transitionDuration'] = 300 + 'ms'
}
return style
},
// 第一张卡片的样式
indexTransform (index) {
let style = {}
if (index === this.currentIndex) {
style['transform'] = `translate3d(${this.displacement.x}px,${this.displacement.y}px,${(this.currentIndex - index) * 60}px) rotate(${this.displacement.x / this.winWidth * -65}deg)`
}
// 非手势滑动状态才添加过渡动画
if (!this.position.swipping) {
style['transitionTimingFunction'] = 'ease'
style['transitionDuration'] = 300 + 'ms'
}
return style
},
之后的拖拽卡片 touch 事件就相当于以前写拖拽 DIV 那样简单容易,返回上一张和背景过渡等细节的方法这里就不再做过多的代码展示了。
到此为止,使用了四本数的 mock 数据,一切都很顺利,动画也非常流畅:
App Webview crash :scream:
接着我开始请求真实数据,并做了一系列的优化,比如:
全机型适配卡片屏幕居中。
记录用户操作,拖拽扔出时的方向存入 localStorage (用户再次打开时看到的第一张卡片依然是之前离开时的,体验更像是在App内)
优化减少请求,首次进页面时加载 2 张图片,之后每飞走一张卡片时加载下一张图片。
优化之后,在 PC Chrome 移动端模式下一切看起来都是那么顺利,我自以为不会有什么问题,最后发布到测试环境用 App 扫码打开后看到的却是这一幕:
我一开始对性能的担忧终于还是发生了,App 内直接发生了崩溃,我再尝试用移动端浏览器打开,并没有发生崩溃,但是操作起来很不流畅,再回到 PC 上体验了一次,依然感知不到有什么卡顿,我想可能是由于手机硬件不如 PC, 发生崩溃的原因可能是 3D 渲染或者性能方面出现了问题。根据这个思路,我打算从数据上进行一次对比查看导致崩溃的关键要素是什么。
性能对比
首先使用 Chrome 自带的 Performance 进行了长达 7 秒的页面录制,在 7 秒钟我疯狂的对卡片操作了一番,最后得出的性能图如下:
除了有一个小警告:Handler took 之外并证明不了什么严重的问题。 我打算再监控一下渲染性能,我从 Chrome 的更多 工具 里调起了 Rendering 面板
在所有的选项全部打上勾后,造成问题的原因一下子就暴露了!
OMG :scream:,帧率只有 18 fps,而且原来所有的卡片都重合在了一起并进行了渲染。我马上意识到开发中的错误点: 那些隐藏的卡片虽然 把透明度设置为了 0,但看不见并不代表不会被渲染,那些被隐藏的卡片在每一次卡片飞出动画后都在实时被渲染推进动画,严重损耗了性能。
也就是说,opacity 造成了页面的大量 reflow,这时我才想起, opacity 和 visibility 都会造成回流,而只要有 reflow 必定会造成 repaint ,只有 display:none 可以避雷,因为它彻底脱离了文档流,在开发这个需求以来,我一直在优化页面还原度和动效,却忘记了这重要的一点。
优化
知道了问题的关键就好办多了,opacity 依然要保留,因为推进动效的过渡需要透明度来美化,光用 display 会变得非常生硬。既然用的是 VUE,那就更好办了,首先给数据中的数组全部添加上 display 属性,默认为 false,然后给 card 元素绑上了 :class="{display:item.display}",再将 css 的 card 样式全部设置为 display:none
在需要显示的时候让它变为 true,随即样式变为 block 。
.card.display {
display: block;
opacity: 1;
}
举个例子,比如我在 touchEnd 时有一个卡片移动的方法 moveNext。
touchEnd () {
this.position.swipping = false
this.position.end['x'] = this.displacement.x
this.position.end['y'] = this.displacement.y
// 判断滑动距离超过设定值时,自动飞出
if (this.displacement.x > this.slideWidth) {
this.moveNext(1) //往右
} else (this.displacement.x <-this.slideWidth) {
this.moveNext(-1) //往左
}
this.$nextTick(() => {
this.displacement.x = 0
this.displacement.y = 0
this.isDrag = false
})
}
我们就可以在 moveNext 时对 index 进行操作。moveNext 中需要对当前显示的第一张卡片和后面堆叠的都添加显示,已经消失的卡片变为隐藏,如此循环无缝衔接。另外,由于数据是不确定的,为避免某些极端情况(例如 首张卡片 再往前或者 最后倒数几张 后都会出现没有更多卡片的情况,所以还需要做细节容错处理)。
moveNext (direction) {
this.position.direction = direction
// 防止在最后倒数几张时操时出错
try {
this.dataArr[this.currentIndex + 3].display = true
} catch (e) {
}
// 防止在第一张时操作出错
if (this.currentIndex > 0) {
try {
this.dataArr[this.currentIndex - 1].display = false
} catch (e) {
}
}
this.currentIndex++ //每次让下一张卡片往前推进,反之 -- 就是返回上一张
!direction ? this.position.end['x'] -= this.offsetWidth : this.position.end['x'] += this.offsetWidth
this.position.end['y'] += this.offsetWidth / 2
},
在一番调整优化后,我重新调起了 Rendering 面板查看结果:
和预想的一样,帧数达到正常的 60 fps,不管如何操作,始终只有 3 张卡片是可见(被渲染的),性能得到了大大提升,重新回到 App 中访问也没有再遇到崩溃的问题。
扫码体验(使用起点 APP 查看效果更佳)
总结
经过这次 App webview 引起的崩溃事件,我从中吸取到了一些经验和总结,也希望对阅读此文章的你有所帮助 :blush:。
用 Web 模拟 App 原生动画时,特别是在移动端,使用高阶属性去实时动态地改变元素时需要特别谨慎。
“肉眼感知”并不准确,也不能作为衡量依据,一切要以开发工具中的性能数据为基准来证明。
reflow 和 repaint 在 PC 端只要不是怀有明知山有虎,偏向虎山行的心态去写代码,几乎不会引发性能问题,但是移动端的渲染能力和 PC 端差了一大截,一个不小心,由 CSS 引发 reflow 和 repaint 就会成为移动端的“性能杀手”。所以,在完成需求和动效前,对自己的方案提前进行一次性能的心理预期也是很有必要的,在考量页面性能的时候分析 reflow 和 repaint 也算是一个切入点。