Автор — Нолан Лоусон, менеджер проекта Microsoft Edge

Прокрутка — одно из самых древних взаимодействий в вебе. Задолго до появления методов pull-to-refresh и списков бесконечной загрузки скромная полоса прокрутки решила изначальную проблему масштабирования в вебе: как взаимодействовать с контентом, который распространяется за пределы доступной области просмотра?

Сегодня прокрутка всё ещё остаётся самым фундаментальным взаимодействием в Сети, и, возможно, самым неправильно понятым. Например, вы знаете разницу между следующими сценариями?

  • Пользователь прокручивает страницу двумя пальцами на тачпаде
  • Пользователь прокручивает одним пальцем на тачскрине
  • Пользователь прокручивает колесо мыши
  • Пользователь щёлкает по полосе прокрутки и тянет её вниз и вверх
  • Пользователь нажимает стрелки «вверх», «вниз», PageUp, PageDown и «пробел» на клавиатуре

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

Как выясняется, эти пять методов ввода имеют очень разные характеристики, особенно в части производительности и кроссбраузерной совместимости. Некоторые из них (как прокрутка по тачскрину) вероятно будут плавными даже на странице с использованием тяжёлых JavaScript, в то время как от других (как прокрутка с клавиатуры) та же самая страница будет лагать и станет невосприимчивой. Более того, некоторые виды прокрутки можно замедлить обработчиками событий DOM, а другие нельзя. Что здесь происходит?

Чтобы ответить на этот вопрос и понять, как реализовать наиболее плавную прокрутку для своего сайта, отступим на шаг понять и разберёмся, как браузеры разбираются с многопоточностью и вводом.

Многопоточный веб


Концептуально, веб является однопоточной средой. JavaScript блокирует DOM, а DOM блокирует JavaScript, потому что оба борются за один и тот же поток, часто называемый «основным потоком» или «потоком UI».

Например, если вы добавите этот (ужасный) сниппет JavaScript на страницу, то немедленно заметите ухудшение в работе:

setInterval(() => {
  var start = Date.now();
  while (Date.now() - start < 500) {/* wheeeee! */}
}, 1000);

Пока этот JavaScript крутится в бесконечном цикле, кнопки не работают, элементы форм не реагируют и даже анимированные GIF'ки тормозят — во всех смыслах и отношениях страница зависла. Можете изучить эффект в действии в простом демо.



Более того, если вы попытаетесь прокрутить страницу клавишами «вверх» и «вниз» на клавиатуре, страница предсказуемо застрянет, пока JavaScript не прекратит выполнение. Всё это явные свидетельства нашего представления веба как однопоточной среды.

Есть забавная аномалия: если попробовать прокрутку через тачскрин, то страница отлично прокручивается вверх и вниз, хотя JavaScript и блокирует всё остальное на странице. То же самое относится к прокрутке с тачпада, колесом мыши и прокрутке после захвата страницы курсором click-and-drag (в зависимости от браузера).

Каким-то образом некоторые действия по прокрутке могут изменять состояние страницы, в то время как всё остальное — кнопки, поля ввода данных, GIF'ы — полностью зависло. Как мы можем совместить это с нашей теорией однопоточного веба?



История двух потоков выполнения


Как выясняется, в целом тезис «браузеры однопоточные» правдив, но есть важные исключения. Прокрутка, во всём своём многообразии, является одним из таких исключений.

С годами разработчики браузеров осознали, что выгрузка вспомогательной работы в фоновые потоки может дать значительную выгоду по плавности работы и чувствительности. Прокрутка настолько важна для ключевого опыта работы с браузером, что эту задачу быстро выбрали для такой оптимизации. В наше время все основные браузерные движки (Blink, EdgeHTML, Gecko, WebKit) поддерживают прокрутку за пределами основного потока выполнения в той или иной степени (Firefox последним присоединился к клубу, с версии Firefox 46).

С фоновой прокруткой даже загромождённая страница будет плавно прокручиваться, потому что вся прокрутка выполняется в отдельном потоке. Только если вы попытаетесь взаимодействовать со страницей через некий посторонний механизм, не связанный с прокруткой — нажать клавишу, ввести данные в поле ввода, нажать на ссылку — тогда фасад сбрасывается и суть салонного трюка полностью раскрывает себя. (Учитывая, насколько хорошо он работает, это отличный трюк!)

Правда, у асинхронной прокрутки есть распространённый побочный эффект, который называют эффектом шахматной доски (checkerboarding). Он впервые проявился на в Safari для iOS в виде серых и белых клеток, словно с шахматной доски. В большинстве современных браузеров эффект проявляется как пустое пространство на экране, если вы осуществляете прокрутку быстрее, чем браузер может отрисовать страницу. Это не идеально, но это приемлемый компромисс, по сравнению с заблокированной, дёргающейся или неоткликающейся прокруткой.



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

Здесь будет поучительной небольшая история. Когда впервые вышли операционные системы вроде Windows и macOS, они допускали только один поток выполнения, и мало кто предвидел необходимость предусмотреть одновременный ввод. Только когда появились многоядерные машины, операционные системы начали встраивать параллелизм в свою архитектуру.

Также как рудиментарные органы животных дают понять их эволюционную историю, однопоточное происхождение операционных систем проявляет себя, если посмотреть на способы прокрутки в вебе. Только если операционная система допускает параллельный ввод — с мыши, клавиатуры или другого устройства — браузеры могут эффективно оптимизировать прокрутку, чтобы на неё не влияло длительное выполнение JavaScript, захламившего основной поток выполнения.

Однако в группе разработки Microsoft Edge мы делаем успехи, чтобы гарантировать плавный и восприимчивый скроллинг, независимо от его метода. В EdgeHTML 14 (который вошёл в состав Windows 10 Anniversary Update) мы поддерживаем фоновую прокрутку для следующих методов:

  • Один палец, тачскрин
  • Два пальца, тачпад
  • Колесо мыши
  • Полоса прокрутки

Если сравнить Edge с другими десктопными браузерами, то вы заметите, что только он поддерживает асинхронную прокрутку с помощью полосы прокрутки, то есть удерживание и перемещение мышью ползунка прокрутки, щелчок по полосе прокрутки или стрелкам. (На самом деле, мы без анонса представили эту функцию ещё в Anniversary Update!)

По результатам тестирования в Windows 10 (14393, Surface Book) и macOS Sierra (10.12, MacBook Air) мы получили следующие результаты:

Два пальца тачпад Тач Колесо мыши Полоса прокрутки Клавиатура
Edge 14 (Windows) Есть Есть Есть Есть Нет
Chrome 56 (Windows) Есть Есть Есть Нет Нет
Firefox 51 (Windows) Нет Нет Нет Нет Нет
Chrome 56 (MacOS) Есть N/A Есть Нет Нет
Firefox 51 (MacOS) Есть N/A Есть Нет Нет
Safari 10.1 (MacOS) Есть N/A Есть Нет Нет

Как демонстрирует* эта таблица, поведение прокрутки может драматически изменяться от браузера к браузеру, и даже от одной ОС к другой. Если вы тестируете один метод прокрутки только в одном браузере, то получите весьма неполные результаты производительности своего сайта, по сравнению с тем, как в реальности с ним работают пользователи!

В целом должно быть ясно, что у прокрутки особенное место в вебе и браузеры очень много работают, чтобы сделать её быстрой и восприимчивой. Однако, есть тонкие способы, как веб-разработчик может непреднамеренно отключить встроенные в браузер оптимизации. Посмотрим на то, как веб-разработчики могут влиять на прокрутку в браузере, по-хорошему и по-плохому.



Как прослушивающие процессы мешают прокрутке


Фоновая прокрутка даёт ощутимую прибавку в эффективности — прокрутка и JavaScript полностью разделены, позволяя им работать параллельно без помех друг другу.

Но каждый, кто немного разрабатывал веб-страницы, знает, как установить связь между JavaScript и прокруткой:

window.addEventListener(“wheel”, function (e) {
  e.preventDefault(); // oh no you don’t!
});

Когда мы добавляем прослушивающий процесс wheel, который вызывает event.preventDefault(), то он на 100% блокирует прокрутку, как для колеса мыши, так и для тачпада. И очевидно, если прокрутка заблокирована, то фоновая прокрутка тоже заблокирована.

Менее очевидно влияние такого примера:

window.addEventListener(“wheel”, function (e) {
  console.log(‘wheel!’);
  // innocent listener, not calling preventDefault()
});

Вы можете наивно подумать, что если функция не вызывает preventDefault(), то она вообще не может блокировать прокрутку или, в худшем случае, блокирует её только на время выполнения самой функции. Однако правда в том, что даже пустой прослушивающий процесс полностью блокирует прокрутку, пока не закончены все процессы JavaScript на этой странице, что вы можете проверить в этом демо.

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

Почему он должен ждать? Ну, JavaScript — это динамический язык программирования, и браузер не может знать наверняка, что preventDefault() никогда не вызовут. Даже если для разработчика очевидно, что функция делает просто запись console.log(), разработчики браузеров предпочитают не оставлять шансов. На самом деле, даже пустая function() {} вызовет тот же эффект.

Обратите внимание, что это относится не только к колесу мыши: на тач-устройствах прокрутка тоже может быть заблокирована прослушивающими процессами touchstart или touchmove.

Нужно быть осторожным, добавляя прослушивающие события на страницу, потому что они влияют на производительность!

Есть несколько интерфейсов JavaScript API, связанных с прокруткой, однако они не блокируют прокрутку. Событие scroll, хотя это в чём-то нелогично, не может блокировать прокрутку, потому что оно запускается после прокрутки, и поэтому является неотменяемым. Также и новый Pointer Events API, представленный в IE и Microsoft Edge, и который недавно начали внедрять в Chrome и Firefox, изначально спроектирован с целью избежать неумышленного блокирования прокрутки.

Даже в тех случаях, когда нам действительно нужно прослушивать события wheel или touchstart, есть определённые хитрости, как веб-разработчики могут гарантировать работу прокрутки с максимальным качеством. Посмотрим на некоторые из этих хитростей.



Глобальные и локальные прослушивающие процессы


В предыдущем примере мы видели случай глобального прослушивающего процесса (то есть прикреплённого к window или document). Но что насчёт прослушивающих процессов для индивидуальных элементов прокрутки?

Другими словами, представьте страницу, для которой работает прокрутка, но на странице есть отдельная область с собственной независимой прокруткой. Блокирует ли браузер прокрутку для всей страницы, если вы добавите прослушивающий процесс только в этой области?

document.getElementById(‘scrollableDiv’)
.addEventListener(“wheel”, function (e) {
  // In theory, I can only block scrolling on the div itself!
});

Если вы проверите на простой демонстрационной странице, то заметите, что Microsoft Edge и Safari оставят плавную прокрутку для целого документа, если прослушивающий процесс для прокрутки находится в div с независимой прокруткой.

Вот таблица браузеров и их поведения:

Два пальца тачпад Тач Колесо мыши Click-and-drag Клавиатура
Десктопный Edge 14 (Windows) Есть Есть Есть Есть Нет
Десктопный Chrome 56 (Windows) Нет Есть Нет Нет Нет
Десктопный Firefox 51 (Windows) Нет Нет Нет Нет Нет
Десктопный Chrome 56 (MacOS) Нет N/A Нет Нет Нет
Десктопный Firefox 51 (MacOS) Есть N/A Есть Нет Нет
Safari 10.1 (MacOS) Есть N/A Есть Нет Нет

Результаты показывают*, что для веб-разработчиков есть доступные оптимизации, чтобы получить пользу от этих функций браузеров. Вместо использования прослушивающих процессов wheel/touch для всего документа, предпочтительно добавить прослушивающие процессы в конкретный подраздел документа, так что прокрутка останется плавной для всех остальных частей страницы. Другими словами, вместо делегирования прослушивающих процессов wheel/touchstart на максимально высокий уровень, лучше всего изолировать их для элемента, где это нужно.

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



Пассивный прослушивающий процесс


Уход от глобальных прослушивающих процессов wheel/touchstart — это хорошая практика, но иногда такое просто невозможно, в зависимости от эффекта, которого вы пытаетесь добиться. И в некоторым роде выглядит глупо, что простое прослушивание событий заставляет браузер остановить весь мир, просто потому что существует гипотетическая вероятность вызова PreventDefault(), и он его ждёт.

К счастью, в браузерах начала появляться новая функция, когда веб-разработчики могут явно пометить прослушивающий процесс как «пассивный» и поэтому избежать ожидания:

window.addEventListener(“wheel”, function (e) {
  // Not calling preventDefault()!
}, { passive: true } // I pinkie-swear I won't call preventDefault()
);

