New Adventures in Responsive Web Design

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


В основе статьи расшифровка выступления Виталия Фридмана из Smashing Magazine на декабрьской конференции Holy JS 2017 Moscow.
Чтобы нам с вами не было скучно, я решил подать эту историю в формате небольшой игры, назвав ее Responsive Adventures.

c3d635f3533095f0d4d89e88ec32e6f5.png

В игре будет пять уровней, и начнем мы с простого уровня — компрессии.

Уровень 1 — Компрессия


3a48a5c2dae5e30f785c035431ff4501.png

Компрессия — это сжатие, а сжимать во frontend можно, например, изображения, текст, шрифты и так далее. Если имеется необходимость максимально оптимизировать страницу в плане текста, то на практике обычно используют библиотеку для сжатия данных gzip. Чаще всего применяют наиболее распространенную реализацию gzip — zlib, которая использует комбинацию алгоритмов кодирования LZ77 и Huffman.

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

Существуют алгоритмы сжатия данных Brotli и Zopfli. Zopfli можно рассматривать как более эффективный, но при этом более медленный вариант gzip. Brotli — это новый формат сжатия и декомпрессии без потерь.

803475d3a8534b0eaba3182dbcdb4494.png

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

Brotli и Zopfli


  1. Brotli значительно медленнее при сжатии данных в сравнении с gzip, но обеспечивает гораздо лучшую компрессию.
  2. Brotli — это формат сжатия без потерь с открытым исходным кодом.
  3. Декомпрессия у Brotli быстрая — сопоставима с zlib.
  4. Brotli дает преимущество при работе с большими файлами при медленных соединениях.
  5. Brotli сжимает эффективнее на 14–39%.
  6. Идеально подходит для HTML, CSS, JavaScript, SVG и всего текстового.
  7. Поддержка Brotli ограничена соединениями HTTPS.
  8. Zopfli часто используется для сжатия на лету, но при этом является хорошей альтернативой для однократного сжатия статического содержимого.


a6b5f4cd7b5d3df2e492311f4fc2a546.png

3a3c590edd9048639546d4067b565bff.png

Стратегия сжатия Brotli/Zopfli


Стратегия выглядит следующим образом:

  1. Предварительно сжимать статические ресурсы с помощью Brotli + Gzip.
  2. Сжимать при помощи Brotli HTML на лету с уровнем сжатия 1–4.
  3. Проверить поддержку Brotli на CDN (KeyCDN, CDN77, Fastly).
  4. Использовать Zopfli, если нет возможности установить/поддерживать Brotli на сервере.


Уровень 2 — изображения


f486d963bb88a13e7b42e12adebb7581.png

А вот что мы будем делать с изображениями?

Давайте представим, у вас есть хороший landing page со шрифтами и изображениями. Необходимо, чтобы страница загружалась очень быстро. И мы говорим об экстремальном уровне оптимизации изображений. Это проблема, и она не надуманная. Мы предпочитаем о ней не говорить, ведь в отличие от JS изображения не блокируют рендеринг страницы. На самом деле это большая проблема, ведь размер изображений со временем увеличивается. Сейчас в ходу уже 4K-экраны, скоро будет 8K.

25b78a3b13b8bc18e596be5e77d3591a.png

В целом 90% пользователей видят на странице 5,4 МБ изображений — это очень много. Эта проблема, которая требует решения.

Конкретизируем проблему. Что если у вас есть большая картинка с прозрачной тенью, как на примере ниже.

328988a7f86051621ac5d280c63890af.png

Как ее сжать? Ведь png достаточно тяжела, а после компрессии тень будет выглядеть не очень хорошо. Какой формат выбрать? JPEG? Тень так же будет выглядеть недостаточно хорошо. Что можно сделать?

0645152e1264528173a6e2df52dc29a4.png

Один из самых лучших вариантов — разделить изображения на две составляющие. Основу изображения поместить в jpeg, а тень в png. Далее соединить две картинки в svg.

494d5668fa6332d39df7e323f6a34666.png

Почему это хорошо? Потому что изображение, которое весило 1,5 МБ, теперь занимает 270 КБ. Это большая разница.

Но есть еще пара трюков. Вот один из них.

