Обзор митапа JS Party в новосибирске

Вчера я сходил на очередной местный митап, в Новосибирске, который в этот раз проводил Яндекс (это уже пятый Javascript Party в Нск от Яндекса, страничка митапа, на которой возможно появятся ссылки на видеозаписи и презентации: https://events.yandex.ru/events/meetings/28-02-2019/). Вообще, я люблю формат митапов, они бесплатны, не занимают целый день в отличии от конференций, можно спокойно сходить после работы, пообщаться с ребятами и конечно же узнать что-то новенькое. А Яндекс реально проводит эвенты на высоком уровне (и нет, я это говорю не потому, что я работаю тут).

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

Фотография с митапа

Анимация в реакте #

Первый доклад рассказывал Артур Шелашский из Яндекс.Маркета про работу с анимациями в React. Он рассказал все на примере продуктовой задачи (Выдача ачиков за отзывы), что довольно круто. Были бы все такие задачи, по которым можно делать доклады.

Css-анимация #

Для простой анимации появления контейнера, трансформации или его перемещения, можно воспользоваться обычными CSS-transition вместе со сменой состояния компонента → анимация в реакте из коробки:

.container {
  transition: transform 1s ease;
}
/* Используя стейт компонента */
<div className={isVisible ? 'is-visible' : 'is-hidden'} />

/* Используя прямую манипуляцию со стилями */
<div style={ transform: `translateX({ scale })` }} />

Чтобы добиться максимальной эффективности при анимации, лучше использовать свойства transform и opacity, которые не запускают в конвейере вывода пикселей на экран такие слои, как расчет макета (layout) и прорисовку (paint), а только компоновку (composite). Если вам эти слова ни о чем не говорят, то советую почитать небольшой материал "Performance rendering" от команды google и статью Пола Льюиса - "Pixels are expensive".

