Web 仿 App 动画居然引出了“性能杀手”

本文做者:杨晔css

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取受权,并注明做者、出处和连接。前端

背景

在我参与开发的对话小说项目过程当中,咱们发现创意类的活动对拉升转化数据颇有帮助。通过调研,这款对话式小说产品的用户群体大多数都是比较年轻的 90-95 后,因此最后结论是但愿以目前业界年轻化 APP 流行的交互形式 —— 《滑卡片》对推书活动作一次改版,也同时但愿这个页面能和产品自己结合做为一个常驻功能页,咱们先来看一下最终的实现效果:web

图片

是否是挺流畅?接下来我会按照当时开发的思路和过程来说述开发中经历了什么。数组

参考

在极为用心的设计师交付设计稿后,她还特意使用 flinto ⤵️作了交互原型来辅助我达到策划预期的效果。浏览器

图片

图片
《flinto 交互稿》

见到这份贴心的交互稿后,我首先想到的就是先去参考即刻 App 中的探索页,以及交友软件《探探》的交互形式,他们的交互效果分别以下:bash

图片
《探探 App 》

图片
《即刻 App - 探索》

二者效果很是类似吧?😏但和我此次需求不一样的是:咱们的页面是内嵌在起点读书 App 内的 H5,而以上二者皆是由原生 App 开发实现的效果,因此我对“可否高度还原”以及”如何保证良好的性能”仍是产生了一点担心 🤔。ide

尝试

样式重构思路 在获取真实数据和开发复杂逻辑以前,我先用草图整理了一下实现思路:工具

图片
如图所示:

  • 初始状态为3张卡片叠在一块儿,要有 3D 立体感,在拖拽的时候能露出后面两张
  • 拖拽第一张时卡片须要跟随手指滑动方向,超过必定距离放开手指后卡片飞出,后面的卡片自动往前推动一张,页面中始终须要 3 张卡片可见状态。

根据以上思路,既然要有 3D 立体感和推动动效,若是单独使用 z-index 来实现确定不能知足,因此我选择使用 translateZ 来搭配完成这个堆叠卡片的推动效果,由于他能更好的显示出三维空间景深。如此一来,卡片往前推动和被扔出的卡片自动飞出等动效均可以彻底交给 CSS3 动画过渡来完成。post

样式代码(主要结构属性)性能

.card_container {
  position: relative;
  width: 6.86rem;
  height: 8.96rem;
  perspective: 1000px;
  perspective-origin: 50% 150%;
  -webkit-perspective: 1000px;
  -webkit-perspective-origin: 50% 150%;
}
.card {
  transform-style: preserve-3d;
  width: 100%;
  height: 100%;
  position: absolute;
  opacity: 0;
}
复制代码

堆叠的卡片须要有一个父容器,让全部堆叠的卡片产生 3D 透视效果。

HTML 和绑定方法

<div class="card_container">
  <div
    v-for="(item,index) in dataArr"
    :key="item.id"
    ondragstart="return false"
    class="card"
    :style="[cardTransform(index),indexTransform(index)]"
    @touchstart.stop.capture="touchStart($event,index)"
    @touchmove.stop.capture="touchMove($event)"
    @touchend.stop.capture="touchEnd($event,index)"
    @mousedown.stop.capture="touchStart($event)"
    @mousemove.stop.capture="touchMove($event)"
    @mouseup.stop.capture="touchEnd"
    @transitionend="onTransitionEnd(index)"
  >
</div>
复制代码

咱们还须要一些关键变量来记录一些可能实时变化的属性:

// 当前展现的图片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 😱

接着我开始请求真实数据,并作了一系列的优化,好比:

  1. 全机型适配卡片屏幕居中。
  2. 记录用户操做,拖拽扔出时的方向存入 localStorage (用户再次打开时看到的第一张卡片依然是以前离开时的,体验更像是在App内)
  3. 优化减小请求,首次进页面时加载 2 张图片,以后每飞走一张卡片时加载下一张图片。

优化以后,在 PC Chrome 移动端模式下一切看起来都是那么顺利,我自觉得不会有什么问题,最后发布到测试环境用 App 扫码打开后看到的倒是这一幕:

我一开始对性能的担心终于仍是发生了,App 内直接发生了崩溃,我再尝试用移动端浏览器打开,并无发生崩溃,可是操做起来很不流畅,再回到 PC 上体验了一次,依然感知不到有什么卡顿,我想多是因为手机硬件不如 PC, 发生崩溃的缘由多是 3D 渲染或者性能方面出现了问题。根据这个思路,我打算从数据上进行一次对比查看致使崩溃的关键要素是什么。

性能对比

首先使用 Chrome 自带的 Performance 进行了长达 7 秒的页面录制,在 7 秒钟我疯狂的对卡片操做了一番,最后得出的性能图以下:

图片

除了有一个小警告:Handler took 以外并证实不了什么严重的问题。 我打算再监控一下渲染性能,我从 Chrome 的更多工具里调起了 Rendering 面板

图片

在全部的选项所有打上勾后,形成问题的缘由一会儿就暴露了!

图片

OMG 😱,帧率只有 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

<div class="card_container">
  <div
    v-for="(item,index) in dataArr"
    :key="item.id"
    ondragstart="return false"
    class="card"
    :style="[cardTransform(index),indexTransform(index)]"
    @touchstart.stop.capture="touchStart($event,index)"
    @touchmove.stop.capture="touchMove($event)"
    @touchend.stop.capture="touchEnd($event,index)"
    @mousedown.stop.capture="touchStart($event)"
    @mousemove.stop.capture="touchMove($event)"
    @mouseup.stop.capture="touchEnd"
    @transitionend="onTransitionEnd(index)"
    :class="{display:item.display}"
  >
</div>
复制代码

在须要显示的时候让它变为 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 引发的崩溃事件,我从中吸收到了一些经验和总结,也但愿对阅读此文章的你有所帮助 😊。

  1. 用 Web 模拟 App 原生动画时,特别是在移动端,使用高阶属性去实时动态地改变元素时须要特别谨慎。
  2. “肉眼感知”并不许确,也不能做为衡量依据,一切要以开发工具中的性能数据为基准来证实。
  3. reflow 和 repaint 在 PC 端只要不是怀有明知山有虎,偏向虎山行的心态去写代码,几乎不会引起性能问题,可是移动端的渲染能力和 PC 端差了一大截,一个不当心,由 CSS 引起 reflow 和 repaint 就会成为移动端的“性能杀手”。因此,在完成需求和动效前,对本身的方案提早进行一次性能的心理预期也是颇有必要的,在考量页面性能的时候分析 reflow 和 repaint 也算是一个切入点。

查看更多分享,请关注阅文集团前端团队公众号:

相关文章
相关标签/搜索