Думаю, многие из вас хоть раз видели старую главную страницу Mail.Ru, которая довольно долго не менялась. Поэтому для общего понимания, небольшая преамбула: технический апдейт, о котором пойдет речь в этом посте, стал возможен после внутренних изменений в компании.
Разработкой новой главной занималась довольно большая команда: в нее входили дизайнеры, специалисты по юзабилити и, конечно, разработчики, среди которых я и note.
Во-первых, старая страница была сверстана на таблицах и загружалась не так быстро, как хотелось бы. А значит, первым делом нужно было ускорить загрузку.
Во-вторых, надо было осовременить страницу как визуально, так и технологически. Страница должна соответствовать современным ожиданиям и тенденциям.
Для примера, могу сказать, что одним из требований к редизайну было отсутствие скролла.
Обо всем по порядку.
Быстрая загрузка
Mail.ru загружают пользователи как с хорошим, так и с очень медленным интернет-каналом. Мы хотели, чтобы портальная навигация, логотип, почтовый и социальный блоки отображались максимально быстро, чтобы пользователи с медленным каналом могли как можно скорее начать работу со страницей.
<style>
/* mailbox styles */
</style>
<div class="mailbox">
<!-- mailbox HTML -->
</div>
<script>
// mailbox JS
</script>
Для этого мы поместили перед HTML-кодом каждого из блоков описывающие его CSS-правила в теге style. Браузер, имея на руках все необходимое для рендеринга блока, делает это сразу же.
Чтобы не получилось так, что блок уже отрисован, а функцию свою еще не выполняет, например, форма авторизации в почтовом блоке с «кастомными» элементами интерфейса, следом за HTML помещен базовый JS-функционал в инлайновых скриптах.
Таким образом, ускоряемые блоки отображаются последовательно по мере загрузки страницы, и после отрисовки полностью работоспособны.
Далее можно заняться остальными блоками.
После левой колонки мы загружаем внешнюю таблицу стилей, содержащую оставшиеся правила, HTML центральной колонки, скрипт расширенного функционала и HTML правой колонки.
Мы экспериментально нашли оптимальное место загрузки JS-файла расширенного функционала.
По нашим замерам, переходы на Поиск сильно зависят от наличия поисковых саджестов. Поэтому скрипт, содержащий логику саджестов, хочется загрузить как можно ближе к блоку поиска. Но ниже в центральной колонке находятся не менее важные блоки с контентом, за которым приходят на страницу пользователи. Значит, перемещаем скрипт за центральную колонку.
Если переставить скрипт еще чуть дальше по коду, придется ждать загрузки баннера, который в среднем грузится 180ms, а центральная колонка в два с лишним раза меньше.
Соответственно, оптимальное место – между центральной и левой колонками.
Следующим этапом загружаются блоки, которые нет возможности или необходимости загружать сразу же.
Это блок с фотографиями в левой колонке и скрытые изначально табы новостей.
Блок с фото содержит внешний нестатичный контент. Это изображения, которые пользователь в любом случае увидит позже отображения левой колонки, т.к. придется ожидать их загрузки, а значит, можно отложить загрузку.
Изначально вместо блока стоит placeholder просто занимающий необходимое пространство. В конце страницы, в пост-загрузке, placeholder заменяется реальным кодом блока и начинается загрузка изображений.
При загрузке страницы пользователь увидит всего одну вкладку новостей, а остальные – лишь после клика по табу. Скрытые вкладки мы поместили в пост-загрузку и подставляем в нужное место лишь по запросу пользователя, что существенно сокращает время загрузки блока.
Существует еще ряд редко используемых блоков (вроде различных промо-попапов), элементы вызова которых находятся до объявления самих блоков.
Например, ссылка «Узнайте больше» в промо-плашке о новом дизайне, открывающая попап с дополнительной информацией.
Т.к. блок редко используемый, нет никакого смысла загружать его рядом со ссылкой вверху страницы, замедляя загрузку более важного контента. Поэтому скрытый попап находится в конце страницы. Но ссылка отображается почти сразу же, и пользователь может на нее кликнуть еще до загрузки попапа.
Чтобы обработать такие ситуации, мы используем простенькую систему отложенного вызова функций.
На ссылку вешается обработчик, сохраняющий переданную ему функцию в очереди:
<a onclick="callbackQuery.run(function(){openPopup()});">...</a>
В конце страницы обработчику очереди отдается команда о том, что страница загружена, обработчик запускает все сохраненные в очереди функции, после чего все новые переданные ему функции будут запускаться сразу же.
<div class="popup">...</div>
<script>
function openPopup(){
…
}
</script>
...
<script>
callbackQuery.loaded();
</script>
</body>
Забегая вперед, стоит обсудить загрузку логотипа.
На самом деле их два. Большой – для больших разрешений, и маленький – для остальных.
При загрузке страницы покажется один из них, а второй логотип пользователь все равно не увидит в данный момент. Чтобы отложить загрузку скрытого логотипа, используется следующее решение:
изначально в коде вместо изображений стоят две span’ки с аналогичными изображениям классами
<style>
@media all and (min-height: 765px) {
.logo__link__img_medium {
display:none;
}
}
@media all and (min-height: 765px) {
.logo__link__img_wide {
display:block;
}
}
</style>
<span class="logo__link__img logo__link__img_medium"></span>
<span class="logo__link__img logo__link__img_wide"></span>
После чего скриптом определяется, какая из них скрыта через Media Queries для разрешения данного пользователя и соответствующее изображение добавляется в пост загрузку. Второе выводится сразу же.
<script>
logos.forEach(function(logo){
if (logo.currentStyle.display === "block"){
document.write("<img … />");
} else {
var image = document.createElement("img");
...
}
});
</script>
Разумеется в noscript выводятся оба изображения для пользователей с отключенным JS.
<noscript>
<img class="logo__link__img logo__link__img_medium" … />
<img class="logo__link__img logo__link__img_wide" … />
</noscript>
CSS
Стили каждого из блоков отделены друг от друга и лежат каждый в своем файле.
Для блоков, загрузку которых надо ускорить, стили собираются поблочно в соответствующие подкаталоги папки forced-blocks, и затем автоматически подставляются в тег style в необходимом месте на странице.
/* css/forced-blocks/mailbox.scss */
@import "../../blocks/mailbox/mailbox";
<style>
<fest:insert src="css/forced-blocks/mailbox.scss" />
</style>
Остальные стили собираются в один файл (/css/styles.scss) и загружаются на страницу через link, стоящий после левой колонки.
@import "blocks/news/_news.scss";
@import "blocks/text-banner/_text-banner.scss";
@import "blocks/banner/_banner.scss";
@import "blocks/informers/_informers.scss";
<link href="styles.css" />
Такой подход дает возможность легко манипулировать загрузкой блока. Если нам нужно ускорить какой-то блок, нужно всего лишь иначе подключить его стили.
JS
В процессе загрузки JS тоже есть некоторые нюансы.
В HEAD страницы инлайном загружается базовая часть «ядра», например функции работы с событиями, необходимые для базового функционала ускоренных блоков.
<head>
<script>
var mr = {
bind: function(){}
};
</script>
</head>
<div class="mailbox">...</div>
<script>
mr.bind
</script>
После чего идет загрузка внешнего скрипта расширенного функционала, в котором
базовый набор функций расширяется.
<script>
extend(mr, {
position: function(){}
});
</script>
Таким образом, загрузкой скриптов тоже легко управлять.
Что это дало
В результате проделанной работы получились следующие усредненные цифры:
- Страница полностью загружена через 550ms после начала запроса, из которых 16ms – время генерации страницы сервером
- Портальная навигация отображается через 195ms
- Левая колонка – 180ms
- Поиск – 40ms
- Новости — 12ms
- Информеры – 30ms
- Баннер – 180ms
- Стили – 190ms
- Пост-загрузка – 117ms
- Скрипты – 500ms
Что хорошо проиллюстрирует видео процесса загрузки страницы с эмуляцией медленного соединения.
Как показано на графике, пользователь видит большую часть страницы, а именно портальную навигацию и левую и центральную колонки уже через 600 с небольшим миллисекунд, а большая часть функционала работоспособна еще через 500ms. А через 1,5 секунды страница полностью работоспособна.
Скролл
Второй большой задачей было отсутствие скролла.
Всем сразу вспомнилась технология Media Queries, с помощью которой можно подогнать страницу под необходимые размеры.
Главное – понять, подо что подгонять.
Самым простым, на первый взгляд, решением было бы посмотреть уже имеющуюся статистику по разрешениям экранов.
Эти данные хоть и дают представление о конкретных цифрах, но не дают реальной картины: не учитывают настроек элементов интерфейса ОС и браузера. Непонятно, сколько же места доступно странице.
Потому было решено сделать собственные замеры реальных вьюпортов.
По оси x – высота вьюпорта, по оси y – количество пользователей, у которых вьюпорт меньше этой высоты.
Сначала мы измеряли вьюпорт на загрузке страницы. Но нам казалось, что на эти данные не стоит опираться, т.к. все мы часто видели, как пользователи открывают страницу, затем разворачивают окно браузера и продолжают работу со страницей.
Мы посчитали количество ресайзов окна. Делающих это пользователей оказалось довольно много, порядка 12%.
Чтобы подтвердить наши догадки, мы решили измерять максимальный вьюпорт за сеанс. А также максимальный вьюпорт за сеанс в браузерах, не поддерживающих Media Queries, которых у наших пользователей довольно много – около 15%.
Полученные данные подтвердили наши догадки.
По графику видно, что у одних и тех же пользователей при открытии страницы один размер окна, а позже – иной.
На графиках отчетливо видны скачки, означающие, что на данной высоте очень много пользователей. На эти скачки мы и решили ориентироваться.
Мы выбрали следующие значения:
- 633px, что чуть ниже большого скачка
- 765px, что чуть ниже второй группы скачков
- 830px – максимальная высота, выше которой мы ничего не меняем
Что означает 633px?
Что 21% пользователей, у которых вьюпорт меньше, увидят скролл. Нас это не устраивало, и мы разбили эту область еще одним значением в 576px – там, где практически нет изменений на графике.
Область от 633 до 765 пикселей мы так же решили разбить еще одним значением в 670 пикселей. Исключительно из эстетических соображений, решили заполнить свободное пространство у большой группы пользователей.
Итак, начиная с 576px у пользователя нет скролла.
На 633px мы изменяем отступы, отображаем подписи в левой колонке, дополнительную информацию по главной новости и пару игр в правой колонке.
На 670px мы просто изменяем отступы, чтобы заполнить свободное пространство.
На 765px мы увеличиваем логотип и блок фото в левой колонке, отображаем блок «Сейчас ищут» под поиском, дополнительные информационные блоки и большую игру в правой колонке.
На 830px изменяются отступы и отображается дополнительная новость.
По ширине тоже есть изменения.
На широких страницах увеличиваются отступы между колонками, а на узких не влезающие табы новостей переносятся в выпадушку «Ещё».
Для браузеров без поддержки Media Queries мы выбрали вариант в 633px по высоте.
Реализация
Код Media Queries и пороговые цифры могут в любой момент поменяться.
Поэтому, чтобы не пришлось перелопачивать полпроекта, нужно централизованное место генерации media-выражений.
Мы решили использовать SASS mixin.
Для блока сначала указываются стили по умолчанию, а ниже в minxin передаются динамические стили с указанием высот и оси, к которым надо их применить.
.someblock {
margin-top:10px;
@include respond-to-media(xsmall, vertical, margin-top, 5px);
@include respond-to-media(small, vertical, margin-top, 10px);
}
@mixin respond-to-media($screen, $direction, $property, $value: "") {
@if $direction == vertical {
@if $screen == xsmall {
@media only screen and (max-height: 632px) { ... }
} @else if $screen == small {
@media only screen and (min-height: 633px) and (max-height: 668px) {...}
}
...
} @else if $direction == horizontal {
...
}
}
Mixin по переданным константам понимает, какое media-выражение нужно вывести, и в результате получается следующий код:
Состояние по умолчанию
.someblock {
margin-top:5px;
}
И изменение состояний для браузеров, поддерживающих Media Queries
@media only screen and (max-height: 632px) {
.someblock {
margin-top:5px;
}
}
@media only screen and (min-height: 633px) and (max-height: 668px) {
.someblock {
margin-top:10px;
}
}
Синхронизация с портальной навигацией
На всем портале Mail.Ru вверху страницы находится единый блок активной портальной навигации.
В нем по таймауту обновляются пользовательские данные: количество непрочитанных писем в Почте, количество новых событий в Моем Мире и Одноклассниках.
На новой главной странице в левой колонке, кроме почтового блока, размещена расширенная информация по социальным сетям, которую тоже нужно динамически обновлять.
Первая проблема в том, что данные мы получаем из разных источников: в портальной навигации данные Моего Мира и Почты с одного сервера, Одноклассники – с другого, а расширенные данные по Моему Миру мы получаем с самой главной страницы.
Вторая проблема в том, что портальная навигация, будучи кросс-портальной, выделена в отдельный проект, и на странице мы на нее влиять не можем, а данные нужно обновлять синхронно: при получении в портальной навигации данных от Одноклассников обновлять также данные на странице, а при получении данных по Моему Миру и Почте делать запрос за расширенными данными Моего Мира, обновлять свою часть страницы, и только потом обновлять портальную навигацию.
Для этого в JSONP callback’ах в портальной навигации была внедрена возможность отдать управление скриптам на странице и обновить цифры по обратной команде.
Отрисовка запускается только если не объявлен callback на странице, возвращающий false.
// Портальная навигация
function JSONPCallback(data){
var _cb = JSONPCallback._pageCallback;
if (typeof _cb == 'function' && _cb(data, draw) !== false){
draw();
};
function draw(){
// обновление портальной навигации
}
}
Если объявить такой callback, можно сделать асинхронный запрос, обновить необходимые блоки на странице, и лишь потом вернуть управление портальной навигации, запустив переданную функцию отрисовки портальной навигации.
// Страница
JSONPCallback._pageCallback = function(data, draw){
new AJAX('/', …, function(data){
// обновление расширенной информации
…
// возврат управления портальной навигации
draw();
});
// возвращаем false, чтобы отложить обновление портальной навигации
return false;
}
Т.о. страница может управлять отображением обновлений в зависимости от наличия необходимой информации.
Итоги
- Добились ускорения загрузки страницы в целом и управляемости этапов загрузки на медленных каналах
- Избавились от скролла – весь контент доступен на одном экране
- Стабильность работы
Хотя тема стабильности выходит за рамки данной статьи, не могу совсем ее не затронуть.
Главная страница, по сути – агрегатор данных с разных проектов. Раньше с проектов мы получали непосредственно HTML-блоки. Проектов много, контролировать выдачу нереально. Высоки шансы, что страница развалится из-за незакрытого атрибута или какой-то другой ошибки.
В процессе разработки новой главной страницы мы перешли на получение данных и трансформацию уже на нашей стороне, что существенно снизило вероятность взрыва.
Егор Дыдыкин,
лидер команды разработки главной страницы Mail.Ru и кросс-портальных проектов
Автор: madimp