Анимация веб-приложений на самом деле приводит к «убийству производительности».

JavaScript CSS
Анимация веб-приложений на самом деле приводит к «убийству производительности».

Автор этой статьи: Ян Йе

Исходное заявление: Эта статья подготовлена ​​членами команды интерфейса чтения YFE, пожалуйста, уважайте оригинальность, пожалуйста, свяжитесь с общедоступной учетной записью (id: yuewen_YFE) для авторизации и укажите автора, источник и ссылку.

задний план

в моем развитиироман-диалогВ ходе проекта мы обнаружили, что творческая деятельность очень помогла повысить данные о конверсиях. После исследования большинство групп пользователей этого разговорного нового продукта являются относительно молодыми после 90-х-95-х годов, поэтому окончательный вывод состоит в том, что мы надеемся использовать популярную в отрасли интерактивную форму молодого приложения - «Слайд-карту» для создания книги. рекламные мероприятия. После доработки я также надеюсь, что эту страницу можно будет объединить с самим продуктом в качестве страницы резидентной функции. Давайте посмотрим на окончательный эффект реализации:

图片

Это гладко? Далее я расскажу, что происходило в разработке согласно мышлению и процессу разработки в то время.

Ссылаться на

После того, как очень внимательный дизайнер представил проект дизайна, она также специально использовалаflinto⤵️ Сделал интерактивный прототип, чтобы помочь мне добиться нужного эффекта планирования.

图片

图片
"интерактивный проект флинто"

Увидев этот интимный интерактивный проект, первое, о чем я подумал, это обратиться к странице исследования в приложении Instant App и интерактивной форме программного обеспечения для знакомств «Tan Tan». Их интерактивные эффекты заключаются в следующем:

图片
«Избиение приложения»

图片
«Мгновенное приложение — открытие»

Эти два эффекта очень похожи? 😏 Но что отличается от моих потребностей на этот раз, так это то, что наша страница H5 встроена в приложение Qidian Reading, и оба вышеперечисленных эффекта являются результатом разработки собственного приложения, поэтому я не уверен в том, «может ли это быть сильно восстановлен» и «Как обеспечить хорошую производительность» все еще немного волнует 🤔.

пытаться

Идеи рефакторинга стиляПрежде чем получить реальные данные и разработать сложную логику, я сначала разобрал идеи реализации с эскизами:

图片
как показано на рисунке:

  • Исходное состояние состоит в том, что 3 карты сложены вместе, и должно быть трехмерное трехмерное ощущение.При перетаскивании последние две карты могут быть открыты.
  • При перетаскивании первой карты карта должна следовать направлению скольжения пальца.После того, как палец будет отпущен на определенное расстояние, карта вылетит, а следующие карты автоматически продвинутся на одну карту.Три карты всегда видны на страница.

В соответствии с приведенными выше идеями, поскольку существует потребность в 3D-стереоскопическом эффекте и эффекте движения, если для его достижения используется только z-индекс, он определенно не будет удовлетворен, поэтому я решил использовать translateZ, чтобы завершить эффект движения этого сложенная карта, потому что она может лучше отображать глубину резкости 3D-пространства. Таким образом, анимационные эффекты, такие как продвижение карты и автоматический вылет брошенной карты, могут быть полностью дополнены анимационными переходами CSS3.

Код стиля (основные структурные свойства)

 .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;
}

Сложенные карты должны иметь родительский контейнер, который дает всем сложенным картам трехмерную перспективу.

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,

Затем привяжите к стилю два метода инициализации. 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
 }

Последующее событие касания карты перетаскивания так же просто и легко, как запись и перетаскивание DIV до этого, а метод возврата к предыдущему и детали фонового перехода не будут показаны здесь слишком много кода.

До сих пор с использованием четырех фиктивных данных все шло гладко, а анимация была очень плавной:

图片

Сбой веб-просмотра приложения 😱

Затем я начал запрашивать реальные данные и сделал ряд оптимизаций, таких как:

  1. Все модели адаптируются к центру экрана карты.
  2. Записывайте пользовательские операции и сохраняйте направление перетаскивания в localStorage (первая карта, которую пользователь видит, когда снова открывает ее, остается той, когда он ушел, и опыт больше похож на приложение)
  3. Оптимизируйте и уменьшите запросы, загружайте 2 изображения при первом входе на страницу и загружайте следующее изображение каждый раз, когда карта улетает.

После оптимизации все так плавно выглядит в мобильном режиме PC Chrome.Я думал, что проблем не будет.Наконец, когда я выпустил его в тестовую среду и открыл, просканировав код, то увидел такую ​​сцену:

Мои опасения по поводу производительности в начале, наконец, оправдались. Приложение сразу рухнуло. Я попытался открыть его с помощью мобильного браузера, и оно не рухнуло, но работа не была гладкой. Я вернулся к ПК, чтобы испытать его снова. Я до сих пор не чувствую никаких заиканий. Я думаю, это может быть из-за того, что аппаратное обеспечение мобильного телефона не так хорошо, как у ПК. Причиной сбоя может быть проблема с 3D-рендерингом или производительностью. В соответствии с этой идеей я планирую сравнить данные, чтобы увидеть, какие ключевые элементы вызывают сбой.

Сравнение производительности

Сначала я использовал собственную производительность Chrome для записи страницы до 7 секунд, через 7 секунд я лихорадочно управлял картой, окончательный график производительности выглядит следующим образом:

图片

За исключением небольшой оговорки: хендлер не доказывает ничего серьезного. Я собираюсь снова отслеживать производительность рендеринга, я открыл панель рендеринга из дополнительных инструментов Chrome.

图片

После того, как все опции отмечены галочками, причина проблемы выявляется сразу!

图片

ОМГ 😱, фреймрейт всего 18 fps, и получается, что все карты накладываются и рендерятся. Сразу понял неправильный момент в разработке:Хотя прозрачность этих скрытых карт установлена ​​на 0, но невидимость не означает, что они не будут рендериться.Эти скрытые карты рендерятся в реальном времени после каждого вылета карты из анимации, что серьезно снижает производительность.

Другими словами, непрозрачность вызывала большое количество перекомпоновок страницы, и тут я вспомнил,И непрозрачность, и видимость вызовут перекомпоновку, а пока есть перекомпоновка, она обязательно вызовет перерисовку.,Толькоdisplay:noneLightning можно избежать, потому что он полностью отделен от документооборота.С момента разработки этого требования я занимался оптимизацией восстановления страницы и динамических эффектов, но забыл об этом важном моменте.

оптимизация

Зная ключ к проблеме, справиться с ней гораздо проще, и непрозрачность все же необходимо сохранить, потому что переход динамического эффекта требует прозрачности для украшения, и отображение станет очень жестким. Так как используется VUE, то сделать это проще.Сначала добавляем ко всем массивам в данных атрибут display, по умолчанию false, а потом уже привязываем элемент card.:class="{display:item.display}", а затем установите для всех стилей карточек css значение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>

Сделайте его истинным, когда его нужно отобразить, и стиль изменится на блочный.

.card.display {
  display: block;
  opacity: 1;
}

Например, у меня есть метод moveNext, который перемещает карту при касании End.

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. В 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 кадров в секунду.Независимо от того, как выполняется операция, всегда видны (рендерятся) только 3 карты, производительность значительно улучшена, и при возврате в приложение нет проблемы с вылетом.

Опыт сканирования кода (используйте приложение отправной точки, чтобы лучше видеть)

图片

Суммировать

После этого сбоя, вызванного веб-просмотром приложения, я получил некоторые впечатления и выводы, и я надеюсь, что это будет полезно для вас, кто читает эту статью 😊.

  1. При моделировании нативной анимации приложений в Интернете, особенно на мобильных устройствах, необходимо быть очень осторожным при использовании расширенных свойств для динамического изменения элементов в режиме реального времени.
  2. «Визуальное восприятие» не является точным и не может использоваться в качестве основы для измерения, все должно сравниваться с данными о производительности в инструментах разработки.
  3. Перекомпоновка и перерисовка на стороне ПК вряд ли вызовут проблемы с производительностью, если у них нет менталитета, знающего, что в горах есть тигры, и предпочитают писать код.Однако возможности рендеринга мобильного терминала намного хуже. чем на стороне ПК.Когда CSS запускает перекомпоновку и перерисовку, это становится «убийцей производительности» на мобильной стороне. Поэтому, прежде чем выполнять требования и динамические эффекты, также необходимо заранее сделать психологическое ожидание производительности собственного плана.При рассмотрении производительности страницы анализ перекомпоновки и перерисовки также является точкой входа.

Для получения дополнительной информации, пожалуйста, обратите внимание на публичный аккаунт фронтенд-команды China Literature Group: