Рассказываю об оптимизации скорости загрузки сайта на минималках, также рассмотрю некоторые более традиционные методы/технологии.
Скорость или что понимать под "скоростью"
Согласен с теми, кто считает, что современный Lighthouse (находится под капотом PageSpeed Insights) очень неплохо приближен к восприятию веб сайта пользователем в разрезе скорости.
Следовательно, для ускорения сайта достаточно данных от Page Speed Insights, и только по мобилам (если это оптимизируем, то на десктопе всё будет нормально). Суть работы над скоростью:
Оценка скорости
Что нас интересует в отчёте тестера Гугла, в порядке значимости:
- Время до получения первого байта;
- Минимизируйте работу в основном потоке;
- Сократите время выполнения кода JavaScript;
- Уменьшите влияние стороннего кода;
- Избегайте длительных задач в основном потоке;
- Устраните ресурсы, блокирующие отображение;
- Отложите загрузку скрытых изображений;
- Включите сжатие текста;
- Уменьшите размер кода JavaScript;
- Уменьшите размер кода CSS;
В п.п. 2-6 нас интересуют показатели в милли-секундах. Они говорят о том, сколько времени тратит мобильный тестовый браузер на определённые ресурсы или типы задач.
TTFB
Время до получения первого байта. Это итоговый вердикт по серверной оптимизации сайта. Стремимся к зелёной зоне, для разных типов страниц. Пример:
Гугл вещает о значении до 600 мс (хотя выше на той же странице скриншот с красной зоной для 330 мс), лучше стремиться к <= 200 мс.
Если с TTFB проблемы, надо выяснить, почему. Либо дело в хостинге, либо в серверных настройках/компонентах сайта (движок/база данных/и т.д.).
Хостинг
Бывает, что хостинг медленный, и не тянет сайт. Если это из-за нехватки серверных ресурсов, то можно попробовать перейти на более лучший тарифный план (виртуальный хостинг) или увеличить параметры железа (VPS/VDS). Также, если хостинг провайдер буржуйский, то могут быть лаги просто из-за удалённого расположения сервера с сайтом.
SSD
При прочих равных, SSD хостинг выигрывает у обычного HDD просто засчёт физики процесса чтения/записи:Поэтому, по логике лучше всегда использовать SSD.
Коробочные популярные системы управления сайтом (CMS)
Если сайт сделан на неплохой CMS, без подключения прожорливых надстроек/плагинов, или, наоборот, с подключением таких, которые реально улучшают производительность CMS'ки, то без явной необходимости лезть в код движка чтобы его ускорить, неоправданно — усилия/расходы, если и принесут результат, то он их не отобъёт.
Следовательно, вся "оптимизация" направлена на использования минимума соответствующего "обвеса" и избегания плохого кода (при допиливании недостающего функционала).
Конструкторы
Например, Tilda, Wix и прочее. Не могу ничего сказать по их качеству в разрезе скорости. По идее, должно быть примерно так, как в случае с CMS.
Оптимизация JS
JS это основная причина "низкой скорости" (понижения баллов за скорость в тестере Гугла), причём это направление оптимизации несопоставимо по значимости с прочими факторами.
Какие пути решения:
- Максимально оттянуть разбор и выполнение "второстепенного" JS;
- Грузить JS в определённой последовательности (чем меньше одновременных операций производит JS, тем выше отзывчивость страницы).
- В идеале отказ от лишнего JavaScript (без чего сайт не особо потеряет), но можно и без этого, если предыдущие п.п. неплохо проработаны.
Разделение JS, скрипт управления
Цель — сделать так, чтобы браузер не только не выполнял JS, но даже не разбирал бы.
В идеале следует использовать продвинутый подход (раз, два), однако, на практике работает нечто значительно более простое: управляющий скрипт с атрибутом async запускается как можно раньше и контролирует загрузку и выполнение всего JS кода с помощью плагина управления зависимостями (далее рассмотрим).
Управляющий скрипт также можно вбить inline (если он небольшой и шустрый).
Пример запуска:
<script async src="/assets/js/bundle_critical.min.js."></script>
</head>
Или:
<script>
// содержимое bundle_critical.min.js
</script>
</head>
Что тут понимается под "разделением" JS: разнос по разным файлам, которые догружает управляющий файл. Если не разносить, тогда браузер всё равно будет разбирать код, хоть и не выполнять. Поэтому, разносим, догружаем и выполняем позже — только тогда, когда и если понадобится соответствующий код.
Получение данных с бэкенда
Часто JS функционал страницы зависит от её типа. То есть, на некоторых типах страниц мы можем не грузить лишнее (например, интерактив для товарного фильтра, баннера, виджета комментариев и т.д.). Поэтому, есть смысл сделать передачу данных с бэкенда в объект данных JS.
Пример задания шаблона (типа) страницы на бэкэнде (для Evolution CMS 1.X):
<script>window.mysite = { template: [*template*] }</script>
Пример функции в критическом JS (вверху кода — для удобства редактирования массивов):
// для обновления данных в глобальном объекте
function updateObj() {
// объект задан в ЦМС, и уже существует
Object.assign(window.mysite, {
// шаблоны, где используется интерактивный фильтр
aTplsFilter: [5],
// шаблоны, где используется интерактив слайдера
aTplsSlider: [1],
// шаблоны, где используются комментарии
aTplsComments: [8, 10, 11],
// метод проверки текущего шаблона
checkTplArray: function(arr) {
return arr.includes(this.template) ? true : false;
}
});
}
Тут предусмотрен отложенный вызов функции (всё для того же — выиграть что-то по скорости), поэтому сделана именной. Далее, мы на каком-то этапе её выполняем, после чего, сопоставляя тип страницы и нужный функционал, отсекаем лишний код. Например:
// функционал фильтра
(function() {
var obj = window.mysite;
if ( !obj.checkTplArray(obj.aTplsFilter) ) return;
/* шаблон прошёл проверку, можно загрузить плагин фильтра
из другого файла */
})();
В данном примере, JS код для фильтра можно впихнуть в эту IIFE, однако тогда браузер всё равно его будет разбирать, поэтому лучше вынести в отдельный файл и подключать библиотекой управления зависимостями.
Плагин (библиотека или либа) управления зависимости
Рекомендую одну из двух либ:
- LoadJS — (субъективно) более удобная, т.к. есть поддержка загрузки CSS-файлов, и методы обратного вызова для более тонкой настройки, но нет поддержки старых браузеров;
- $script.js — без плюсов первой, но зато с поддержкой IE 6+.
Оба плагина:
- обеспечивают параллельную загрузку js файлов — всегда;
- параллельное (по умолчанию) или последовательное их выполнение — ставить ли тегам script атрибут async;
Интересный приём — последовательное выполнение второстепенных скриптов в сочетании с загрузкой DOM — подключать их к странице только после загрузки ДОМ:
- т.к. DOM уже загружен, асинхронное выполнение скриптов теряет смысл;
- последовательный разбор и выполнение скриптов может обеспечивать меньшие пиковые нагрузки на CPU.
Ну и несколько примеров работы с либой loadjs, включающих в себя и ранее рассмотренное разделение Javascript'а:
/* либа проверки загрузки DOM https://github.com/ded/domready
выполняем её асинхронно, чтобы не блокировать рендеринг страницы */
loadjs('/assets/site/js/ready.min.js', 'domready');
// загрузить цсс
loadjs([
'css!https://fonts.googleapis.com/css?family=Josefin+Sans:300, 400,700|Inconsolata:400,700',
'/assets/site/css/bootstrap.min.css',
'/assets/site/css/style.min.css'
], 'cssTemplate');
// шаблонные скрипты загрузить после цсс
loadjs.ready(['cssTemplate', 'domready'], function() {
domready(function() {
loadjs([
'/assets/site/js/jquery-3.2.1.min.js',
'/assets/site/js/bootstrap.min.js',
'/assets/site/js/main.min.js'
], 'jsTemplate', {
// НЕ устанавливать атрибут async тегам script
async: false,
// добавляем эти скрипты перед закрывающим боди
before: function(path, scriptEl) {
document.body.appendChild(scriptEl);
/* return `false` to bypass default DOM insertion mechanism */
return false;
}
});
});
});
// догрузить ксс
loadjs.ready('jsTemplate', function() {
loadjs('/assets/site/css/mod-rest.min.css', 'cssRest');
});
// слайдер
loadjs.ready('cssRest', function() {
var obj = window.mysite;
if ( !obj.checkTplArray(obj.aTplsSlider) ) {
return loadjs.done('slider');
}
loadjs('/assets/site/js/slider.min.js', 'slider', {async: false});
});
Сторонние системы статистики
Например, Яндекс метрика, Гугол Аналитикс. Настраиваем их запуск из GTM (менеджера тегов Гугла). Вызов скрипта ГТМ лучше отложить, т.к. он лаговый (впрочем, как и ряд других популярных счётчиков). Вместо этого встраиваем в цепочку загрузки с помощью либы. Например (используется вторая из упоминавшихся библиотек):
/* грузим ГТМ по загрузке мобильного меню;
прочие счётчики стартуют из GTM */
$script.ready('mobilenav', function() {
// google tag manager
function getGtmSrc(w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
var dl = l != 'dataLayer' ? '&l=' + l : '';
return 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
};
var gtmSrc = getGtmSrc(window, document, 'script', 'dataLayer', 'GTM-XXXXXX');
$script(gtmSrc, 'gtm');
});
Даже если из-за такого подхода возникнет некоторое искажение в статистике, плевать — скорость гораздо важнее.
Вебвизор 2.0 от Метрики
Лучше отказаться и использовать обычный Вебвизор — настраивается в настройках Яндекс Метрики путём отката на "устаревший" код счётчика (watch.js). На сложных сайтах в тестере Гугла наблюдал пожирание "новым" скриптом Метрики (tag.js) и 800 лишних мс, и 1,5 с и т.д. (разница между работой обычного Вебвизора и 2.0).
В последний раз проверял 2.0 в марте 2019. Однако, тогда же нашёл жалобы от сентября 2018. Получается, спустя 1,5 года (на тот момент) косяк с пожиранием Вебвизором ресурсов был по-прежнему не исправлен. Это даже для беты перебор. Для себя сделал вывод пока держаться подальше от 2.0.
На мобилах не грузим лишний JavaScript
Критический JavaScript:
window.mysite = {
isMobile: undefined
};
Отложенный JavaScript:
// мобильность
window.mysite['checkMobile'] = function() {
/* отказался от записи результатов в куку для скорости;
кажется, что быстрее выполнить 2 операции, чем грузить
либу для куки */
/* touch
https://stackoverflow.com/a/17326467
https://stackoverflow.com/a/20293441
https://stackoverflow.com/a/32824825
*/
var touchSupport = 'ontouchstart' in document.documentElement;
/* это условие чтобы отсеять десктопные устройства с тачскрином;
ширина экрана (Typical Device Breakpoints)
https://www.w3schools.com/css/css_rwd_mediaqueries.asp
https://stackoverflow.com/a/10364620
более длинно и криво:
https://stackoverflow.com/a/34162696
*/
var smallScreen = window.matchMedia("only screen and (max-width: 760px)").matches;
if (touchSupport && smallScreen) {
this.isMobile = true;
} else {
this.isMobile = false;
}
};
window.mysite.checkMobile();
Далее, проверяя свойство isMobile, не выполняем какой-то код, снимая лишнюю нагрузку с более слабых мобильных CPU.
Выполнение JavaScript при прокрутке страницы
Выполняем JavaScript по мере показа заданного элемента на экране, используя IntersectionObserver или полифил. Рекомендую эту либу — она использует IntersectionObserver, плюс с CDN можно грузить полифил для фолбэка (там прямо в примерах указано).
HTML:
<div class="lazy" data-lazy-function="loadSomePlugin">...</div>
Критический JavaScript:
window.mysite = {
// потом будет наполнятся нужными методами
lazyFunctions: { },
executeLazyFunction: function(element) {
var lazyFunctionName = element.getAttribute("data-lazy-function");
/*
если нет атрибута, то это непосредственно
загружаемый элемент, а не элемент,
который выполняет запуск JS кода
*/
if (!lazyFunctionName) return;
/* this нельзя использовать, потому что при колбеке
от lazyload this получается undefined */
var lazyFunction = window.mysite.lazyFunctions[lazyFunctionName];
// если ф-ция из атрибута не задана в объекте
if (!lazyFunction) return;
lazyFunction(element);
}
};
Отложенный JavaScript:
window.mysite.lazyFunctions['loadSomePlugin'] = function() {
// загружаем какой-то плагин с помощью либы управления зависимостями
};
Примитивная отложка JS
С помощью setTimeout задаём ожидаемое время в секундах, раньше которого точно не понадобится какой-то JavaScript код/функционал, либо задаём setTimeout от события загрузки DOM или страницы. Например:
window.addEventListener('load', function(event) {
setTimeout(function() {
// комбинирование с либой управления зависимостями
loadjs.ready('cssRest', function() {
// выполняем отложенный JS
});
}, 300);
});
Можно также сочетать с выполнением JS по показу элемента — что наступило раньше.
requestIdleCallback
Использование этого метода должно позволять достигать по-настоящему крутых высот в оптимизации скорости JS (и Гугол его жалует), хотя requestIdleCallback и не поддерживается на Иос и Сафари, что сильно его обесценивает и всё равно принуждает к костылям для неподдерживаемых браузеров.
Атрибуты async и defer
Сами по себе не приносят явной пользы, только в сочетании с другими техниками.
Оптимизация CSS
Тут всё просто — разделить на два файла — критически важные стили и прочие.
Критические стили
Добиться нормального отделения критических стилей от прочих как правило нет возможности, потому что такое разделение необходимо на этапе создания шаблона/вёрстки. А обычно шаблон создаётся без оглядки на оптимизацию стилей, и тогда нам проще забить, чем переделывать шаблон с перспективой убить много времени ради небольшого выигрыша по скорости.
И грузим критические стили как можно раньше.
Если после минификации получился небольшой фрагмент, то можно заинлайнить (разместить в style в теге head, как учит Google). Чтобы получилось нечто вроде такого:
<style>
.ui-helper-hidden-accessible{position:absolute!important;clip:rect(1px,1px,1px,1px);}.ui-helper-reset{border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none;margin:0;padding:0;}.ui-helper-clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;}* html .ui-helper-clearfix{height:1%;}
</style>
Или подключаем в head, как обычно:
<link rel="stylesheet" type="text/css" href="/assets/site/css/critical.min.css" />
Или вызываем из JS с помощью плагина LoadJS (выше в примерах был такой вызов).
Отложенные стили
Такие стили грузим асинхронно с помощью JS, встраивая в очередь загрузки/выполнения JS. Удобно использовать упоминавшуюся либу, например:
loadjs([
'/assets/site/css/animate.css',
'/assets/site/fonts/ionicons/css/ionicons.min.css',
'/assets/site/fonts/fontawesome/css/font-awesome.min.css',
'/assets/site/fonts/flaticon/font/flaticon.css'
], 'cssNonCritical');
Дублируем в head с noscript:
<noscript>
<link rel="stylesheet" href="/assets/site/css/animate.css" />
<link rel="stylesheet" href="/assets/site/fonts/ionicons/css/ionicons.min.css" />
<link rel="stylesheet" href="/assets/site/fonts/fontawesome/css/font-awesome.min.css" />
<link rel="stylesheet" href="/assets/site/fonts/flaticon/font/flaticon.css" />
</noscript>
Автоматическое определение критичных стилей
Есть высокие технологии, которые автоматически определяют критические стили. Если их использовать, тогда вроде бы и нет необходимости в рассмотренных выше костылях.
Оптимизация картинок
Отложенная загрузка (lazy load)
Проще всего использовать JS либу, например, вышеупоминавшуюся. В HTML указываем наподобие такого:
<img class="lazy" src="full-sized.jpg" srcset="zaglushka.png" data-srcset="full-sized.jpg" />
После обработки либой получится:
<img class="lazy entered loaded" src="full-sized.jpg" srcset="full-sized.jpg" data-srcset="full-sized.jpg" data-ll-status="loaded" />
Что здесь происходит — браузер загружает заглушку, плагин при прокрутке страницы ставит в srcset данные из data-srcset, браузер пытается подобрать из scrset наиболее подходящее изображение, но т.к. указано одно, оно и берётся.
Такая настройка отложки редко рекомендуется, однако, она лучше, чем:
<img class="lazy" src="" data-src="full-sized.jpg" />
Потому что в атрибуте src у нас всегда есть то изображение, которое хотим скормить Гуглу (Яндекс понимает data-src), а не пустота или заглушка.
Пережатие картинок
Делать либо вручную (например, здесь), либо настроить на бэкэнде. Это второстепенная мера, главное, чтобы была настроена отложка загрузки изображений.
Сжатие текстовых данных
Как правило, это GZIP сжатие, и на многих хостингах оно включено по умолчанию. Если нет сжатия, тестер Гугла об этом сообщит.
Также, иногда полезен такой сервис для быстрой проверки настройки сжатия для разных типов файлов на VPS/VDS — скриптов, стилей, XML-карт (бывает, что админинстратор сервера пропускает некоторые типы файлов).
Минификация скриптов и стилей
Все скрипты и стили (inline и подключаемые), а особенно, критически важные, должны быть минифицированы, желательно. Разумеется, сжимать вручную неудобно, нужно настроить это на сервере (например, установить соответствующий модуль в CMS).
Пример, чего можно добиться
Используя вышеописанные приёмы (исключая экзотику вроде requestIdleCallback и автоподгрузку критических стилей), было достигнуто ускорение хоть и простенького сайта, но в 2 раза по тестеру Гугла (до 80 баллов на мобилах), без какого-либо вмешательства на бэкэнде, с гирляндой неоптимизированного JavaScript'а. Отзывчивость сайта "на глаз" возросла где-то в такой же пропорции.
Прочее
Всё, что хотел сказать по реально полезному по скорости для обычных сайтов, сказал.
HTTP2
Обычная рекомендация — подключать HTTP2, однако есть существенный недостаток:
Вполне допускаю, что в некоторых ситуациях это может ухудшать скорость, а не улучшать.
CDN
Если дело касается загрузки каких-то JavaScript либ, то CDN удобная штука. В остальных случаях без необходимости не использовал бы эту технологию — не факт, что окупит себя в разрезе скорости, плюс есть явные минусы:
Версии серверных компонентов
Например, PHP, Mysql, CMS и т.д. Обычно, желательно обновляться на последние стабильные версии, но это врядли принесёт заметную пользу по скорости.
AMP (Accelerated mobile pages) и Яндекс Турбо
Сразу исключу из рассмотрения такой аспект и его вариации (предположим, что это не соответствует действительности, и вообще, скорость разбираем):
Главный минус означенных технологий — необходимость тратить ресурсы на поддержку 4-х (!) версий сайта, а не двух — мобильной (для простоты сравнения посчитаем адаптивный дизайн отдельной версией) и десктопной.
Однако, мобильную версию можно причесать так, что она будет летать, и посетителю сайта этого хватит с головой. Ну не будет супер-скоростей на мобилах, однако, нужны ли они кому-нибудь? Это вопрос.
Поэтому, мой вывод таков (это только моё мнение, на истину не претендую):
Другое дело, поисковики фактически принуждают их использовать, предоставляя плюшки (например, в плане трафика, более привлекательных сниппетов) тем сайтам, которые их используют. Разумеется, "притеснение" касается не всех сайтов/тематик.
Вместо эпилога
Всё просто — взял готовый неоптимизированный шаблон, покачал головой, забил (хорошо хоть подключил либу loadjs), залил на уже оплаченный бурж хостинг с далёкими датацентрами. Вообщем, типа пока и так сойдёт, не клиентский же сайт :)