Возьмем два изображения, которые визуально отображаются на веб-сайте с одинаковыми пропорциями.

342d7832539456d2a949a50a98f41b50.png

Первое — с очень плохим качеством, имеет реальный и визуальный размер 600×400 px, а ниже оно же, но визуально уменьшенное до 300×200 px.

a25e5c1da8453656df775f76a24dd216.png

Давайте сравним это изображение с изображением, которое имеет реальный размер 300×200 px, но сохранено с качеством 80%.

0c6e08c2b908cbe8b5e3ce8acdb8561d.png

Большинство пользователей не в состоянии различить эти изображения, но картинка слева весит 21 КБ, а справа — 7 КБ.

Есть две проблемы:

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


Интересный тест, в котором использовался этот прием, провел шведский онлайн-журнал Aftonbladet. Изначальная настройка качества изображений была установлена в 30%.
016480e35cfb065342b8416e8b70b07e.png
В итоге их главная страница с 40 изображениями с применением данной техники заняла 450 КБ. Впечатляет!

Вот еще хорошая техника.

b0f30b28f8d9f2bf41d3063ff01b6d0c.png

У нас есть картинка, и нам нужно уменьшить ее размер. За счет чего она будет лучше сжиматься? Контраст! Что если его убрать или уменьшить значительно, а потом вернуть при помощи CSS-фильтров? Но опять таки, тот, кто захочет скачать эту картинку, столкнется с плохим качеством.

Этим приемом можно добиться больших результатов. Вот несколько примеров:
a61adce8dbe4386e7beecaa2c2119ce7.png
f55ddf07bd3766e7f33a7da1de8e0d1e.png
7865c56e159078b85e3d8e1a0a57c377.png
dad064932849f278083956226d941002.png
Все бы хорошо, но как же дополнительные задержки рендеринга? Ведь браузеру приходится применять фильтры к изображению. Но тут все достаточно позитивно: 27 мс против 23 мс без применения фильтров — разница несущественная.

8276721cd1d5fe63d10a1ecff2332eb5.png

Фильтры поддерживаются везде, кроме IE.

ba7e787ac3e7bf30d3efce7f22c7b307.png

Какие еще есть приемы? Сравните два фото:

d963d88c5ac2023938380fe839f76a0e.png

b7bf9bc842846d190a8b8de279632a3d.png

Разница — это размытие несущественных деталей фотографии, которое позволяет уменьшить размер до 147 КБ. Но этого недостаточно! Пойдем в кодирование JPEG. Предположим, у вас есть последовательный и прогрессивный JPEG.

f5d957dadfb2c3a9ae097d16a4acf87c.png

Последовательный JPEG грузится на странице построчно, прогрессивный — сначала в плохом качестве сразу целиком, а затем качество постепенно улучшается.

Если посмотреть, как работают кодировщики, то можно увидеть несколько уровней сканирования.

42ae57be30bce6ad7d2cae10e9d101fa.png

Множество разных уровней сканирования находятся в этом файле. Наша цель, как разработчиков, показать сразу детальную информацию об этой картинке. Тогда можно позаботиться о том, чтобы Ships Fast и Shows Soon были с какими-то коэффициентами, которые могут подходить картинке лучше, и тогда уже на первом уровне мы увидим структуру, а не просто нечто размытое. А на втором — практически все.

8bf2075ae90eb431aace97fb926c98de.png

6749afbd854295fdc6b138e19de0975f.png

Существуют библиотеки и утилиты, которые позволяют делать такие трюки: Adept, mozjpeg или Guetzli.

Уровень 3


1f40422f757ef7fdaaaec40655c97dc5.png

Помню, семь-десять лет назад — захотел шрифты, добавил font-face и готово. А сейчас нет, необходимо думать, что я хочу сделать и как необходимо загружать. Итак, какой оптимальный метод выбрать для загрузки шрифтов?

80caeaa32dfe5afba09a5235a4fb1635.png

Мы можем использовать синтаксис font-face, чтобы избежать общих ловушек на этом пути:

6797ba0fe98db23543b2160e3731fc6c.png

Если мы хотим поддерживать лишь более менее нормальные браузеры, то можно написать еще короче:

