Хабрахабр

Оптимизируем веб с Виталием Фридманом, — компрессия, картинки, шрифты, фичи HTTP/2 и Resource Hints

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

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

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

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

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

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

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

Но сейчас не все браузеры его поддерживают, а нам необходима полная поддержка. В будущем мы сможем использовать 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 часто используется для сжатия на лету, но при этом является хорошей альтернативой для однократного сжатия статического содержимого.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В итоге их главная страница с 40 изображениями с применением данной техники заняла 450 КБ. Впечатляет!

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

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

Вот несколько примеров:




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

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

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

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

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

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

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

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

Уровень 3

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

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

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

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

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

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

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

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

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

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

Вместо загрузки через JavaScript API веб-шрифт внедряется непосредственно в разметку как встроенный Data URI. Существует еще один способ — критический FOFT с 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, если коротко. Далее проверяем есть ли он и если есть, то сразу берем его оттуда, а если нет, то все эти действия происходят заново.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Уровень 4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Уровень 5

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

Prefetch

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

<link rel="prefetch" href="(url)">

Prerender

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

<link rel="prerender" href="(url)">

Dns-prefetch

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

<link rel="dns-prefetch" href="(url)">

Preconnect

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

<link rel="preconnect" href="(url)">

Preload

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

<link rel="preload" href="(url)" as="(type)">

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

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

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть