Для работы этой страницы вам нужно включить JavaScript. You need to enable JavaScript to run this app.
WebpurpleWebpurple
  • #performance
  • #webpack
  • #loading
November 12, 2018 12:00 AM
⌚ 8 минут

Советы по оптимизации загрузки

Несколько практических советов по оптимизации загрузки

🚀 Brief intro

Ни для кого не секрет, что от современных веб-сайтов ждут молниеносной загрузки за тысячную долю секунды, и работать они должны также плавно как и нативное приложение. Для разработчиков создаются инструменты, которые помогают в нахождение боттлнеков приложения (места в которых приложение работает медленно). Например в Google Chrome реализованы шикарные инструменты для аудита перформанса приложения, да и другие вендоры тоже не отстают (наверное). React тима выпустила новый профайлер, и если вы его еще не опробовали, то настоятельно рекомендую с ним ознакомиться.

Помимо этого в сети каждую минуту появляются статьи и советы как правильно улучшать производительность (как например и эта статья). Одним из корифеев можно считать Addie Osmani. У него есть целый цикл статей о том как надо и как не надо делать приложения. И вообще, советую ознакомиться с его блогом на medium чтобы стать настоящим 🧙‍ в данном вопросе.

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

👨‍💻 Предзагрузка данных

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

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/styles/main.css">

В файле стилей main.css мы загружаем дополнительный кастомный шрифт.

@font-face {
  font-family: 'Mali';
  font-style: normal;
  font-weight: 400;
  src: url(/fonts/mali/Mali-Regular.ttf) format('truetype');
}

Важно: В примере мы загружаем шрифт формата ttf. Первой оптимизацией загрузки в данном случае было бы использовать стандартный шрифт, а если дизайнер настаивает что проклянет тебя - то хотябы постараться подключить формата woff/woff2 link. Truetype же в примере выше используется лишь чтобы грузилось подольше и можно было бы ярче подсветить проблему.

В консоле браузера открываем вкладку Network и перезагружаем страницу.

Before preloading

Итак, чтоже мы с вами видим. Вначале у нас скачиваются наши файлы стилей и скриптов, а только потом уже скачиваются шрифты. Обратите внимание, что браузер начинает скачивать шрифты только в тот момент, когда они действительно применяются к элементу. Что приводит к тому что текст какое-то время вообще не отображается.

Таким образом мы видим, что готовый текст появляется где-то на 8 секунде, а текст с примененным шрифтом на 9 секунде. (Подробнее про применение шрифтов будет рассказано чуть позже).

Возникает вопрос, а можно ли каким то образом заставить браузер грузить шрифт заранее, но дожидаясь "реального" применения.

В этом нам как раз и поможет предзагрузка данных. Нам необходимо выполнить несколько простых шагов.

Шаг первый. Добавить линку на ресурс с типом preload в html

<!-- Предзагрузка -->
<link rel="preload" as="font" crossorigin="crossorigin" type="font/ttf" href="/fonts/mali/Mali-Regular.ttf" />
<!-- /Предзагрузка -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
    crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
    crossorigin="anonymous"></script>
<link rel="stylesheet" href="/styles/main.css">

Шаг второй. Открываем 🍻 и наслаждаемся полученным результатом.

After preloading

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

Но все не может быть так хорошо, и с большой силой приходит и ответственность. На HTTP/1 браузер умеет устанавливать 6 стабильных подключений к одному хосту. Таким образом, установив повышенный приоритет на скачивание одним файлам, мы откладываем скачивание других файлов.

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

Еще один важный момент. Вернемся к SPA приложениям. Допустим у нас есть клиентский роутинг, и код для каждой страницы подгружается отдельным чанком. А нельзя ли подгружать эти чанки кода, чтобы когда пользователь решил перейти на страницу, ему не пришлось бы ждать пока загрузится файл скриптов? Ответ - нет. См. выше. Предзагрузка нужна чтобы помочь пользователям быстрее получить отрисованную текущую страницу, для будущих действий предзагрузка не поможет. В Google Chrome будет даже выброшен warning в консоль о том что предзагруженный ресурс не был использован в течении 3 секунд.

Но где же нам искать спасения?

Предпредзагрузка или prefetch

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

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

<button class="btn btn-primary" id="btn">Click</button>
<dialog id="dlg">
    <section>
        <header>Hi, I am a modal dialog</header>
        <img data-src="/photo.jpg" width="200" />
    </section>
</dialog>

И непосредственный обработчик

document.getElementById('btn').addEventListener('click', () => {
  dlg.showModal()
  const images = document.querySelectorAll('img[data-src]')
  for (let image of images) {
    image.src = image.dataset.src
    image.removeAttribute('data-src')
  }
})