А максимальный профит будет, если вынести анимируемые объект в отдельный слой с помощью css-свойства will-change: transform, которое поможет браузеру понять, что к элементу будут применены изменения, и позволив выполнить до анимации необходимые операции (Подробнее можете почитать на getinstance в материале "Введение в свойство CSS will-change)".

React transition #

Но как быть, например, с модалками, когда мы хотим показать анимацию ухода, но при закрытии она моментально уходит из DOM? Необходимо перед закрытием делать задержку на какое-то время, показывать анимацию и потом "убивать" компонент.

На помощь приходит библиотека reactjs/react-transition-group, которая позволяет описывать переход из одного состояния компонента в другое с помощью простого декларативного API. Используется очень просто: оборачиваем компонент в HOC из библиотеки, и она возвращает одно из четырех состояний: enteringenteredexitingexited.

По умолчанию не изменяется поведение компонента, только отслеживается состояния «входа» и «выхода». А мы уже сами можем придать смысл этим состояниям. Например, мы можем добавить стили к компоненту, когда он "появляется" или "уходит", тем самым сделав такую анимацию (причем компонент после ухода "unmount-ится" из DOM):

FLIP принцип #

Но если мы хотим изменить высоту, ширину или другие свойства (кроме трансформации и непрозрачности), то анимация на css обычно получается не совсем такая, какую мы хотим получить - немного дерганная.

Когда свойство, которое вызывает изменения layout-а, изменяется (например, height), браузер рекурсивно проверяет, не изменился ли layout какого-нибудь другого элемента, и это может дорого стоить. Если это вычисление занимает больше одного кадра анимации (около 16,7 миллисекунд), то кадр анимации будет пропущен, что приведет к небольшому «рывку».

Как решение, использовать технику FLIP (First, Last, Invert, Play, которую описал в своем блоге уже знаковый нам Пол Льюис: "Flip your animations"). Она помогает делать анимации более "дешевыми" для браузера, тем самым позволяя добиваться плавных переходов (60 кадров в секунду). По ссылке выше принцип FLIP описан очень подробно, я расскажу только идею:

  • First: пока ничего не произошло, сохраняем позицию и размеры элемента (для этого можно использовать element.getBoundingClientRect), который будет изменяться.
  • Last: выполняем код, который вызывает мгновенный переход на конечное положение и сохраняет позицию и размеры элемента.
  • Invert: самый интересный шаг: сейчас у нас элемент находится в финальной позиции, а нам нужно его переместить в начальное положение. С помощью transform меняем позицию и размеры. Тут нужно выполнить небольшие расчеты, но они не будут сложными. Например, если элемент переместился на 90px вниз между First и Last, мы бы применили преобразование -90px по Y.
  • Play: инвертированный элемент (который притворяется, что находится в начальной позиции) мы можем переместить обратно в последнюю позицию, просто удалив преобразования.

Если вы ничего не поняли, то вот наглядная демка этих четырех шагов:

Эта техника идеально подходит, когда нам нужно реагировать на действия пользователя и потом что-то анимировать. Например, есть галерея изображений, и при клике на изображение мы увеличиваем его. Часто начальные и конечные положение элементов не известны, и сделав расчеты заранее, получится поддерживать анимацию c 60fps. Однако есть одно но, эти расчеты должны уложится в 100мс после действия пользователя, которые он не сможет заметить, и ему будет казаться, что сайт отреагировал мгновенно.

В коде это будет выглядеть примерно так:

/* First - получаем размеры и позицию в начале */
const first = el.getBoundingClientRect();

/* Перемещаем элемент в конец */
el.classList.add('totes-at-the-end');

/* Last - получаем размеры и позицию в конце */
const last = el.getBoundingClientRect();

/* Invert - инвертируем */
const invert = first.top - last.top;

/* Play - запускаем анимацию из инвертированного положения в финальное  */
const animation = el.animate([
  { transform: `translateY(${invert}px)` },
  { transform: 'translateY(0)' }
], {
  duration: 500,
  easing: 'cubic-bezier(0, 0, 0.32, 1)',
});

// После завершения анимации что-нибудь делаем
animation.addEventListener('finish', tidyUpAnimations);

Сейчас для анимации можно использовать полифил the Web Animations API. Еще одним приятным плюсом является то, что финальный элемент может не совпадать с начальным.

Контролирование рендеринга в React #

После того, как применили все техники, анимация все равно немного "дерганная". Скорее всего происходит лишний перерендеринг компонент, который можно избежать.

Для понимания, все ли правильно отрисовывается, очень помогает вкладка Rendering (пункт Paint flashing, подробнее можете почитать в документации google) в Chrome DevTools, которая показывает области, на которые распространяется прорисовка. Если установлен этот параметр, экран будет мигать зеленым, когда выполняется прорисовка в браузере. Если зеленым мигают области экрана, которые не должны были прорисовываться, то значит есть проблемы, которые нужно исправлять.

Например, на этой странице отрисовываются только те компоненты, которые должны быть обновлены:

Пример хорошей перерисовки компонентов

А вот на данной странице обновляется еще и статическая часть контента, причем можно заметить, что даже не один раз:

Пример с лишней перерисовкой статической части контента

Обновляется заголовок с описанием

Кроме использования данных с вкладки Rendering в Chrome DevTools, можно контролировать рендеринг с помощью библиотеки maicki/why-did-you-update, которая будет спамить в консоль, если react компонент отрендерился без изменения пропсов.

Пример работы библиотеки why-did-you-update

Что-бы контролировать обновление компонента, можно использовать PureComponent. Такие компоненты определяют lifecycle-метод shouldComponentUpdate, который в случае возврата false, не будет перерендеривать компонент. PureComponent будет вызывать рендер только в том случае, если обнаружит изменения в state или props компонента. Но стоит учесть, что происходит неглубокая проверка (так как глубокая проверка трудозатратная операция), так что сравнение не будет происходить по вложенным объектам и массивам.

Использовать PureComponent очень просто:

class MyComponent extends Component {...}

/* Заменить на */

class MyComponent extends PureComponent {...}

Подробнее почитать можно в официальной документации.

Хинт: еще одним важным моментом является то, что если вы считаете координаты объекта через JS, меняя state, то нужно отказываться от этого. Подробнее об этом и анимации в целом можете послушать в докладе Егора Банщикова c 6:37.

В общем на этом доклад и закончился, задача успешна выполнена, анимация радует пользователей. Могу так же порекомендовать отличный доклад с HolyJS по анимации в реакте:

Отпимизация изображений #

Второй доклад был про оптимизацию изображений Тимофей Чаптыков из Вконтакте. Вообще, я уже писал об оптимизации в рамках материала "image lazy-loading", Тим рассказывал, что 60% данных в интернете занимают изображения, и нет смысла неделями оптимизировать js бандл, когда одно неоптимизированное изображение будет весить больше javascript-а.

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

  • Фотографии - только jpg
  • Абстрактные картинки - png
  • Маленькие картинки и анимации - gif
  • А векторную графику - в svg

Так же был упомянут нынче модный сервис squoosh от команды Google Labs для оптимизации изображений путем применения разных алгоритмов сжатия. Приложение (PWA) работает офлайн, сразу показывает превью и позволяет делать резайз. На самом деле очень крутая вещь, всем рекомендую. Подробнее о нем можете послушать в обзоре Вадима Макеева.

Наложение эффектов на фото #


Знаете, как сделать фотку черно-белой на клиенте?

Для этого нужно загрузить изображение в canvas, и обработать каждый пиксель. Самый простой вариант использовать среднее значение красного, зеленого и синего: x = (red + green + blue) / 3. Но так же можно использовать нормализованное значение, заданное формулой x = 0.299 * red + 0.587 * green + 0.114 * blue.

В коде это будет выглядеть примерно так:

  const img = new Image();

  img.src = '...';
  img.onload = function() {
    draw(this);
  };

  function changeToGrayscale(canvas, context) {
    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    const { data } = imageData;

    for (let i = 0; i < data.length; i += 4) {
      /* Подсчитываем "серую" составляющую */
      const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;

      data[i] = gray;
      data[i+1] = gray;
      data[i+2] = gray;
    }

    context.putImageData(imageData, 0, 0);
  }

  function draw(img) {
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('2d');

    canvas.width = img.width;
    canvas.height = img.height;

    context.drawImage(img, 0, 0);

    changeToGrayscale(canvas, context);
  }

А вообще, можно просто применить css-фильтр filter: grayscale(100%), но спикер об этом не упомянул, не знал наверно :)

