Вам знакомы муки, когда пытаешься добиться чего-то от тормознутого сайта, а он не реагирует на щелки мыши или пробуксовывает при прокрутке? Подобные проблемы с производительностью могут провоцировать:

Нервозное перещёлкивание (rage clicking)

Повышенный отток пользователей и снижение показателей конверсии

Потерю позиций в поисковой выдаче

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

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

Общее время блокировки: почему так важны длительные задачи

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

  1. Выполнится задача JavaScript на 600 мс

  2. Выполнится задача соответствующего обработчика щелчков

  3. Браузер выполнит все необходимые этапы рендеринга, нужные, чтобы страница визуально обновилась

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

На каждый шаг требуется время, а любое взаимодействие, при котором на завершение визуального обновления уходит более 100 мс, пользователь может воспринимать как медленное. Поэтому Google трактует любую задачу, занимающую более 50 мс, как «длительную», и такая задача может снизить отзывчивость страницы при приёме пользовательского ввода. В Google даже разработали специальную метрику для оценки этого показателя, именуемую «Total Blocking Time» (Общее время блокировки) или TBT.

Здесь две длительные задачи (> 50 мс) — одна из них выполняется 80 мс, а другая — 100 мс
Здесь две длительные задачи (> 50 мс) — одна из них выполняется 80 мс, а другая — 100 мс

Что такое «общее время блокировки»?

Метрика TBT измеряет совокупную блокирующую составляющую всех длительных задач, выполняемых в главном потоке браузера между первым существенным отображением (First Contentful Paint, FCP) и временем до интерактивности (Time To Interactive, TTI). «Блокирующая составляющая» — это время, затрачиваемое на каждую длительную задачу сверх первых 50 мс.

Давайте попробуем подсчитать TBT в нижеприведённом примере:

  1. Задача на 80 мс выполняется сверх 50 мс ещё 30 мс — записываем в TBT 30 мс.

  2. Задача на 30 мс ничего не привносит в TBT, поскольку на её выполнение уходит менее 50 мс, и она НЕ является длительной.

  3. Задача на 100 мс выполняется сверх 50 мс ещё 50 мс, соответственно, она добавляет в TBT 50 мс.

Поскольку TBT — это сумма всего времени сверх первых 50 мс, затрачиваемых на каждую длительную задачу, в данном примере TBT составляет 30 мс + 50 мс = 80 мс.

При тестировании на среднестатистическом мобильном устройстве Google рекомендует поддерживать TBT мобильной версии сайта на уровне менее 200 миллисекунд. Но в Википедии была задача, на выполнение которой могло уйти более 600 миллисекунд — что втрое выше рекомендуемого предела TBT, причём в рамках всего одной задачи.

Как же можно улучшить TBT?

Как сократить общее время блокировки

  • Поскольку TBT – это сумма всего времени сверх первых 50 мс, затрачиваемых на каждую длительную задачу, в данном примере TBT составляет 30 мс + 50 мс = 80 мс.

    При тестировании на среднестатистическом мобильном устройстве Google рекомендует поддерживать TBT мобильной версии сайта на уровне менее 200 миллисекунд. Но в Википедии была задача, на выполнение которой могло уйти более 600 миллисекунд — что втрое выше рекомендуемого предела TBT, причём, в рамках всего одной задачи.

  • Как же можно улучшить TBT?Как сократить общее время блокировки

  • Чтобы сократить TBT, требуется сделать одно из двух:

Чтобы сократить TBT, требуется сделать одно из двух:

  • Выполнять меньше работы в главном потоке между первым существенным отображением и временем до интерактивности.

  • Не уменьшать количество работы, но дробить длительные задачи на более мелкие, на выполнение каждой из которых тратится не более 50 мс.

Данная статья рассказывает, как выиграть время за счёт первого из двух этих вариантов.

Шаг 1: Удалите ненужный JavaScript

Притом, что в главном потоке действительно выполняются многие вещи, в том числе, синтаксический разбор HTML, отрисовка, сборка мусора, именно долгое выполнение JavaScript зачастую провоцирует проблемы с TBT. В конце концов, JavaScript – это самый быстрый способ замедлить сайт.

Занимаясь профилированием мобильной версии Википедии, я обнаружил, что большая часть времени уходит на выполнение метода _enable. Этот метод инициализировал раскрытие и схлопывание секций на странице в мобильной версии сайта. Также профилировка показала, что внутри метода _enable выполняется вызов метода .on("click")  библиотеки jQuery, также очень медленный.

function _enable( $container, prefix, page, isClosed ) {
  ...
  // Ограничено ссылками, созданными редакторами – и, следовательно, вне нашего контроля
  // T166544 – не делайте этого для ссылок на источники – они будут обрабатываться не здесь
  var $link = $container.find("a:not(.reference a)");
  $link.on("click", function () {
    // ссылка может указывать на раздел внутри той же статьи и сопровождаться символом #
    // if – это проверка, нужно ли нам раскрывать какие-либо разделы
    if (
      $link.attr("href") !== undefined &&
      $link.attr("href").indexOf("#") > -1
    ) {
      checkHash();
    }
  });
  util.getWindow().on("hashchange", function () {
    checkHash();
  });
}

Вызов .on("click"), привёл к тому, что почти ко всем ссылкам в содержимом страницы были прикреплены слушатели щелчка по ссылке так, что соответствующая секция открывалась, если в адресе нажатой ссылки содержался #. В коротких статьях со считанными ссылками влияние на общую производительность оставалось пренебрежимым. Но в длинных статьях, например, «United States» содержится по 4 000 ссылок и более, в результате чего на маломощных устройствах эта страница открывалась по 200 мс и более.

Хуже того, такое поведение просто не требуется. Расположенный ниже код, слушавший событие hashchange, уже вызывал тот же самый метод, что и слушатель события щелчка. Если в области просмотра, открытой в окне, не было прямого указания на то, где находится ссылка, то при щелчке по ссылке метод checkHash вызывался дважды: один раз для обработчика события щелчка по ссылке, а второй раз для обработчика hashchange.

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

При профилировании всегда проверяйте, на что тратится больше всего времени. Затем смотрите, есть ли код, который можно оптимизировать или, ещё лучше — удалить.

Помните, что самый верный способ ускорить сайт — удалить JavaScript.

Шаг 2: Оптимизируйте имеющийся JavaScript

Дополнительная проверка производительности показала, что на выполнение метода initMediaViewer тратится ~100 мс. Этот метод отвечал за прикрепление слушателя щелчков к каждому эскизу в контенте на странице так, что щелчок по эскизу должен был открывать средство просмотра медиа:

/**
 * Обработчик события, срабатывающий при щелчке по изображению-эскизу
 *
 * @param {jQuery.Event} ev
 * @ignore
 */
function onClickImage(ev) {
  ev.preventDefault();
  routeThumbnail($(this).data("thumb"));
}
 
/**
 * Добавление маршрутов, ведущих к изображениям, и обработка щелчков
 *
 * @method
 * @ignore
 * @param {jQuery.Object} [$container] Опциональный контейнер для поиска
 */
function initMediaViewer($container) {
  currentPageHTMLParser.getThumbnails($container).forEach(function (thumb) {
    thumb.$el.off().data("thumb", thumb).on("click", onClickImage);
  });
}

Как и в примере со ссылкой из шага 1, такая операция, как прикрепление слушателя событий к каждому эскизу на странице плохо масштабируется. Редакторы статей Википедии могут создавать статьи с тысячами картинок (и так и делают). На выполнение этого блока кода могло уходить гораздо более 100 миллисекунд, если на странице было много картинок — соответственно, метрика TBT этой страницы увеличивалась. Какой альтернативный подход здесь возможен?

Делегируйте события.

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

  1. Прикрепляем слушатель событий к контейнерному элементу.

  2. В обработчике событий используем параметр event, проверяем свойство event.target, чтобы посмотреть источник события. Опционально используем API event.target.closest(selector), чтобы проверить, каков предковый элемент.

  3. Если источником события является интересующий нас элемент или его потомок — обрабатываем его.

Обновлённый код выглядит примерно так:

/**
 * Обработчик события, срабатывающий при щелчке по изображению-эскизу
 *
 * @param {MouseEvent} ev
 * @ignore
 */
function onClickImage(ev) {
  var el = ev.target.closest(PageHTMLParser.THUMB_SELECTOR);
  if (!el) {
    return;
  }
 
  var thumb = currentPageHTMLParser.getThumbnail($(el));
  if (!thumb) {
    return;
  }
 
  ev.preventDefault();
  routeThumbnail(thumb);
}
 
/**
 * Добавление маршрутов, ведущих к изображениям, и обработка щелчков
 *
 * @method
 * @ignore
 * @param {HTMLElement} container Container to search within
 */
function initMediaViewer(container) {
  container.addEventListener("click", onClickImage);
}

В данном случае:

  1. Был пересмотрен метод initMediaViewer: теперь один слушатель щелчков прикреплялся к единственному контейнерному элементу, в котором содержались все изображения.

  2. В методе onClickImage использовался API ev.target.closest(selector), при помощи которого проверялось, откуда поступил щелчок: от эскиза или от дочернего элемента эскиза. Если ни первое, ни второе не подтверждается, то код сразу возвращается, ведь нас интересуют только щелчки по эскизам. Если первое или второе подтверждается, то происходит обработка события.

Но каковы были результаты этой работы?

Заключение

Состоялся релиз оптимизаций, обрисованных в шагах 1 и 2. Они были развёрнуты в продакшен в два этапа: сначала шаг 1, затем шаг 2.

По данным синтетического теста производительности Википедии, первая развёрнутая часть позволила сократить TBT примерно на 200 мс, а вторая позволила улучшить TBT ещё примерно на 80 мс при тестировании на настоящем смартфоне Moto G (5). В целом два этих шага позволили сократить TBT примерно на 300 мс при просмотре длинных статей на таких устройствах как Moto G (5).

Синтетический тест производительности Википедии на Moto G (5) при посещении статьи «Sweden» из английского раздела Википедии
Синтетический тест производительности Википедии на Moto G (5) при посещении статьи «Sweden» из английского раздела Википедии

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

