Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Некоторое время назад я заинтересовался производительностью веб-сайтов, оптимизациями загрузки и тому подобными вещами. И вот, зайдя очередной раз на Хабр, подумал, что привык воспринимать довольно быструю загрузку ресурса как данность, даже не задумываясь о том, как этого удалось добиться. Поэтому я решил совместить приятное с полезным — посмотреть, как обстоят дела с производительностью Хабра и какие технические решения были сделаны для его оптимизации. Для тех, кому интересно узнать, что было сделано что бы мы получали контент как можно быстрее и как выглядит загрузка Хабра из Аргентины — прошу под кат.
Подготовка
Нам понадобится свежая версия Chrome/Canary работающие в анонимном режиме (проверьте что бы у вас были выключены все расширения). Так же, в консоли разработчика (Developer Tools – F12), во вкладке network нужно установить флаг disable cache, т.к. профилировать мы будем только первую загрузку, пока никаких ресурсов в кэше еще нет.
Этап первый — "По верхам"
Основная цель этой части – познакомиться с сайтом в целом, разобраться в его структуре и узнать какие конкретно ресурсы ему нужны.
Открываем developer tools, переходим на вкладку network, открываем сайт и в самом низу вкладки, смотрим статистку по использованию сети:
Весь сайт был загружен за 2.02 секунды, что выглядит просто замечательно (учитывая нагрузку Хабра). Событие DomContentLoaded (дальше просто DCL) вообще появилось за 1.01 секунды что выглядит еще лучше. При всем при этом, сайт делает 189 запросов и грузит 9.6МБ ресурсов. Это говорит нам о том, что либо команда Хабра — гении (вполне может быть), а статью надо заканчивать прямо тут (и проситься к ним в команду за кофе и печеньки), либо вспомнить что у меня канал 100 Мб/c и Сore I7. Т.е. нужно немного приблизиться к реалиям и хотя бы ограничить ширину канала.
Включаем режим Fast 3g и смотрим еще раз:
DCL ухудшился до 3.48 секунд что все еще достаточно приемлемо. А вот окончательно сайт загрузился за безбожные 54.76 секунды. Теперь все логично — нельзя так просто взять и загрузить почти 10 мегабайт, когда у тебя слабое соединение. Скорее всего, ребята провели хорошую работу что бы максимально быстро показать нам контент (об этом говорит то, что даже в режиме fast3g DCL возникает достаточно быстро), а все не критичное оставили грузиться в фоне. Чуть позже мы это проверим, а теперь давайте посмотрим почему мы грузились так долго. Отсортируем все запросы по времени загрузки:
кликабельно
Дольше всего грузятся изображения (топ-7) и один JavaScript файл — prebid.js. Если мы посмотрим на их размер, можно будет предположить, что именно это есть причина медленной загрузки.
С предположениями нужно быть предельно осторожным. Так, например, длительная загрузка ресурса может быть вызвана не только размером файла, а и, например проблемами с DNS, пиковой нагрузкой на хранилище или холодным кэшем сервера. Поэтому, не проверенное предположение может вынудить вас потерять время над решением проблемы, которой даже не существует.
Глянув на статистику (TTFB: 0.610 ms, загрузка: 40 000 ms) можно сделать вывод что наше предположение достаточно вероятно. Давайте полюбуемся на наш TOP 1 (png, 1560Х780, 24bit):
На самом деле проблемы с изображениями — это, в основном, проблемы нашего с вами траффика. Изображения (в отличии от стилей и скриптов) не блокируют рендер веб-страницы, и поэтому, несмотря на наличие таких тяжеловесов (бывает и хуже, видели), на производительности это почти не сказывается. Хотя, конечно, оптимизация в этом направлении (например перекодировка в jpeg2000 или webp, или прогрессивная загрузка была бы не лишней).
Как я писал выше, изображения не блокируют рендер страницы. Зато они используют наш пул соединений, а в http 1.x их количество ограничено, да и с http 2.x и Chrome тоже не все так гладко насколько я слышал. Поэтому даже тут есть шанс, что какое-то изображение может замедлить загрузку синхронного скрипта, а тот, в свою очередь, уже остановит рендер страницы. Кроме того, загрузка изображения так же может вызвать пересчет лейаута тем самым замедляя рендер. Если мы посмотрим на разметку Хабра, практически на всех img тегах стоит ширина и высота. Это убирает необходимость reflow положительно влияя на производительность загрузки. Подробнее об этом можно почитать тут.
Давайте посмотрим какие еще ресурсы тянет Хабр — пройдемся по типам и отсортируем по времени загрузки. Начнем с JavaScript
JavaScript
У javascript есть несколько проблем в плане производительности сайта. Во-первых (про это все знают и давно говорят), синхронный js блокирует дальнейший рендер страницы. Т.е. пока мы наш JavaScript не получим и не выполним, браузер будет ждать (на самом деле не совсем, браузер, особенно Chrome, умеет в оптимизацию, но это уже другая история) и не будет рендерить дальнейший контент. Используя синхронные скрипты в head, мы откладываем момент, когда пользователь увидит хотя бы что-то (даже текст). Поэтому все стараются выбросить Js в конец страницы или вообще сделать его асинхронным. Это работает, но не решает вторую проблему, которую приносит нам факт того, что JavaScript это все-таки скриптовый язык. Поэтому, браузеру мало его просто скачать — его нужно еще и "понять". И тут, оказывается, что это проблема, потому что слабые процессоры (например, в дешевых телефонах или нетбуках, или даже хороших ноутах в режиме ограниченной производительности) делают это медленно, блокируя основной поток! Ниже будет пример как асинхронный рекламный скрипт влез в критическую секцию рендера Хабра и пусть немного, но все же попортил производительность. Если кто заинтересовался, вот тут есть очень (очень-очень) хорошая статья на эту тему
Скриптов Хабр грузит много — 1.1 МБ (и это уже в пожатом виде) на 40 запросов. Это прямо скажем существенно, это нам еще аукнется и с этим надо что-то делать. Отсортируем наши скрипты "по водопаду". Наша задача — найти скрипты которые загрузились ДО синей линии, так как именно они (вероятнее всего) мешают нам отрисовать сайт как можно быстрее.
кликабельно
Открываем html, который отдал нам Хабр (важно смотреть именно ответ, так как итоговый html будет выглядеть по-другому) и проходимся по списку. Как видим — jQuery, raven.js, advertise.js и adriver.js грузится синхронно прямо из head тега (т.е. блокируют вообще все). Gpt и publisher грузятся из head но уже асинхронно (т.е. ничего не блокируют, браузер будет рендерить страницу дальше пока они качаются). Vendors, Main и Math, checklogin грузятся в конце, но синхронно (Т.е. текст уже есть, можем читать, но DCL не появиться пока они не загрузятся). Остальные в начальном ответе не появляются — они добавляются динамически, но это уже другая тема.
Итак, мы нашли те скрипты, которые так или иначе влияют на то, как быстро мы увидим текст на странице. Это первые кандидаты на оптимизацию. В идеале — их нужно сделать асинхронными или поместить как можно ниже, чтобы дать браузеру возможность отрисовать для нас контент. Однако идеал не достижим, так как скорее всего на сайте есть другие скрипты, которые зависят от того же jQuery. Желание как можно раньше загрузить raven — библиотеку для отслеживания разных ошибок, возникающих на клиенте, тоже можно понять. А вот advertise.js и adriver.js уже реальные кандидаты на, как минимум переезд вниз страницы, а как максимум еще и в асинхронный режим. Аналогичная история и с gpt и publisher. Да, они грузятся асинхронно, но, тем не менее они могут (и будут) нам мешать при загрузке. Поэтому их тоже можно было бы отправить в самый низ страницы. Кроме того, можно попробовать воспользоваться атрибутами Resource Hints — preload/prefetch/dns-prefetch для подсказки браузеру что стоит подгрузить заранее. Кстати, советую почитать про Resource Hints – очень интересный инструмент хоть и с ограниченной (пока) поддержкой.
Теперь отсортируем список по размеру файла:
https://github.com/Drag13/articles/blob/habrformance/habrformance/scripts.PNG
Видим скрипт, который грузится дважды (pubads). Так же замечаем, что prebid.js грузится из папки not-for-prod
Для очистки совести проверяем что все скрипты минифицированны. Внезапно, оказались не минифицированными микроскрипты check-login.js и adriver.js. Особенно порадовал контент последнего:
И со скриптами можно временно заканчивать.
Стили
Со стилями все тоже не так просто. Во-первых, синхронная загрузка стилей тоже блокирует основной поток (правда парсятся они быстро, быстрее чем JavaScript). А во-вторых, все немного сложнее. Зачем браузеру нужен CSS? Что бы собрать СSSOM. А что может делать JavaScript с CSS? Правильно – менять. А что будет если CSSOM еще не собран, а js уже пытается что-то там менять? Ответ – а кто его знает. Поэтому браузер откладывает выполнение JavaScript-а до момента, пока CSSOM не будет высчитан. Как правильно написали в еще одной очень полезной статье – нет CSS = нет JavaScript. Поэтому загрузка CSS блокирует выполнение JS.
Вот стили которые загружает Хабр:
кликабельно
Как видим, их всего три, причем второй и третий грузятся в отдельном фрейме, поэтому мы их игнорируем. А вот первый набор стилей критично важен для всего сайта. И загружали мы его очень долго. Почему? Во-первых, долго ждали ответа от сервера (TTFB 570 ms, мы к нему еще вернемся в третьей секции), во-вторых, долго грузили 713 ms. Что тут можно сделать. Первое и самое простое — попробовать добавить атрибут preload. Это даст подсказку браузеру начать загружать CSS как можно раньше. Второй вариант — выделить критический CSS и внедрить его непосредственно в веб страницу, а остатки загрузить синхронно (или даже асинхронно). Это приведет к росту размера страницы (но она и так не маленькая и +5кб уже ничего не испортят) и возможной перерисовке лейяута (потеря времени), но можно попробовать.
Шрифты даже показывать не буду. Он там один, правда почему-то грузится с сервера Хабра напрямую вместо CDN (и скорее всего не просто так). Зато скажу спасибо, за то, что шрифт только один.
На этом обзорную часть разбора сайта можно считать завершенной. Время идти глубже.
Этап второй — "Ручной режим"
На прошлом этапе мы посмотрели, что грузит Хабр. Теперь мы посмотрим как это рендерится.
Переходим на вкладку профайлинга. Проверяем что стоит slowdown fast3g (потом запустим еще раз без ограничений) и запускаем. В первую очередь нас интересует все что происходит до события First contentful paint (FCP), и, на всякий случай DCL. Выделяем область от старта загрузки до FCP и смотрим на итоговую диаграмму.
Все происходит довольно быстро — 2.161 сек до FCP (было быстрее, но, видимо, что-то поменялось), однако как видно, большую часть времени браузер простаивал. Полезная нагрузка заняла всего 14% времени (около 310 ms). В идеале, основной поток браузера работает постоянно — парсит html, CSS, выполняет JS. А тут — ничего. Почему? Потому что браузеру было просто нечем заняться. Помните мы урезали траффик? Браузер разослал запросы и теперь просто ждет их выполнения. Если мы откроем диаграмму сетевых процессов (самая верхняя) все станет сразу понятно.
кликабельно
В 2029 браузер увидел main.bundle.css, отправил запрос на его получение и стал ждать. В это время пресканер (хром умный, он умеет забегать наперед) обнаружил, что внизу есть синхронные скрипты и не дожидаясь пока придет css отправил запрос на получение скриптов. Потом загрузились advertise и adriver (но мы помним, что пока CSSOM не создан, трогать JS нельзя), поэтому браузер их проигнорировал. После этого загрузились gpt и raven, но CSS все еще грузился, поэтому их тоже проигнорировали. Наконец-то загрузился CSS, который браузер распарсили за 12 ms и тут же пошел парсить внедренный в страницу JS. Потом браузер снова «заснул» почти на 150 ms ожидая jQuery.min.js. И после этого уже взялся за работу всерьез — разобрался с загруженным jQuery (20мс), распарсил raven.js (4мс), распарсил почти всю страницу (36мс), пересчитал стили (28мс), рассчитал лейяут (78 ms + 8 ms) и, наконец, за ~6 ms раскрасил нам страницу. Тут мы и получили FCP. Дальше мы парсим страницу, парсим скрипты — привет никому не нужные (publishertag.js, gpt.js которые влезли в main thread до DCL), немного поигрались со стилями (что вызвало небольшую потерю времени из-за перерасчета лейяута) и начали ждать vendors.bundle.js. На то что бы дождаться вендоров мы, почти вхолостую, потратили еще около 1100 ms. Правда параллельно с ним мы загружали еще и main.bundle.js (который, кстати, загрузился быстрее), поэтому не все так плохо. Дальше все снова пошло хорошо. Распарсили вендоров, распарсили основной бандл, распарсили Math.Jax, и наконец то допарсили страницу и получили DCL. Правда тут же сработал какой-то хендлер который на моем I7 остановил основной поток на 80 ms (т.е. сайт как бы подвис на 80 ms). Сейчас мы этого почти не заметили, но на слабом процессоре, теоретически, это может быть заметно, Если такое у вас встречается уже после рендера контента — это повод проверить JS.
Что тут можно сказать. Опять-таки, несмотря на то как это выглядит все достаточно неплохо. Основные проблемы нам доставили:
- Большой простой браузера (в том числе из-за искусственной диспропорции между вычислительной мощностью компьютера и шириной канала, но это тоже вполне валидный сценарий)
- Долгая загрузка CSS которая отложила работу с критическим js
- Синхронная загрузка jQuery, vendors и main бандлов которые остановили появление контента.
Что с этим можно сделать мы уже обсуждали:
- Попробовать добавить атрибут preload для CSS и возможно некоторых JS
- Уменьшить или заинлайнить CSS
- Попытаться вообще выбросить скрипты из синхронной загрузки (хотя бы какие-то)
Теперь давайте повторим тоже самое уже без ограничений на ширину канала. Картина уже гораздо лучше: 0.452 секунды до FMP, простой всего! 13 ms. DCL — 953 ms, простой 15 ms. Вот как выглядит загрузка моей мечты. Вот только это получилось из-за того, что открыл новую вкладку и не убрал кеширование. Попробуем то же самое, но без кэша:
Все тоже достаточно хорошо, FCP/FMP — 1689, полезная нагрузка 45%. Кстати, подгонять загрузку до 100% тоже плохо, так как более слабые машины будут перегружены. Так что лучше иметь запас на простое. Но, тут неожиданно много времени ушло на Rendering — 400 ms для FMP. Из них 200 ms ушло на пересчет стилей и перерасчет лейаута.
Кстати, еще один интересный момент. Помните рекламные скрипты — gpt.js и publishertag.js которые упоминались в первой части? Теперь можно заметить, что несмотря на их асинхронность они таки смогли (пусть и немного) но испортить нам статистику. Это произошло потому, что выполнение асинхронных скриптов выполняется по готовности самих скриптов. Т.е. это может случиться в любой момент, в том числе до FCP/DCL, что и произошло в 3309.
Итак, ручную разборку мы сделали. Пора расчехлять автоматику.
Этап третий — "С этого надо было начинать"
Я думаю, все понимают, что это исследование очень условно. Как минимум, потому что все время пока я тестировал появлялись новые статьи, изменялась нагрузка на сервер и загруженность сетевых магистралей. По-хорошему, нужно развернуть выделенный сервер, в который никто не будет ничего писать, эмулировать релевантную нагрузку с релевантными задержками и только тогда что-то профилировать. Иначе вы можете высказать некоторое предположение, (например про необходимость добавления preload атрибута на CSS), добавите его, задеплоите на продакшн, а потом окажется что нагрузка во время теста резко выросла и тот же стиль нам сервер отдал уже на 500 ms позже. И окажется что preload плохой — он все только сделал хуже. А уж автор и вовсе редиска последняя. Кроме того, профилировать вручную (как во второй части), пока вы не прогнали все автоматические инструменты и не решили обнаруженные там проблемы тоже имеет мало смысла, потому что после первых же ваших изменений картина изменится.
Поэтому, перед любыми экспериментами нам нужна максимально стабильная среда. Это раз. Второе — никогда не надо лезть глубоко (например, в профилирование) с самого начала. Сперва запустите автоматические инструменты, пусть они работают за вас. Соберите отчеты, посмотрите, что максимально хорошего можно сделать за минимальное время. Попробуйте это сделать. Если получиться, возможно вам уже этого хватит. Например у вас css.bundle весит 100kb, а в реальности вам нужно 45kb (кстати, реальная ситуация: взяли весь bootstrap, а нужна была только сетка). Уменьшили размер стилей и выиграли половину секунды практически даром. Это два. Всегда перепроверяйте. Казалось бы, заинлайнил стили, все должно стать лучше, а стало хуже. А почему? А потому что в стилях 50kb base64 картинок, а наша страница всего грузила 200кb ресурсов. Просто помните, что тут нет серебряной пули и универсальных сценариев. Это три. И последнее, пусть оно и противоречит предыдущему предложению. Если у вас не сложный сайт — просто мониторьте TTFB (проблемы с сервером), размер загружаемых ресурсов (привет логотипам за 2MB) и не давайте доступ к Google Tag Manager-у кому попало. Скорее всего этого будет вполне достаточно.
Не будем далеко ходить, в том же Developer Tools откроем вкладку audit и запустим LightHouse (LH) со следующими настройками: desktop, performance only, no throttling, clear storage. Немного подождав (не уходите со страницы, на которой проходит аудит и лучше вообще ничего не делайте) получаем фантастические цифры (даже с отключенным кэшем)
Мне даже кажется, что под эти цифры специально подгонялись.
При этом LH все-таки жалуется на:
- Устаревший формат изображений (предлагает использовать webp, jpeg2000, etc)
- Количество DOM node – их аж слишком много: 2533, при рекомендованных 1500.
- Слишком интенсивный траффик
- Не корректную политику кеширования — отсутствуют заголовки кеширования на 23 ресурсах
- Использование document.write
Тут все понятно. Изображения пережать и перекодировать (ха), DOM — найти кто создает так много node (ха-ха), выставить кеширование на статические ресурсы (ну это можно), и оторвать document.write-у его writer (три ха-ха если это чужой скрипт)
Многие из нас (и я не исключение чему свидетельствует эта статья) любим давать советы. Однако многое из сделанного, имеет свою, часто скрытую, причину. Кроме того, давать советы легко, а вот выполнять их гораздо сложнее. Чего стоит один лишь совет уменьшить CSS, — попробуй разберись какие стили не нужны, если их тонны, а часть стилей прилетает из JavaScript. Плюс, любые подобные изменения скорее всего потребуют регрессионного тестирования, что влетит клиенту в копеечку. Поэтому к любым оптимизациям нужно подходить с хорошим скептицизмом и в первую очередь оптимизировать те проблемы что дают наибольший выигрыш на единицу времени.
Теперь попробуем запустим тоже самое, но уже ограничив ширину канала на fast3g и, внимание, впервые установив четырехкратное замедление процессора (прощай I7, привет celeron):
Как видим все стало хуже (но FCP все еще 3500 ms, FMP 5.7):
- Значительно выросли проблемы, связанные с изображениями. LH снова предлагает использовать next-gen форматы изображения, но теперь, по его мнению, это сэкономит нам целых 11 условных секунд
- Выросла проблема с блокирующими рендер ресурсами. Причем тут всплыли два синхронных микроскрипта adriver.js и advertise.js по 50байт каждый, о которых я уже упоминал.
- Появилось предложение порезать CSS с намеком на то, что используется всего 5kb из 45kb.
- Впервые появилась проблема слишком загруженного main thread. Мы порезали вычислительные мощности и браузер просто начал захлебываться в JavaScript — (8.5 секунд для script evaluation из них 2186 ms ушло на raven.js). Не слабо да?
Какие выводы можно вынести отсюда? Хабр хорошо работает на быстром интернете и мощных машинах. Смещение в сторону чего-то подешевле может вызвать проблемы. Проблемы вызваны:
- Большими изображениями
- Большим объёмом DOM
- Большим количеством JavaScript на странице
Было сделано очень много что бы эти проблемы не мешали быстрому появлению контента, однако для слабых машин это все еще актуально (например, блокирующий raven.js).
И наконец, давайте выйдем за пределы консоли разработчика, и попробуем еще один известный инструмент — https://www.webpagetest.org/
Он замечателен многим — и подробными графиками, и подсказками, и настройками. Но что у него еще есть хорошего — он позволяет вам выбрать локацию, из которой вы якобы смотрите сайт. Все мы помним, что Хабр стал интернациональным. Так давайте посмотрим, как Хабр будет грузиться, например из Аргентины. Конечно, был бы у меня доступ к метрикам, я бы выбрал более релевантную локацию, а так — будет Буэнос-Айрес.
И для чистоты эксперимента используем https://habr.com (без ru/en постфикса).
(показан частично, полная версия тут)
Здесь все очень подробно. Можно посмотреть сколько времени (и кому) требовалось что бы отрезолвить DNS (500 ms для основной страницы), сколько ушло на установку соединение, ssl и так далее. Тут же видно, что мы потратили 1150 ms только на переадресацию c основной страницы на англоязычную версию (повод посмотреть почему так долго). Кстати, переадресаций оказывает довольно много (больше всего этим грешит habrastorage).
Можно так же посмотреть злополучный main.bundle.css. Оказывается, это первое обращение к dr.habracdn.net что, приводит к необходимости выполнить dns lookup — 36 ms (кстати я видел и по 400 ms). Плюс SSL negotiation 606 ms, плюс TTFB 601 ms, плюс еще загрузка. В общем не быстро. Но не смотря на все это DCL — 4100 ms или около того, что тоже радует.
Еще есть отличная вкладка image analysis которая, показывает, как и сколько можно сэкономить если пережать изображения. Фото какого-то рыжего котейки с сайта в PNG весит 1.4МБ, а в webp + downscaling (да читерство, но все же) — 17.7 KB. Для сравнения, то же фото в таком же разрешении, но все еще в png весит 154кб. В общем только об этом инструменте можно писать отдельную статью. Но, если вкратце:
- Потеряли около двух секунд на начальной загрузке страницы (и это при том, что потом мы ее загрузили за несколько миллисекунд, т.е. канал был просто шикарным).
- Большое количество переадресаций. Это ест время, с этим надо разбираться.
- Долгий TTFB (webpagetest даже поставил нам F за это) — возможно слишком высокая нагрузка
- Действительно большая диаграмма загрузок которая позволяет оценить, насколько большое количество запросов делает Хабр (и при этом все равно основной контент отдается довольно быстро)
Выводы
А выводов у нас не будет. Будут моменты для дальнейшего изучения:
Для сервера:
- Иногда TTFB прыгает до 500 ms и более для статичных ресурсов (это без dns, ssl и initial connection)
- Перенаправление habrastorage
Для клиента:
- Не оптимальные изображения
- Большой размер DOM-a
- Возможно не оптимальные позиции блокирующих скриптов
При этом, очевидно, что проведена хорошая работа по оптимизации. Сайт достаточно быстро грузит основной текст, остальное происходит уже после того, как мы видим контент. Понятно, это не SPA, сервер отдает уже готовый к показу контент, для которого даже JS не нужен — только стилизировать и готово. Но тем менее ребята молодцы. Есть правда еще пожелание уменьшить количество трекеров/аналитики/рекламы, но это уже скорее мечты мало сопоставимые с требованиями бизнеса
Полезные ссылки
- Про метрики производительности
- Про производительность
- Глубокое профилирование в Хроме
- Webpagetest
- Как использовать webpagetest
- Как работает рендер 1
- Как работает рендер 2
- Про FCP
PS. Прошу прощение за большое количество англицизмов (особенно за лейаут) и за лонгрид. Но без них уж очень тяжело, а все эти ч.1, ч.2, ч.3 мне уже порядком надоели.