Перевод статьи JonLuca De Caro: "10x Performance Increases: Optimizing a Static Site"
Пару месяцев назад я отдыхал за границей и захотел показать другу ссылку на личный сайт - статический веб-ресурс. Открытие этого сайта и навигация по нему заняли куда больше времени, чем я предполагал. И это было странно, потому что динамическими на сайте были только анимации и отзывчивая верстка, а сам контент не менялся.
Я был неприятно удивлен результатом: примерно 4 сек. заняла загрузка HTML-файла (DOMContentLoaded) и 6.8 сек. загрузка целой страницы. За это время было зарегистрировано 20 запросов к серверу, в результате исполнения которых было передан 1 Мб данных. В Лос-Анджелесе при скорости 1 Гбит/с и при минимальном числе задержек данные с сервера в Сан-Франциско загружались практически моментально. В Италии на скорости 8 Мбит/с картина переставала быть такой радужной.
Именно тогда я впервые задумался об оптимизации. До этого времени всякий раз, когда я хотел добавить библиотеку или сторонний ресурс, я просто скачивал его или загружал по ссылке через src=”…”. И совершенно никакого внимания не обращал на производительность, кэширование, время компиляции и "ленивую загрузку".
Тогда я обратился к опыту тех людей, которые уже сталкивались с похожей проблемой. К несчастью, информация по оптимизации статических сайтов не по-божески быстро устарела - в статьях 2010 - 2011 годов предлагались библиотеки или решения, которые в наше время уже не работают, причем зачастую эти рекомендации сводились к набору кочующих из статьи в статью рецептов.
К счастью, мне удалось найти два действительно полезных источника — High Performance Browser Networking и похожий опыт Дэна Лу (Dan Luu) по оптимизации статических сайтов. И хотя я не зашел так далеко, как Дэн, в сжатии кода и контента, мне удалось добиться 10-кратного увеличения скорости загрузки: HTML-файл (DOMContentLoaded) теперь загружается за пятую долю секунды (197 ms), а вся страница за 388 ms (цифры не совсем точные, потому что не учитывается "ленивая загрузка", о чем расскажу ниже).
Начало отладки
Начать нужно было с профилирования ( — сбора характеристик работы программы, таких как время выполнения отдельных фрагментов — прим. пер.). Я хотел выяснить, на что уходило больше всего времени и каким образом лучше распараллелить загрузку. Я использовал много сервисов для оценки и тестирования своего сайта из разных мест по всему миру, в том числе:
https://tools.keycdn.com/speed
https://developers.google.com/web/tools/lighthouse/
https://developers.google.com/speed/pagespeed/insights/
https://webspeedtest.cloudinary.com/
Некоторые из них предлагали советы по улучшению производительности, но всё, что можно сделать со статическим сайтом с 50 запросами к серверу - удалить из верстки gif-картинки, которые когда-то использовались в качестве разделителя (наследие 90-х) и избавиться от лишних фалов библиотек (из 6 загруженных шрифтов я использовал только 1).
Я хотел улучшить все, что было возможно — от контента и скорости JavaScript до конфигурации веб-сервера (Nginx) и настроек DNS.
Методы оптимизации
Сжатие и объединение файлов
Первое, что я заметил, это десятки запросов к разным сайтам (в том числе по https) для подгрузки содержимого CSS и JS-файлов, и для каждого из них создавалось новое соединение, потому что HTTP keepalive — повторное использование соединений — настроено не было. Это создало целую карусель повторных обращений к различным CDN и серверам, а некоторые JS-файлы запрашивали другие, что вызвало блокирующий каскад, рассмотренный выше.
Я использовал webpack для объединения всех ресурсов в один файл js. Каждый раз, когда я вношу изменения в срипт, он автоматически минимизируется и запаковывает все зависимости в один файл.
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const ZopfliPlugin = require("zopfli-webpack-plugin");
module.exports = {
entry: './js/app.js',
mode: 'production',
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
}, {
test: /(fonts|images)/,
loaders: ['url-loader']
}]
},
plugins: [new UglifyJsPlugin({
test: /\.js($|\?)/i
}), new ZopfliPlugin({
asset: "[path].gz[query]",
algorithm: "zopfli",
test: /\.(js|html)$/,
threshold: 10240,
minRatio: 0.8
})]
};
Я поиграл с различными параметрами — сейчас этот единый файл bundle.js находится в <head> сайта и блокирует рендеринг страницы. Его итоговый размер составляет 829 Кб, включая шрифты, css, библиотеки с зависимостями, а также js-файлы — то есть абсолютно все assets, кроме изображений. Подавляющее большинство из них — шрифты Font Awesome, которые занимают 724 из 829 Кб.
Я прошелся по коду библиотеки шрифтов Font Awesome и оставил только 3 иконки, которые реально используются на сайте: fa-github, fa-envelope и fa-code. Вытащить только нужные иконки мне помог сервис fontello. После этого размер файла стал составлять всего лишь 94 Кб.
В том режиме сборки, который я использую сейчас, только таблицей стилей не достаточно для корректного отображения сайта, поэтому я смирился с тем, что загрузка единого большого файла bundle.js будет немного тормозить отрисовку контента. Время загрузки составляет ~ 118 мс, что на порядок больше указанного ранее числа.
Но у этого решения есть дополнительное преимущество — больше не нужно обращаться к сторонним ресурса или CDN, поэтому отпадает необходимость в:
1) отправке DNS-запрос к этому стороннему ресурсу;
2) выполнении https-рукопожатия;
3) ожидании полной загрузки всех запрошенных у этого ресурса файлов.
Хотя CDN и распределенное кэширование можно считать полезными для оптимизации широкомасштабных распределенных сайтов, для моего маленького статического сайта это не нужно. Дополнительные сто миллисекунд или около того — это выгодный компромисс.
Сжатие ресурсов
Я загружал портрет размером 8 мб, а затем отображал его в масштабе 1:10. Таким образом я не просто пренебрегал оптимизацией, а небрежно злоупотреблял пропускной способностью пользовательской сети.
Тогда я подверг компрессии все свои изображения с помощью https://webspeedtest.cloudinary.com/ , который также предложил конвертировать все в webp, но я хотел добиться совместимости с максимальным количеством браузеров, поэтому оставил jpg. Можно было бы настроить отображение webp только в браузерах, которые поддерживают этот формат, но простоты ради я решил не добавлять дополнительный уровень абстракции.
Улучшение веб-сервера - HTTP2, TLS и многое другое
Первым шагом в этом направлении стал переход на https - в самом начале я запускал сайт на Nginx на 80 порте, файлы лежали в папке /var/www/html
server{
listen 80;
server_name jonlu.ca www.jonlu.ca;
root /var/www/html;
index index.html index.htm;
location ~ /.git/ {
deny all;
}
location ~ / {
allow all;
}
}
Для начала я настроил https и редирект всех HTTP-запросов с http на https. Для этого получил TLS-сертификат в Let's Encrypt (отличная организация, которая только начала выпускать wildcard-сертификаты!).
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jonlu.ca www.jonlu.ca;
root /var/www/html;
index index.html index.htm;
location ~ /.git {
deny all;
}
location / {
allow all;
}
ssl_certificate /etc/letsencrypt/live/jonlu.ca/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/jonlu.ca/privkey.pem; # managed by Certbot
}
Просто добавив директиву http2, Nginx смог воспользоваться всеми преимуществами новейших функций HTTP. Обратите внимание: для доступа к плюшкам HTTP2 (ранее SPDY) нужно использовать HTTPS. Об этом подробнее здесь.
Вы также можете использовать фичу HTTP2 push с изображениями http2_push/Headshot.jpg;
Примечание: включение gzip и TLS делает ваш сайт уязвимым для BREACH. Поскольку мой сайт статический, и реальные риски BREACH невелики, оставлять компрессию я не боялся.
Использование кэширования и сжатия
Что еще можно сделать с помощью одного только Nginx? Первое, что приходит на ум, это включить кэширование и сжатие gzip.
Передаваемый раньше файл HTML компрессии я не подвергал . Одна только строчка gzip on смогла на целых 50% уменьшить размер передаваемых данных с 16000 байт до 8000 байт.
На самом деле можно добиться еще большего процента сжатия - если в настройках Nginx установить режим gzip_static on, сервер будет заранее искать предварительно сжатые версии всех запрошенных файлов. В этом нам помогут упомянутые ранее настройки webpack, а для предварительного сжатия всех наших файлов прямо во время сборки мы можем использовать ZopflicPlugin! Что здорово экономит вычислительные ресурсы и позволяет нам добиться максимальной компрессии не в ущерб скорости.
Кроме того, мой сайт меняется довольно редко, поэтому я хотел, чтобы кэшированые копии ресурсов хранились в браузерах как можно дольше. В результате чего при последующих посещениях сайта пользователям не нужно будет повторно загружать все assets (особенно bundle.js).
Моя обновленная конфигурация сервера выглядит так. Обратите внимание, что описание части настроек (TCP, директивы gzip и кэширование файлов) я здесь опустил. Если вы хотите узнать о них подробнее, прочитайте эту статью о настройке Nginx.
worker_processes auto;
pid /run/nginx.pid;
worker_rlimit_nofile 30000;
events {
worker_connections 65535;
multi_accept on;
use epoll;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Turn of server tokens specifying nginx version
server_tokens off;
open_file_cache max=200000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
include /etc/nginx/mime.types;
default_type application/octet-stream;
add_header Referrer-Policy "no-referrer";
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /location/to/dhparam.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload';
ssl_certificate /location/to/fullchain.pem;
ssl_certificate_key /location/to/privkey.pem;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
gzip_min_length 256;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
И соответствующий блок кода для сервера:
server {
listen 443 ssl http2;
server_name jonlu.ca www.jonlu.ca;
root /var/www/html;
index index.html index.htm;
location ~ /.git/ {
deny all;
}
location ~* /(images|js|css|fonts|assets|dist) {
gzip_static on; # Tells nginx to look for compressed versions of all requested files first
expires 15d; # 15 day expiration for all static assets
}
}
"Ленивая загрузка"
Наконец, еще одна хитрость позволила мне немного выиграть в скорости. Есть 5 изображений, которые не видны, пока вы не нажмете на соответствующие вкладки, но загружались они одновременно с остальным контентом (из-за того, что они находятся в теге <img src = "...">.
Я написал короткий скрипт для модификации атрибута всех элементов класса lazyload. Теперь изображения загружаются только после нажатия на соответствующую вкладку.
$(document).ready(function() {
$("#about").click(function() {
$('#about > .lazyload').each(function() {
// set the img src from data-src
$(this).attr('src', $(this).attr('data-src'));
});
});
$("#articles").click(function() {
$('#articles > .lazyload').each(function() {
// set the img src from data-src
$(this).attr('src', $(this).attr('data-src'));
});
});
});
Таким образом, как только документ полностью загрузится, этот скрипт найдет теги <img>, заменит <img data-src = "..."> на <img src = "..."> и подгрузит изображения в фоновом режиме.
Идеи на будущее
Есть еще несколько способов повысить скорость загрузки страницы — в первую очередь, можно использовать Service Workers для кэширования и перехвата всех запросов, а также для работы сайта даже в режиме offline и кэширования содержимого на CDN, чтобы пользовательским браузерам не приходилось отправлять запросы к серверу в далекий Сан-Франциско. Все это стоит попробовать, но делать этого я не буду, потому что для такого простого сайта-визитки, как мой, это не так критично.
Вывод
Все описанные меры позволили мне сократить время загрузки страницы с более чем 8 секунд до ~ 350 мс при первой загрузке страницы и до безумных ~ 200 мс для повторных загрузок. Я настоятельное рекомендую статью High Performance Browser Networking — читается она довольно быстро, на пальцах объясняет принципы работы современного интернета и предлагает все доступные возможности оптимизации сайтов.
Статью перевела Журавлева Дарья
Привет, это редакция канала Nuances of programming!
Если тебе понравилась статья - ставь лайк и подписывайся, чтобы не упустить новые материалы.
Кстати, наш телеграм-канал: https://t.me/nuancesprog