Этот опыт показывает, что можно добиться значительного улучшения производительности, выполнив мелкую прицельную оптимизацию. Удалив или оптимизировав конкретные фрагменты кода, можно существенно повлиять на общую производительность сайта. Такой опыт лишний раз напоминает, что для повышения отзывчивости при работе с браузером практически на любом устройстве не всегда требуется вносить в базу кода сложные и обширные изменения. Иногда наибольшая разница достигается малой кровью.

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


  1. Zara6502
    01.06.2023 03:06
    -4

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

    Вопрос - а в чем проблема открытия страниц за 600 мс? Я о том, что конкретно происходит с пользователем? Это всего пол секунды, я дольше осознаю что сделал клик. А еще я в 90% случаев ссылки открываю средней кнопкой мыши, а читаю позже. Я дитя dialup, для меня скорость загрузки страниц находится в приемлемой и комфортной зоне примерно в 3-4 секунды, быстрее мне просто не нужно, так как не даст никакого эффекта. То есть для меня бы это выглядело так: 20000 программистов пять лет трудились над кодом, запыхавшиеся и в мыле прибегают и говорят - всё, теперь ваш любимый сайт будет вместо 600 мс грузиться 120 мс и я такой "Нуууууу, ок". Ну или еще аналогия - быстрый и мощный автомобиль в городе Москве ползающий со скоростью потока.


    1. vanxant
      01.06.2023 03:06
      +4

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


      1. Zara6502
        01.06.2023 03:06
        -1

        И по этой статистике большинство людей не читают сайты, а просматривают по диагонали

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

        по статистике приводит к резкому росту числа отказов - люди просто уходят на другую вкладку

        То есть человек написал запрос "Черчилль вики", подождал 200 мс, закрыл и открыл что? Пардон конечно, я старпер, я не знаю как мыслит и существует современное поколение.

        Я понимаю логику - подошел к кофейне, там очередь, постоял минуту, очередь даже не сдвинулась, ты ушел за квартал в другую. А в случае с информацией в интернете как это работает? Пойму такой подход к онлайн кинотеатру например, так как цель посмотреть кино и не важно кто его показывает, а как быть с информацией? Например есть Cyberforum, он утром постоянно лежит с ошибкой 503, потом чинят, но посещаемость все равно большая, так как важна информация а не качество работы сайта.


        1. Zara6502
          01.06.2023 03:06

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


          1. domix32
            01.06.2023 03:06
            +2

            Милисекунды на фронте и на бэке это как минимум разные миллисекунды. Ну а то что он пользуется спросом при хреновом уровне говорит только о монопольном положении оного форума/сайта.


        1. ris58h
          01.06.2023 03:06
          +1

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


          1. Zara6502
            01.06.2023 03:06
            +1

            попробуйте как-нибудь отмерить для себя продолжительность в 600 мс. Я, как человек, иногда ремонтирующий технику (ноутбуки и ПК) вижу в основном что от запроса до появления заголовка страницы в браузере проходит до 3-4 секунд. То есть от момента "я кликнул" до момента "оно открывается через 600 мс" я буду воспринимать процесс как - моментально.

            Например тыкнул в Авито на весьма быстром ПК и интернете, появилась полоска загрузки, по её активности я вижу что загрузка идёт, от клика до названия в шапке 3 секунды, прогрузка основной части сайта еще секунда, я могу кликать и клики будут работать, но загрузка еще идет, еще через 2 секунды загрузка завершена.

            Сайт 4pda - 1.5 секунды. Хабр - 2.5 секунды. Ютуб - 2.5 секунды. Али - 3.5 секунды. Дзен - 3 секунды. Wiki стартовая - 1 секунда. Сбер - 3 секунды и 22 секунды до полной загрузки. NGS - меньше секунды (если серверы в Новосибирске, то внутри сети города).

            Простите, но я пока не понимаю.


            1. domix32
              01.06.2023 03:06

              Попробуйте с дешманского телефона открыть.


              1. Zara6502
                01.06.2023 03:06
                +1

                зачем? в статье утверждается что нужно 200мс вместо 600, что это важно, что есть статистика гугла, где даже 600 мс приводит к отказу от просмотра страницы и её закрытию. Я показал, что даже на современном ПК открытие популярных сайтов занимает больше времени. Если я возьму медленный телефон, то открытие будет еще дольше. То есть нет корреляции со статистикой гугла.


                1. domix32
                  01.06.2023 03:06

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

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

                  Ну и сравнивать личный опыт с оценками конверсии без цифр довольно сомнительное занятие.


                  1. Zara6502
                    01.06.2023 03:06

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

                    Я прочитал статью, увидел обозначенную в статье проблему, прокомментировал, обозначил сомнения. А мне в качестве контраргумента дают совершенно другую отправную точку. Где логика? Так это борьба с гуглом или борьба со скоростью загрузки? Если с гуглом - то вопросов нет, если со скоростью - то проблема отсутствует.


                    1. domix32
                      01.06.2023 03:06

                       увидел обозначенную в статье проблему

                      вы использовали личный в качестве отправной точки. Вам тут же пояснили, что интернет не живёт исключительно под вашим влиянием и если лично вас 3-4 секунды устраивают, то en masse это плохо, о чём докладывал в том числе и гугл и 100 мс экономии на вашем устройстве сэкономят кому-то 1000мс на другом.

                      А в среднем борьба в сети ведётся на много фронтов - и с корпорациями с их SEO, облаками и прочим; и с провайдерами с их воздушными и не очень сетями, для которых изобретают всё новые и все более оптимальные протоколы и много ещё с чем. Википедия всего лишь показывает пример как надо делать, чтобы стало лучше. Если у вас всё работает и вы не испытываете солидарности с муками автора - радуйтесь.


  1. zartdinov
    01.06.2023 03:06
    +5

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


    1. Samael7777
      01.06.2023 03:06

      Поддерживаю двумя руками! Вот как браузер может потреблять ресурсов на пару вкладок больше, чем вся ОС или не самая древняя 3D игра? Сейчас обмазывают JS вообще все, что обмазывается, ИМХО. Причем скрипты занимают в разы больший объем, чем полезная информация. Извиняюсь за эмоции, но наболело....


    1. domix32
      01.06.2023 03:06

      Ну, вот например.

      Но совсем без JS не получится хотя бы потому что формулы рендерятся через MathJAX.