60 кадров в секунду с помощью pointer-events: none; при прокрутке

Пол Льюис (Paul Lewis) не так давно опубликовал интересную заметку о том, как избежать ненужных перерисовок, отключив эффекты при наведении во время прокрутки страницы пользователем — и это отличный подход. Обратной стороной этого решения становится необходимость управлять состоянием при наведении с помощью класса элемента-предка.

.hover .element:hover {
    box-shadow: 1px 1px 1px #000;
}

Этот подход приводит к недостаточной масштабируемости и добавляет излишнюю специфичность селекторам в CSS.

Суть этой техники в том, что при прокрутке класс .hover убирается с элемента body, и все селекторы, содержащие .hover, перестают работать до того момента, когда пользователь остановит прокрутку и класс будет снова добавлен к body.

Позже я увидел гениальный твит от Кристиана Шэфера (Christian Schaefer).

Пол Айриш (Paul Irish, @paul_irish): @tabatkins Это сложно… Распространенная техника переключения глобального класса приводит к серьезному пересчету стилей crbug.com/317007 В любом случае, не обращай на меня внимания :)
Кристиан Шэфер (@derSchepp): @paul_irish Все проще. Применяй "pointer-events: none" к <body> при scrollstart и удаляй его при scrollend @tabatkins

Всех спасет свойство pointer-events

Такой подход гораздо лучше, потому что позволяет элементу, у которого задано свойство pointer-events: none, просто не реагировать на наведение на него мышью. Посмотрите мой скринкаст, показывающий значительную разницу в скорости отрисовки при отключении эффектов при наведении.

Мы получаем все преимущества оригинального подхода без проблем с поддерживаемостью кода и специфичностью в CSS.

.disable-hover {
  pointer-events: none;
}

Все, что нам надо сделать — это добавить класс .disable-hover к body, когда пользователь начинает прокручивать страницу. Это позволит курсору «пролететь» над страницей, не вызывая у элементов реакции при наведении на них мышью.

var body = document.body,
    timer;

window.addEventListener('scroll', function() {
  clearTimeout(timer);
  if(!body.classList.contains('disable-hover')) {
    body.classList.add('disable-hover')
  }
  
  timer = setTimeout(function(){
    body.classList.remove('disable-hover')
  }, 500);
}, false);

Код достаточно прост — мы сбрасываем таймер (важно сделать это после первоначальной прокрутки), проверяем, не установлен ли уже класс для элемента body, а затем запускаем таймер для отложенного удаления класса спустя 500 мс после того, как пользватель остановил прокрутку.

Более основательная техника

Применение свойства pointer-events к body отлично сработает в большинстве случаев, если только для дочерних элементов не указано значение pointer-events: auto, перекрывающее родительское значение и вызывающее торможение при прокрутке.

Решить эту проблему можно использовав селектор * и добавив !important к значению свойства, чтобы отключить pointer-events для всех дочерних элементов.

.disable-hover,
.disable-hover * {
  pointer-events: none !important;
}

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