В результате мы получаем:

prefetch_before

Обратите внимание, что загрузка изображения начинается только при нажатии на кнопку. Но что если изображение слишком большое, а интернет соединение не самое стабильное. В таком случае изображение будет отрисовываться постепенно, а это 💩 а не good user experience. На помощь нам придёт prefetch.

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

Все что нам стоит сделать это добавить одну новую строчку

<dialog id="dlg">
    <section>
        <header>Hi, I am a modal dialog</header>
        <!-- Prefetch -->
        <link rel="prefetch" as="image" href="/photo.jpg" />
        <!-- /Prefetch -->
        <img data-src="/photo.jpg" width="200" />
    </section>
</dialog>

prefetch_after

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

Как и к preload, к prefetch тоже надо относиться с осторожностью. К примеру если этот попап открывается менее чем в 5% случаев захода на страницу, то зачем заставлять пользователя переплачивать за закачку ресурсов, которые им и не пригодятся?

Текущий итог

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

  • Повышать приоритет загрузки ресурсов, которые обязательно будут загружены в рамках текущей навигации. Запрос на загрузку ресурсов может исходить как из javascript так и из css.
  • Не грузить ресурсы в момент начальной загрузки страницы, а предзагружать в момент когда браузер находится в состоянии простоя, т.о. улучшая user experience.

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

Способы применения шрифтов

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

Flash of Invisible Text Этот "эффект" получил название Flash of Invisible Text (вспышка невидимого текста) или сокращенно FOIT.

На нашем тестовом сайте вымышленному пользователю пришлось бы подождать 1.5 секунды прежде чем прочитать увлекательный латинский псевдотекст. Этот режим получил название block.

Если быть совсем педантично точным, то по-умолчанию бразуер выставливают свойство в auto. Но практически во всех современных браузерах block === auto.

Если мы все же хотим дать возможность пользователю начать читать текст не дожидаясь загрузки шрифта, то мы можем выставить в значение swap. Говоря другими словами, пусть вначале отрисуется следующий в списке семейства шрифтов, который уже установлен в системе, и который не надо дополнительно скачивать, а когда загрузится кастомный шрифт, то мы просто перерисуем текст с новым шрифтом.

Flash of Unstyled Text Этот "эффект" получил название Flash of Unstyled Text (вспышка невидимого текста) или сокращенно FOUT.

И собственно что нам надо указать в файле стилей

@font-face {
  font-family: 'Mali';
  font-style: normal;
  font-weight: 400;
  src: url(/fonts/mali/Mali-Regular.ttf) format('truetype');
  font-display: swap;
}

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

Сразу пример кода

@font-face {
  font-family: 'Mali';
  font-style: normal;
  font-weight: 400;
  src: url(/fonts/mali/Mali-Regular.ttf) format('truetype');
  font-display: fallback;
}

И собственно как это выглядит в браузере

Font-display fallback

Как вы можете наблюдать браузер вначале ничего не рисует на протяжение короткого периода времени, как в примере со свойством block, затем алгоритм следующий - если шрифт скачался и доступен то используем его, если нет - то используем дефолтный шрифт и ждем в течении 3 секунд пока подгрузится кастомный и затем как в примере со swap заменяем шрифты. Если на протяжении 3 секунд шрифт так и не подгрузился, то прекращаем ждать и используем fallback шрифт.

Ну и наконец последнее свойство - optional

Font-display optional

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

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

Бонус 🙀

Придумаем реальную задачу. Есть проект на React или даже на Vue, в котором реализованы несколько страниц. У нас есть требование разбить все скрипты на постраничные чанки и подгружать их по мере необходимости, по запросу. Также мы знаем что со страницы А на страницу Б пользователь перейдет в 99% случаев. Отличное время чтобы использовать prefetch данных. Но... проект собирается с помощью webpack, и куда же надо вставлять <link type="prefetch">? Ответ: webpack предоставляет готовый api 🤗

import(
    /* webpackPrefetch: true */
    '/src/pages/Profile'
).then(({default}) => apply(default))

Документация

В Терминологии webpack это называется magic comments. И как написано в документации мы можем устанавливать и preload и prefetch таким образом.

Бонус номер two

Я коснулся лишь мизерной части того как повысить перформанс загрузки приложения. Это целая наука. А если ваша цель просто создать быстрый сайт и вы не хотите вдаваться в такие глубокие детали - то попробуйте Gatsby. В нем большинство функционала идет из коробки. И вообще всем мира и добра, реакта и гэтсби! ✌️