С таким подходом браузер будет обрабатывать прокрутку так, как будто прослушивающий процесс wheel вообще отсутствует. Эта функция уже доступна в последних версиях Chrome, Firefox и Safari, и должна скоро появиться в будущем релизе Microsoft Edge. (Обратите внимание, что нужно применять feature detection для поддержки браузеров, которые не распознают пассивные прослушивающие процессы).

Для некоторых событий (в том числе touchstart и touchmove) Chrome с версии 56 принял решение вмешиваться и сделал их пассивными по умолчанию. Имейте в виду эту незначительную разницу между браузерами, когда добавляете прослушивающие процессы!



Заключение


Как мы видели, прокрутка в вебе — фантастически сложный процесс, и все браузеры находятся на разных этапах улучшения своей производительности. Но в целом мы можем сформулировать некоторые чёткие советы для веб-разработчиков.

Во-первых, лучше не добавлять прослушивающие процессы wheel или touch к глобальным объектам document или window, а вместо этого добавлять их к меньшим элементам с индивидуальной прокруткой. Разработчикам также следует использовать пассивные прослушивающие процессы, где только возможно, с применением feature detection, чтобы избежать проблем совместимости. Использование Pointer Events (там есть polyfill) и прослушивающих событий scroll — тоже верный способ избежать непреднамеренной блокировки прокрутки.

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

Наша группа Microsoft Edge продолжит инновации в данной области, чтобы обеспечить плавную прокрутку для большего количества сайтов и пользователей. Скажем это для скромного скроллбара — самого старого и неоднозначного взаимодействия в вебе!

* Результаты получены на последней версии каждого браузера в феврале 2017 года. С тех пор Firefox 52 обновил поддержку прокрутки, и теперь соответствует поведению Edge 14 во всех тестах, за исключением скроллинга полосой прокрутки. Надеемся, остальные браузеры тоже сделают улучшения в реализации прокрутки и сделают веб быстрее и более восприимчивым!
Поделиться с друзьями
-->