3de5d65b8941a769d02ebc59fef89bd1.png

Что происходит, когда у нас есть этот font-face в css? Браузеры смотрят, есть ли в body или где-то еще указание на шрифт, и если есть, тогда браузер начинает его загружать. И нам приходится ждать.

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

83cf39971a8a08909c41cf14842c25fc.png

Но разные браузеры действуют по-разному. Есть подходы отображения FOUT и FOIT.

7dc916ca8314b035d8099359456f58fa.png

FOIT (Flash of Invisible Text) — ничего не отображается, пока шрифты не загрузятся.

FOUT (Flash Unstyled Text) — контент отображается сразу с дефолтными шрифтами, а потом загружаются нужные шрифты.

Обычно браузеры ждут загрузки шрифтов три секунды, и если они не успели подгрузиться, то подставляются дефолтные шрифты. Есть браузеры, которые не ждут. Но самое неприятное, что есть браузеры, которые ждут до упора. Так не пойдет! Есть множество различных вариантов, как это обойти. Один из них — CSS Font Loading API. Создаем новый font-face в JS. Если шрифты загружаются, то навешиваем их в соответствующие места. Если не загружаются, навешиваем стандартные.

79cac2363cecac80111b57165308a2e6.png

Также мы можем использовать новые свойства в CSS, например, font-rendering, который позволяет нам эмулировать либо FOIT, либо FOUT, но на самом деле они нам даже не нужны, потому что есть Font Rendering Optional.

ee0f9fda8abfb14397dcb67b2e34c7ad.png

Существует еще один способ — критический FOFT с Data URI. Вместо загрузки через JavaScript API веб-шрифт внедряется непосредственно в разметку как встроенный Data URI.

Двухступенчатый рендеринг: сначала римский шрифт, а потом остальные:

  • Загрузка полных шрифтов со всеми весами и стилями
  • Минимальное подмножество шрифтов (A-Z, 0–9, пунктуация)
  • Используйте sessionStorage для обратных посещений
  • Загрузите подстрочный шрифт (Roman) в первую очередь


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

Я подумал, что можно сделать еще лучше. Вместо использования sessionStorage мы встраиваем веб-шрифт в разметку и используем Service Workers.

Например, у нас есть какой-то шрифт, но он весь нам не нужен. И мы делаем не то, чтобы subsetting, а именно выбираем, что для данной страницы нужно. Например, берем italic, уменьшаем его, сначала подгружаем его, отображаем на странице, и он будет выглядеть как normal, bold будет как normal, все будет как normal. Потом подгружается все как нужно. Далее делаем subsetting и отправляем это в Service Workers.

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

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

5a0ba67beae0179425ab571317566452.png

Изображение выше демонстрирует результаты исследования 2007 года, где говорится о том, что 40–60% пользователей имеют пустой кэш, и 20% всех просмотров страниц происходят с пустым кэшем. Почему так? Потому что браузеры не умеют кэшировать? Нет, просто мы посещаем большое количество сайтов и если бы все кэшировалось, то накопитель ПК или смартфона заполнился бы очень быстро.

Браузеры удаляют из кэша то, что считают уже не нужным.

b2d50a7c1847d822a6a2a205959e1dbc.png

Давайте посмотрим на примере Chrome, что происходит в нем, когда мы пытаемся открыть какую-либо страницу в Сети. Если посмотреть на строку fonts, видно, что шрифты оказываются в memory cache или HTTP cache в лучшем случае в 70% случаев. На самом деле это неприятные цифры. Если шрифты загружаются каждый раз заново, каждый раз пользователь приходит на сайт и наблюдает смену стиля шрифтов. С точки зрения UX не очень хорошо.

Необходимо заботиться о том, чтобы шрифты действительно оставались в кэше. Раньше мы полагались на local storage, а сейчас более разумно полагаться на Service Workers. Потому что если я положил что-то в Service Workers, то оно там и будет.

Что еще можно сделать? Можно использовать unicode-range. Многие думают, что происходит динамический subsetting, то есть у нас есть шрифт, он динамически разбирается, и подгружается только указанная часть в unicode-range. На самом деле это не так, и загружается весь шрифт.

cf30010cecf7ef2a6604ae077398a00f.png

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

Что можно еще сделать? Есть классная вещь, которую необходимо использовать всегда и везде — preload.

b7cb6976e44174ad4f73f4e9c41dfee4.png

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

Мы также можем использовать font-display: optional. Это новое свойство в css. Как оно работает?

1346204758e3f9f5e34c7fdc4e89c787.png

У font-display есть несколько значений. Давайте начнем с block. Данное свойство устанавливает блокировку шрифта на три секунды, в течение которых шрифт подгружается, потом происходит замена шрифта и далее непосредственно его отображение.

Свойство swap работает почти так же, но за некоторым исключением. Браузер сразу отрисовывает текст запасным шрифтом, а когда загрузится указанный, то произойдет замена.

5c1e3ca00274ffd9e186bcf5399dad7b.png

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

И наконец мы подошли к optional. Период блокировки равен 100 мс, если за это время шрифт не подгрузился, то текст отображается сразу. Если у вас медленное соединение, то браузер может перестать загружать шрифт. Когда шрифт загрузится, то все равно вы будете видеть дефолтный шрифт. Чтобы увидеть прописанный шрифт, необходимо перезагрузить страницу.

Уровень 4


a7d535861a58591160adb09b3c482c16.png

Существует множество техник, которые мы использовали до появления http/2, например, конкатенация, спрайты и т.д. Но с появлением http/2 необходимость их использования отпала, потому что в отличии от http/1.1, в новой версии грузится почти все сразу, и это здорово, потому что можно использовать множество дополнительных возможностей.

В теории переход на http/2 обещает нам на 64% (на 23% на мобильных) более быструю загрузку страниц. Но на практике все работает медленнее.

Если большая часть вашей целевой аудитории постоянно заходит на ресурс, находясь в автобусе, машине и т.д., то вполне возможно, что http/1.1 окажется в более выгодном положении.

Посмотрите на результаты тестов ниже. По ним видно, что в некоторых ситуациях http/1.1 оказывается быстрее.

1f6bc56319acdf40c4e7fc0599f94771.png

Есть замечательные фичи у http/2, например, HPACK, которую нужно использовать всегда и везде, а еще — server push. Но есть небольшая проблема. Возникает она в зависимости от браузера и сервера. Допустим, загружаем страницу, у нас нет никакого server push.

d35fbcfa9e8d89d02adc0ae8e0906cc5.png

Если происходит повторная загрузка страницы, то все находится в кэше.

aba1b4684f1b1be54f0949172dfb06a5.png

А вот если мы сделаем server push, то наши css дойдут до пользователя гораздо быстрее.

047cbaf5272d5a8f82b68a10dc8a6a10.png

Но также это значит, что даже если css в кэше, они все равно будут пересылаться.

517914c5ed6cd06f2f2bc40cafeac451.png

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

Идем дальше. Есть некоторые рекомендуемые рамки по времени загрузки страниц. Например, для средненького аппарата на android оно составляет пять секунд. Это не так много, если учитывать, что у нас, например, 3G.

8f5890d77ec770024744f1aec70890b1.png

Если посмотреть на рекомендуемое ограничение размера загружаемых файлов, необходимое для начала рендеринга, которое упоминает Google, то оно составляет 170 КБ.

f15bda3401617654e0ba961ef8dd0500.png

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

Существуют различные возможности загружать файлы, например, классический способ, который немного устарел — scout. Мы заводим файл scout.js, он находится в html, мы его подгружаем. Его задача — сделать остальное окружение максимально кэшируемым и при этом своевременно сообщать об изменениях в нем.

b5419ce57479334395425d0e6cdc6c51.png

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

А что делать с http/2? Ведь мы знаем, что можем посылать сколько угодно файлов и нет необходимости объединять их в пакеты. Давайте тогда грузить по 140 модулей, почему нет? На самом деле это очень плохая идея. Во-первых, если у нас есть множество файлов, и мы не используем библиотеку, например gzip для компрессии, то файлы будут большего размера. Во-вторых, браузеры еще не оптимизированы для таких рабочих процессов. В итоге мы начали экспериментировать и искать подходящее количество, и оказалось, что оптимально отправлять примерно 10 пакетов.