Вот результат двух способов:

Сравнение grayscale с оригинальным изображением

Слева оригинал, по середине css filter, справа canvas

Используя не сложные формулы, можно реализовать чуть ли не все эффекты, которые есть, например, в photoshop-е - Multiply, Screen, Darken, Lighten и так далее. Хотя, все это же можно реализовать и css-ом (Онлайн демо: https://www.cssfilters.co/).

Использование svg-масок #

У нас есть большая фотография (более оптимально использовать jpg). И появилась задача, что какая-то ее часть должна быть полупрозрачной. Если нужно добавить прозрачность, то придется использовать png (весить изображение будет намного больше). Но можно разделить изображения на два, прозрачную часть сделать в png, а фото - jpg. Фотку можно обрезать и использовать svg маску, которая будет указывать видимую часть изображения. В качество фона поставить полупрозрачную png. Этим способом можно добиться неплохих результат по уменьшению размера, но маску придется вырезать вручную, и это не так-то просто. Поэтому этот способ можно использовать только в крайних случаях.


Работа с AST #

Последний доклад был немного хардкорный и довольно интересный, Никита Сидоров рассказал про e2e тестирование, а именно как в selenium-подобных системах выбирать нужный элемент, и как ребята в Яндекс.Маркете с помощью парсинга AST (абстрактного синтаксического дерева) добавляли каждому компоненту уникальный "data-атрибут".

Никита уже раньше рассказывал этот доклад, вы можете его посмотреть сами:


Бонус: во время общения с ребятами после митапа услышал интересный паттерн, который явно заслуживает, что-бы просто знать, что такое есть :). Называется он "switch expression" (с альтернативным названием "помойка", которое дали ему те люди, которые его совсем не оценили) и помогает в более компактном виде реализовать обычный свитч. Выглядит он так:

{
  [true]: '#fff',
  [type === types.primary]: '#55f',
  [type === types.error]: '#f00',
  [type === types.disabled]: '#eee'
}.true

Что тут вообще происходит? Мы объявляем объект с вычисляемыми ключами и сразу же обращаемся к ключу true. Первый ключ "true" является значением по умолчанию. Дальше каждый ключ вычисляется и при значении true перезаписывает предыдущий.

const type = types.error;

{
  [true]: '#fff',
  [false]: '#55f',
  [true]: '#f00', /* Перезаписываем ключ по умолчанию */
  [false]: '#eee'
}.true

/* Результат будет равен #f00 */

Конечно создается дополнительный объект, но при использовании такой конструкции, например, в jsx-е, это никак не скажется на производительности. Единственная проблема, что за такой код в пул реквесте могут побить :)

В общем ходите на митапы, а если есть что рассказать, то не в качестве слушателя, а в качестве докладчика. Всем бобра

Бейджик с мероприятия

Оставайтесь на связи
Чтобы не пропустить новые посты или анонсы проектов, которые я делаю, вы можете присоединиться к телеграм каналу. А так же оставаться на связи, задавать вопросы или просто вместе обсуждать различные инженерные темы.
Присоединиться

Вам может быть интересно

Что нового в es2019: Array.prototype.flat, Object.fromEntries

08/02/2019
Недавно стандарт ES2019 был окончательно утвержден, включая несколько новых фич. Все фичи уже реализованы в Chrome 73
2

Разбор JavaScript квиза с CodeFest

29/06/2019
Разбираем 13 интересных вопросов c javascript квиза
37

История одной оптимизации React приложения

29/03/2020
Рассмотрим как оптимизировать React приложения на реальном примере
16