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 也算是一個切入點。

查看更多分享,請關注閱文集團前端團隊公衆號:

相關文章
相關標籤/搜索