Пакеты лучше комплектовать, опираясь на частоту обновлений файлов: часто обновляемые в одних пакетах, а редко обновляемые в других, чтобы избежать лишних загрузок. Например, библиотеки паковать вместе с утилитами и т.д. Ничего особенного. А что же делать с css, как загружать его? Server push тут не подойдет.

В начале мы все загружали как минимизированные файлы, потом подумали, что часть необходимо загружать в критический css, ибо у нас всего 14 КБ, и их необходимо загрузить как можно быстрее. Начали делать loadCSS, писать логику, потом добавили display: none.

12bafe8caea7d5f3a1f480c1bdd242f0.png

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

216389be1b3f7372f561b03eed6a0ca2.png

Необычно! Данный вариант работает хорошо в Chrome, плохо в IE, в Firefox работа немного замедлялась, так как они поменяли рендеринг. Таким образом мы улучшили скорость работы на 120 мс.

Если посмотреть на работу с прогрессивным css и без. То с прогрессивным css все подгружается быстрее, но по частям, а вот без его использования медленнее, т.к. css располагается в header и блокирует страницу как js.

Уровень 5


38ff72b4c991b8993f5f01eb98de57cb.png

И последний уровень, о котором я не могу не рассказать — Resource Hints. Это замечательная функция, которая позволяет делать множество полезных вещей. Пройдемся по некоторым из них.

Prefetch


Prefetch — указывает браузеру, что тот или иной файл нам скоро потребуется, и браузер грузит его с низким приоритетом.




1b8f2ff8eefeb6098b68ae7909dc1d32.png

Prerender


Prerender — данной функции уже нет, но она помогала раньше делать пререндер страницы. Возможно, у нее появится альтернатива…




fd79d762f92ea282985342d81ca462dc.png

Dns-prefetch


Dns-prefetch также ускоряет процесс загрузки страниц. Использование dns-prefetch предполагает, что браузер заранее подгружает адрес сервера указанного доменного имени.




c9bfd8ed3bbd6f0f0d0a95c0c0e90a41.png

Preconnect


Preconnect позволяет делать предварительный хэндшейк с указанными серверами.




1b5acec8f1e54087fe3196f2c1c63bbf.png

Preload


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




62cc961e6b8295df4c2cefe00d33d3fa.png

Помню в 2009 году прочитал статью «Gmail for Mobile HTML5 Series: Reducing Startup Latency», и она поменяла мои взгляды на классические правила. Посмотрите сами! У нас есть JS-код, но ведь он нам весь сейчас не нужен. Так почему нам большую часть JS-кода не закомментировать, а потом, когда нужно, раскомментировать и выполнить в eval?

5916f5a4de844132d8f96b6529650039.png

И причина, по которой они так сделали, кроется в том, что у среднего смартфона время парсинга в 8–9 раз больше, чем у последнего iPhone.

6762ad8a2721e73247c362836e1adf59.png

Давайте обратимся к статистике. Чтобы сделать парсинг 1 МБ кода на среднем телефоне, нужно 4 с.

d2c27ba811a5875ad9114e64bcf94047.png

Это очень много! Но нам не нужен 1 МБ сразу. Если опять обратиться к статистике, выяснится, что сайты используют всего лишь 40% JS-кода из того, что они загрузили.

e306b75d4bbb6e5a605fdd516c843940.png

И мы можем использовать preload взамен eval для таких же ситуаций.

var preload = document.createElement("link"); 
link.href= "myscript.js" ;
link.rel= "preload"; 
link.as= "script"; 
document.head.appendChild(link); 


То есть мы храним файл в кэше, а потом, когда нужно, мы добавляем его на страницу.
Итак, это лишь половина того, чем планировал поделиться Виталий Фридман. Остальные фишки и лайфхаки будут в расшифровке его второго выступления на Holy JS 2017 Moscow, которую мы также подготовим и выложим в нашем блоге.

И если вы любите изнанку JS так же, как и мы, наверняка вам будут интересны вот эти доклады на нашей майской конференции HolyJS 2018 Piter, ключевым из которых мы опять поставили рассказ Виталия Фридмана:

© Habrahabr.ru