Виталий vithar Харисов — один из ключевых разработчиков и руководителей Яндекса. На московском Я.Субботнике по фронтенду Виталий рассказал про лёгкую версию поиска для медленных соединений и способы оптимизации кода, позволяющие уложиться в 10 килобайт.
— Меня зовут Виталий Харисов, я сегодня прочитаю доклад под странным названием 10k. Название доклада относится к конкурсу, который в августе прошлого года проводил Microsoft, — 10k Part. Это конкурс на то, чтобы сделать полнофункциональный сайт, который полностью укладывается в 10 килобайт, это весь его HTML, CSS, JS, картинки — всё, что мы запрашиваем по сети. По условиям конкурса можно было догружать дополнительные ресурсы, которые не влияют на основную функциональность сайта, отдельными запросами. Но сам сайт должен был быть полнофункциональным и полностью доступным, работоспособным в пределах 10 килобайт.
Примерно в августе того же года мы начали работать над проектом под кодовым названием «Мобильная бабуля». У нас довольно давно есть проект под названием «Бабуля». Это проект выдачи Яндекса для очень старых браузеров — IE6, 7, 8, Firefox 3.6 и так далее. Он предлагает браузерам адаптированную для них верстку. И мы на «Бабулю» отправляем все старые браузеры, чтобы нам было проще разрабатывать основную версию поиска.
Когда выбирали название для легкой версии Яндекса, которая будет показываться на медленных соединениях, автоматически выбрали название «Мобильная бабуля».
Выглядит это примерно так. Визуально вы не найдете отличий между полноценной и легкой версией Яндекса. Отличия в том, что легкую версию мы показываем, когда у пользователя медленное соединение. Едете в метро, свалились в EDGE, уехали на дачу, свалились в EDGE — в итоге у вас может быть современный телефон, интернет никакой, и вы очень долго грузите выдачу. Нужно с этим что-то делать и показывать вам какую-то версию попроще.
По нашей статистике, таких запросов у нас порядка 60 млн хитов в месяц. Около 8% мобильных пользователей в выдаче Яндекса сваливаются в легкую версию.
Она должна выглядеть ровно так же, как основной поиск. Она должна обсчитываться ровно теми же счетчики. Она для статистики должна выглядеть точно так же.
И мы сами себе сделали ограничение: легкая версия Яндекса, HTML, который мы делаем, должен приезжать пользователю за 10 TCP-пакетов, порядка 14 Кб. Это максимальный лимит, установленный для легкой версии.
Легкая версия в продакшене примерно с февраля, ей можно пользоваться, там есть трафик, 8% пользователей попадают на нее. Если вы хотите посмотреть ее на своем ноутбуке или смартфоне, вы можете это сделать, добавив параметр lite или перейдя по короткой ссылке bit.ly/10k-2016, где эти параметры уже зашиты.
Что мы хотели делать? Легкая версия должна загружаться быстро. Она должна загружаться и отображаться быстро. Для этого мы делали минимальное количество HTTP-запросов — чтобы она доставлялась пользователю как можно быстрее. Мы обязательно используем для этого gzip, сейчас это минимальный набор, который должен быть включен на любом сайте. И если ваш сайт до сих пор почему-то не использует gzip-сжатие, включите его. Вы получите профит по загрузке автоматически.
Чтобы одновременно и ускорить загрузку, и ускорить отрисовку, и стили, и скрипты основной версии страницы мы включаем внутрь страницы. В первую очередь, это избавляет нас от лишних TCP-запросов, которые на медленном соединении работают плохо, и одновременно мы загружаем то, что нам нужно показывать пользователю, ровно в тот момент, когда необходимо.
При этом страница, которую мы загрузили пользователю, должна работать сразу, как только она отобразилась. И мы можем догружать дополнительную функциональность отдельными запросами. Мы это делаем в виде одного JS-файла, куда мы включаем саджест, какие-то счетчики производительности, отправку обработки ошибок, выставление кук, дополнительные вещи, без которых в принципе функциональность страницы не работает. Мы это догружаем отдельным бандлом. И загружаем метрику отдельным запросом. Если вдруг на медленном соединении метрика не доедет до пользователя, мы всего лишь не посчитаем статистику, но при этом пользователь сможет пользоваться выдачей Яндекса.
Дополнительный загружаемый ресурс search.js — мы делаем его gzip-версию, это минимальный набор. Используем для этого zopfli, потому что он жмет как можно лучше, и используем brotli, потому что версия сжатия лучше, дает примерно 10-процентный профит по сравнению с gzip.
И на самом деле, Can I use говорит нам, что уже сейчас его можно использовать в 50% браузеров. Нет повода не использовать более хороший алгоритм сжатия.
Прежде чем начать говорить про оптимизацию, хорошо бы видеть действия, которые вы делаете на своей странице. Тот код, который вы оптимизируете, — вы должны видеть, что вы получили профит после gzip. Бывают случаи, что вносишь изменения, делаешь gzip и видишь, что твоя страница не уменьшилась от этих изменений, а наоборот, выросла. Нужно иметь приборы, которые показывают, что все, что мы делаем, приносит нам пользу.
Простейший вариант: мы имеем предыдущую и следующую версию страницы, выводим их размер, понимаем, что у нас новая стала меньше, чем старая. Значит, мы сделали что-то правильно, у нас есть профит.
В случае с выдачей Яндекса это не работает. У нас страница динамическая, очень сильно вариативная, сильно зависит от того, что пользователь ввел и что пришло с бэкенда в качестве выдачи. Поэтому у нас есть отдельный внутренний сервис Pulse, мы его запускаем на каждый пул-реквест, это одна из проверок. У нас порядка десятка проверок, и Pulse — одна из них. Он показывает, насколько у нас изменилось время шаблонизации, насколько изменился размер до и после gzip, и мы можем видеть, что этот пул-реквест не приносит никаких плохих изменений. На скриншоте мы видим, что время шаблонизации замедлилось, при этом размер HTML уменьшился. Мы сделали такую оптимизацию, что удлинило нам шаблонизацию, а результирующий HTML стал меньше.
И мы можем в Pulse смотреть динамику в пул-реквестах — что, допустим, по времени мы в каком-то пул-реквесте драматически все улучшили. Или что в каком-то пул реквесте можно найти концы, где мы что-то сломали и что-то ухудшили.
Про оптимизацию. Первый пункт — оптимизация загрузки, отрисовки.
Архитектура нашей выдачи построена так, что у нас есть две стадии отрисовки и передачи данных пользователю. У нас есть так называемый presearch. Как только пользователь сделал запрос к поиску Яндекса, мы тут же можем пользователю ответить. И еще даже не сделав запросы в бэкенды, еще даже не начав искать то, что пользователь у нас запросил и чем ему ответить, — еще до всего перечисленного мы уже можем вывести шапку, вывести запрос пользователя, который потом может скорректироваться. Одновременно — начать искать. Эта выдача уже пойдет пользователю в его браузер. И отрисовать все остальное, уже когда мы обработали запрос.
При разработке легкой версии поиска было решено, что мы выводим стили внутри страницы. И мы выводим стили не как обычно, в шапке, а выводим стили in place в теле ответа ровно в тот момент, когда эти стили нужны. Если подвал находится в самом конце страницы, стили для подвала приедут в самом конце страницы — ровно тогда, когда пользователь до них догрузит и когда они начнут использоваться. Если у него очень медленное соединение, оборвется коннект и он недозагрузит страницу, к нему не будут загружены лишние байтики, которые на тот момент были ему не нужны.
Сначала мы такие стили специально писали в шаблонизаторе, вставляли руками. Потом немного подумали и написали автоматический механизм. Мы раскладываем всю файловую структуру по БЭМ и можем собрать все стили на проекте в некоторый JSON на момент сборки. Поскольку мы используем БЭМ-шаблонизатор и можем декларативно донасыщать его шаблонами отдельно, сбоку, то мы написали небольшой шаблон. Перед тем, как выводить любой домузел в любой HTML, он смотрит, не выделили ли мы для него еще стилей. Если мы стили не выводили, то добавляем их, а если выводили, то не делаем ничего. Это позволяет нам автоматически выводить стили в HTML ровно в тот момент, когда они впервые нам пригодились. И мы для этого отдельно в коде ничего не пишем. Мы написали отдельный шаблон, который это делает, и больше ничего не делаем.
Мысль пошла дальше. Мы это не внедрили, будем внедрять в ближайшее время.
Еще подумали, что мы же каждый раз передаем эти стили по сети, одни и те же. Если пользователь делает несколько запросов последовательно, то мы каждый раз in place в HTML будем передавать ему стили раз за разом. Подумали и решили, что реализуем следующий механизм: когда догрузили страницу до конца, то можем сходить отдельным запросом, загрузить CSS, который нужен для всей этой страницы. Благо в легкой версии Яндекса он получается порядка 15 килобайт. Сложить в кэш, выставить куку, что мы его уже загрузили, — и при повторном запросе, вместо того, чтобы in place вставлять стили в HTML, мы делаем обычный запрос за стилями, который поднимает их из кэша. Дальше мы их можем в HTML не выводить. Это еще не сделано, но не то чтобы сложно сделать.
Разобрались с загрузкой, с отрисовкой. Дальше пойдем по технологиям: где какие углы мы можем посрезать, чтобы получить более легкую и оптимальную версию.
В первую очередь, в HTML существует значение атрибутов по умолчанию. У нас есть gzip, он хорошо все сжимает, но самое хорошее сжатие — это если не сжимать ничего. И если мы можем по сети что-то не передавать, а использовать по умолчанию, то так мы и сделаем. У нас есть HTML 5, там script и style имеют значения атрибутов по умолчанию. Мы их просто не выводим.
Следующее место, где можно поэкономить байтики, — использование Entity.
Entity были придуманы еще на заре интернета, когда была таблица ASCII и больше ничего. Мы не могли никаким образом вставить специальные символы в код HTML, и были придуманы такие мнемонические подстановки. Сейчас везде уже есть UTF-8, и мы можем использовать непосредственно символы вместо подстановок.
Как правило, символы весят немного меньше.
Типичная заготовка, с которой все начинают писать свой HTML. Не так много людей знают, что мы можем не указывать ни html, ни head, ни body. Они нам вообще не нужны.
И это полностью валидный HTML, который работает точно так же, как и предыдущий вариант. Просто весит меньше.
Не все знают, что можно некоторые закрывающие теги не указывать в HTML.
Парсером они автоматически понимаются из контекста. Их нет смысла передавать.
Таких тегов не так чтобы мало. Тоже есть, на чем поэкономить.
Мое любимое. Атрибуты берутся в кавычки. В некоторых случаях мы эти кавычки можем не указывать, если у нас, допустим, нет в теле атрибутов пробелов. Если нет спецсимволов, мы можем передавать такой код.
Он тоже абсолютно валидный и работает.
Поскольку мы не пишем HTML руками, а используем bem-xjst, чтобы получить такой оптимальный HTML, то нам достаточно включить опцию не выводить финальные теги, не выводить кавычки. И в результате получается оптимальный HTML.
С HTML закончили. Что мы можем поделать с CSS?
Лучшее, что мы можем поделать с CSS, — использовать оптимизатор. Есть CSSO, лучший структурный оптимизатор CSS на рынке, который быстрый, жмет, делает все прекрасно. Используйте его. В любом случае — разрабатываете ли вы легкую версию или тяжелую, вставляете ли многомегабайтные видео в свой HTML — просто используйте CSSO всегда, он сделает хорошо.
В дополнение к нему, если есть у вас media query, я рекомендую использовать такой плагин для post CSS, который умеет эти media query объединять. CSSO не умеет, у него есть tissue, отдельный плагин, позволяющий объединить media query одинаково и получить немного более оптимальный код.
И не пишите в CSS те вещи, которые там можно не писать. Допустим, правило на слайде эквивалентно тому, что зачеркнуто. Если вы можете какие-то буковки не писать — просто не пишите.
С CSS и HTML все просто. С картинками работы чуть побольше.
Лучшее, что вы можете сделать в своем проекте, чтобы он весил меньше и загружался быстрее, — по возможности используйте векторную графику. Используйте SVG, а не бинарные картинки. Как правило, SVG весит меньше. Еще он отдельно жмется в gzip, и еще его можно красиво вставить в CSS.
Для SVG используйте SVGO. Он имеет кучу плагинов, которые позволяют по-всякому поутаптывать SVG и сделать его как можно более оптимальным. При этом функциональность ровно такой же, просто весит меньше. И для бинарных картинок используйте ImageOptim для Mac. Если у вас другая ОС — есть очень много разных аналогов, которые умеют перебирать алгоритмы сжатия картинок и выдавать вам оптимальную картинку минимального формата.
Можно здесь пару десятков байтиков поэкономить. Иногда GIF весит меньше, чем PNG, как ни удивительно. На очень маленьких картинках, на каких-то пиктограммочках, на монохромных картинках GIF весит меньше. Если в вашем проекте дорог каждый байт, вы можете попробовать сохранить в PNG и GIF и выбрать наименьшее.
Про SVG. Его можно вставить в код по-разному. Если нужно менять SVG, менять его свойства, то нет никаких других вариантов, кроме как вставлять SVG непосредственно в HTML. В нашем случае в этом необходимости нет: мы используем SVG просто как картинки. Потом мы хотим использовать «Бабулю» и для совсем старых смартфонов, которые совсем тупые. И есть еще телефоны, которые не поддерживают SVG. Ими люди пользуются, их довольно много. Нам нужно определять, поддерживает ли браузер пользователя SVG, и отдавать пользователю либо SVG, либо бинарную картинку. Такой сниппет кода позволяет нам в браузере определить, поддерживаем ли мы SVG, и на корневой элемент документа выставить соответствующий класс — есть SVG или нет.
После этого можем начинать писать примерно такой CSS-код. SVG есть — используем. Нет — используем картинки. Потом пропускаем это через сборщик, когда собираем продакшен-версию.
SVG-картинки вставляем непосредственно в CSS, а PNG-картинки загружаем по сети, потому что с очень большой вероятностью, если наш браузер не поддерживает SVG, он и data:URI base64 тоже не поддерживает.
Хочу обратить внимание, что картинка SVG вставлена как data:URI, но при этом используется не base64, а URIеncoded. В этом случае по сравнению с base64 размер картинки получается гораздо меньше, и SVG, вставленная как текст, потом еще хорошо гзипуется, чего с base64 не происходит. Если вставляете картинки в CSS через data:URI, используйте URIencoded, а не base64.
Это мы тоже пока не реализовали, следующим шагом реализуем. Если первый раз загрузили пользователю картинки и поняли, что у него есть поддержка SVG или нет, то при всех следующих запросах мы можем смотреть в куку, и пользователю, у которого есть поддержка SVG, отдавать только такой код. Пользователям, у которых нет поддержки SVG, всегда отдаем картинки. Нет никакого смысла уже при последующем запросе отдавать и так, и так.
Заключительный пункт. Пооптимизируем JS. Тут не очень богатый выбор. Есть UglifyJS, используйте его, других вариантов нет.
Я почти закончил. Хочу акцентировать внимание, что все эти техники можно использовать не только когда вы делаете прям супер-оптимизированную легкую версию и когда вам нужно уложиться в 10k-контест. Большинство из них можно использовать и на обычных сайтах. Они сделают загрузку сайта быстрее, ваших пользователей — счастливее, а вас — может быть, немного богаче.
Автор: Леонид Клюев