Комментарии (12)


  1. sbnur
    28.03.2017 13:56
    -9

    Лучшая рыба — колбаса.
    Лучший перевод — на счет в банке


  1. NLO
    28.03.2017 16:55

    НЛО прилетело и опубликовало эту надпись здесь


  1. OlegZH
    28.03.2017 18:28
    -4

    Интересно, как это всё выглядело бы в случае, если вместо языков разметки, браузеров и скриптов, были бы развитые протоколы передачи структурированных данных (в виде надстройки стека протоколов над TCP/IP и HTTP) и удалённого вызова процедур? Была же CORBA. Вот, зачем, её ней(т)рализовали?

    Чем, по сути, отличается сетевое приложение от обычного настольного приложения? Ничем!!! Есть, только, удалённый источник данных и необходимость ожидания ответа. Сколько же трудностей породило на заре Интеренета обманчивая простота писать HTML-странички. И понеслось…

    Web-тройка, куда несёшься ты? Не даёт ответа…


    1. mayorovp
      28.03.2017 22:10
      +1

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


      Песочница, аналогичная по защищенности браузерной, реализована только в ОС для смартфонов. Там, к слову, сейчас и наблюдается рассвет локальных приложений. И им ничуть отсутствие CORBA не мешает, ведь есть SOAP, XML-RPC, JSON REST...


      1. OlegZH
        29.03.2017 15:31

        Не очень понятная аргументация.

        1. Да, есть SOAP, XML-RPC, JSON REST и т.д. и т.п. Но вопрос не в том, что есть, а в том, что могло бы быть. На этот вопрос Вы не ответили.

        2. CORBA была мною упомянута исключительно как пример технологии, которая, может быть, где-то сейчас и используется, но, в своё время, эта технология претендовала на очень многое. И, если бы, CORBA (или её более продвинутый аналог) хорошо развивалась, то, вполне возможно, SOAP, XML-RPC, JSON REST просто не появились бы по той простой причине, что все их функции были бы изначально встроены в единую технологию, и не надо было бы «городить огород». Но, это всё — не более чем предположение.

        3. За безопасность локального приложения отвечает операционная система (ОС), и, по идее, именно ОС должна обеспечивать приложению что-то вроде «песочницы». А если само создание ОС будет нам гарантировать отсутствие вредоносного кода? Откуда, кстати, он появляется? Передаётся по сети? А зачем код передавать по сети? Всё должно быть и так уже установлено в ОС. Чего может не хватать распределённому приложению? Реализации каких-то специфических функций. Хорошо. Можно загрузить их с сайта, с которым Вы работаете. Но это должна быть рядовая операция установки программного обеспечения (ПО). Но, опять же, это всё — некое предположение о том, что «мир мог быть другим».

        4. Если мы живём в мире, где WEB-приложений нет, то все приложения — локальные. Следовательно, все процессы обслуживаются рядовым ПО, то есть, самой ОС. Заметьте, единообразно! Стороннему приложению нужно, только задать обработчик. Но если документ, с которым Вы работаете, реализуется объектной моделью самой ОС, то внешнему сайту достаточно передать Вашей ОС карту переходов (от одних документов к другим).

        5. И, наконец, самый тяжёлый вопрос: а зачем нам браузер? Большая часть содержимого Интернета хорошо укладывается в формат базы данных, и для просмотра этого содержимого вполне достаточно стандартных визуальных компонентов ОС. Зачем Вам заходить в браузере на нужный Вам сайт со списком, там, «чего-нибудь» (новостей, статей, обсуждений, товаров, книг), если Вы должны зайти у себя в ОС в приложение «База данных», где Вам будет достаточно подключиться к соответствующему узлу и получить от него требуемый Вам набор данных? Зачем нам страничный доступ к Ютьюбу, если у нас есть свой плеер (который всегда работает!), и нам нужно, только, иметь ссылку на источник потока?

        Вот таблица браузеров и их поведения:

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

        Почему нужно всё только усложнять, а не упрощать? Разве грамотное применение информационных технологий не заключается в создании простых и прозрачных схем взаимодействия?

        Сама же обсуждаемая здесь статья довольно любопытна. Спасибо за перевод. Но, разве, есть что-то предосудительное в том, чтобы пытаться представить себе другой мир, в котором таких проблем (вроде описываемых в статье) попросту нет, поскольку «мир другой», и в этом «другом мире» совершенно другая архитектура построения приложений?


        1. mayorovp
          29.03.2017 16:01

          За безопасность локального приложения отвечает операционная система (ОС), и, по идее, именно ОС должна обеспечивать приложению что-то вроде «песочницы».

          Вот только есть одна загвоздка: до появления iOS и Android таких ОС не было. Разве что экспериментальные поделия, не вышедшие из стадии "поиграться".


          Вот браузеры потихоньку и стали чем-то вроде ОС внутри ОС. Так сложилось исторически и с этим ничего не поделать. И обманчивая простота HTML тут вовсе не при чем.


          А если само создание ОС будет нам гарантировать отсутствие вредоносного кода? Откуда, кстати, он появляется?

          Будет установлен пользователем под видом полезной программы. Или будет загружен через уязвимость класса RCE в другой программе.


          А зачем код передавать по сети? Всё должно быть и так уже установлено в ОС.

          Отлична идея — все программы на свете заранее предустановлены! Включая вирусы и трояны :-)


          1. OlegZH
            30.03.2017 21:43

            Вот браузеры потихоньку и стали чем-то вроде ОС внутри ОС.

            Об этом и речь! Это и есть проблема.


            1. mayorovp
              30.03.2017 21:47

              Да, но причем тут HTML и CORBA?..


              1. OlegZH
                30.03.2017 22:12

                Давайте, я задам Вам встречный вопрос. Вы написали:

                Так сложилось исторически и с этим ничего не поделать.

                Вот я и спрашиваю, что именно сложилось и как это получилось.

                Есть ли Вас собственная версия событий (почему именно так, и что было виною)?



  1. zorro1211
    28.03.2017 18:41

    Спасибо за перевод.


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


    Быстрый поиск нашел eslint плагин, заточенный под delegated-events библиотеку и он предупреждает об использовании input, keydown, keypress, keyup, mouseout, mouseover, mousemove, scroll для document. Думаю можно взять его за основу чтобы сделать плагины под другие библиотеки.


    Насчет react, вот пример в обход проблемы.


    1. Serator
      29.03.2017 03:22
      +1

      В Chrome'е уже давно имеется подобный инструмент

      image