[Перевод] Оптимизация статического сайта: десятикратное ускорение
Джонлука Де Каро, автор материала, перевод которого мы сегодня публикуем, однажды оказался в заграничной поездке и захотел показать другу свою личную страничку в интернете. Надо сказать, что это был обычный статический сайт, но, в процессе демонстрации оказалось, что всё работает медленнее, чем можно было бы ожидать.
На сайте не применялось никаких динамических механизмов — там было немного анимации, он был создан с применением методов отзывчивого дизайна, но содержимое ресурса практически всегда оставалось неизменным. Автор статьи говорит, что то, что он увидел, быстро проанализировав ситуацию, буквально привело его в ужас. События DOMContentLoaded
пришлось ждать около 4-х секунд, на полную загрузку страницы ушло 6.8 секунды. В процессе загрузки было выполнено 20 запросов, общий объём переданных данных составил около мегабайта. А ведь речь идёт о статическом сайте. Тут Джонлука понял, что он раньше считал свой сайт невероятно быстрым лишь потому, что привык к гигабитному интернет-соединению с низкой задержкой, используя которое, он, из Лос-Анджелеса, работал с сервером, расположенным в Сан-Франциско. Теперь же он оказался в Италии и воспользовался интернет-соединением на 8 Мбит/с. А это совершенно поменяло картину происходящего.
В этой статье Джонлука Де Каро расскажет о том, как ему удалось ускорить свой статический сайт в десять раз.
Обзор
Вот как выглядел сайт, с точки зрения количества запросов, необходимых для его формирования, времени загрузки и объёма загруженных данных, когда стало понятно, что его надо оптимизировать.
Данные неоптимизированного сайта
Когда я всё это увидел, я впервые занялся оптимизацией своего сайта. До этого момента, если нужно было добавить на сайт библиотеку или что-то ещё, я просто взял бы и загрузил то, что нужно, с помощью конструкции src="…"
. Я совершенно не обращал внимания на производительность, не думал ни о чём, что может на неё повлиять, включая кэширование, встраивание кода, ленивую загрузку.
Тогда я начал искать людей, которые столкнулись с чем-то подобным. К несчастью, публикации, посвящённые оптимизации статических сайтов, очень быстро устаревают. Например, рекомендации, датированные 2010–2011 годами, посвящены использованию библиотек, а в некоторых из них сделаны предположения, касающиеся того, что библиотеками при разработке сайта не пользуются вообще. Анализируя эти материалы, я столкнулся и с тем, что некоторые из них попросту снова и снова повторяют одни и те же наборы правил.
Однако, я всё же нашёл два отличных источника информации: ресурс High Performance Browser Networking и публикацию Дэна Луу. Хотя я и не пошёл в деле оптимизации так же далеко, как Дэн, разбирая содержимое сайта и средства его форматирования, мне удалось ускорить загрузку моей страницы примерно в десять раз. Теперь ждать DOMContentLoaded
приходилось примерно пятую часть секунды, а на полную загрузку страницы было нужно 388 мс (надо сказать, что эти результаты нельзя назвать абсолютно точными, так как на них отразилась ленивая загрузка, о которой речь пойдёт ниже).
Результаты оптимизации
Теперь поговорим о том, как удалось достигнуть таких результатов.
Анализ сайта
Первым шагом процесса оптимизации стало профилирование сайта. Мне хотелось понять, что именно требует больше всего времени, и как лучше всего распараллелить выполнение задач. Я, для профилирования сайта, использовал различные инструменты и проверил то, как сайт загружается из разных мест Земли. Вот список ресурсов, которыми я пользовался:
- https://tools.pingdom.com
- http://www.webpagetest.org/
- https://tools.keycdn.com/speed
- https://developers.google.com/web/tools/lighthouse/
- https://developers.google.com/speed/pagespeed/insights/
- https://webspeedtest.cloudinary.com/
Некоторые из этих ресурсов давали рекомендации по улучшению сайта. Однако, когда для формирования статического сайта требуется несколько десятков запросов, сделать можно очень много всего — от трюков с gif-файлами, пришедших из 90-х годов, до избавления от неиспользуемых ресурсов (я, например, загружал 6 шрифтов, хотя использовал лишь один из них).
Временная шкала для сайта, очень похожего на мой. Я вовремя не сделал скриншот для своего сайта, но то, что тут показано, выглядит практически так же как то, что я увидел несколько месяцев тому назад, анализируя свой сайт
Мне хотелось оптимизировать всё, на что я могу повлиять — от наполнения сайта и скорости выполнения скриптов, до настроек веб-сервера (Nginx в моём случае) и DNS.
Оптимизация
▍Минификация и объединение ресурсов
Первым, что я заметил, было то, что страница выполняет множество запросов на загрузку CSS и JS-файлов (при этом не использовались постоянные HTTP-соединения), которые ведут к различным ресурсам, работа с некоторыми из которых велась по HTTPS. Всё это означало, что ко времени загрузки данных добавлялось время, необходимое для прохождения многочисленных запросов к сетям доставки контента или к обычным серверам, и для получения ответов от них. При этом некоторые JS-файлы запрашивали загрузку других файлов, что и приводило к ситуации каскадов блокировок, показанной на рисунке выше.
Для того чтобы объединить всё необходимое в один JS-файл, я использовал webpack. Каждый раз, когда я вносил какие-то изменения в содержимое страницы, webpack автоматически минифицировал и собирал все зависимости в один файл. Вот содержимое моего файла webpack.config.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
, блокирующая загрузка которого выполняется в разделе моего сайта. Размер этого файла составлял 829 Кб. Сюда входило всё кроме изображений (шрифты, CSS, все библиотеки и зависимости, а также мой JS-код). Большая часть этого объёма, 724 из 829 Кб, приходилась на шрифты font-awesomе.
Далее, я поработал с библиотекой Font Awesome и убрал оттуда всё, кроме трёх иконок, которые использовал: fa-github
, fa-envelope
, и fa-code
. Для того чтобы извлечь из библиотеки только нужные мне иконки, я использовал сервис fontello. В результате мне удалось сократить размер загружаемого файла всего до 94 Кб.
То, как спроектирован сайт, не позволяет вывести его правильно, если браузер обработает только HTML и CSS, поэтому я не пытался бороться с блокирующей загрузкой файла bundle.js
. Время загрузки составило примерно 118 мс, что было более чем на порядок лучше, чем раньше.
У такого подхода обнаружились и несколько дополнительных полезных свойств. Так, мне теперь не приходилось обращаться к сторонним серверам или сетям доставки контента, в результате, когда моя страница загружалась в браузер, система освобождалась от выполнения следующих задач:
- Выполнение DNS-запросов к сторонним ресурсам.
- Выполнение процедур установки HTTP-соединений.
- Выполнение полной загрузки необходимых данных.
В то время как использование CDN и распределённое кэширование могут иметь смысл для масштабных веб-проектов, мой маленький статический сайт от использования CDN-ресурсов ничего не выигрывает. Отказ от них, ценой некоторых усилий с моей стороны, позволил дополнительно сэкономить что-то около сотни миллисекунд, что, на мой взгляд, оправдывает эти усилия.
▍Сжатие изображений
Продолжая анализ сайта, я обнаружил, что на страницу загружался мой 8-мегабайтный портрет, который выводился в размере, составляющем 10% по ширине и высоте от его исходного размера. Это — не просто недостаточная оптимизация. Такие вещи демонстрируют почти полное безразличие разработчика к использованию полосы пропускания интернет-канала пользователя.
Оптимизация фотографии
Я сжал все используемые на сайте изображения, используя https://webspeedtest.cloudinary.com/. Там мне была дана рекомендация использовать формат webp, но мне хотелось, чтобы моя страница была совместима с как можно большим количеством браузеров, поэтому я остановился на формате jpeg. Можно настроить всё так, чтобы webp-изображения использовались только в браузерах, которые этот формат поддерживают, но я стремился к максимальной простоте, и решил, что выгоды от дополнительного уровня абстракции, связанного с изображениями, не стоят усилий, затраченных на достижение этих выгод.
▍Улучшение веб-сервера: HTTP2, TLS и кое-что ещё
Приступив к оптимизации сервера, я сразу же перешёл на HTTPS. В самом начале я использовал обычный Nginx на 80-м порту, который просто обслуживал файлы из /var/www/html
. Вот код моего исходного файла nginx.conf
.
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-запросов на HTTPS. Я получил TLS-сертификат от Let«s Encrypt (эта замечательная организация, кроме того, недавно начала подписывать и wildcard-сертификаты). Вот обновлённый nginx.conf
.
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 images/Headshot.jpg;
можно воспользоваться push-директивами HTTP2.
Обратите внимание на то, что использование gzip и TLS повышает риск BREACH-атаки на веб-ресурс. В моём случае, так как речь идёт о статическом сайте, риск подобной атаки весьма низок, поэтому я спокойно пользуюсь сжатием.
▍Использование директив кэширования и сжатия
Что ещё можно сделать, пользуясь лишь Nginx? Первое, что приходит в голову — использование директив кэширования и сжатия.
Раньше мой сервер отдавал клиентам обычный, несжатый HTML. Воспользовавшись единственной строкой gzip: on;
, я смог уменьшить размер передаваемых данных с 16000 байт до 8000, а это означает уменьшение их объёма на 50%.
На самом деле, этот показатель можно улучшать и дальше, если воспользоваться директивой Nginx gzip_static: on;
, что позволит системе ориентироваться на использование предварительно сжатых версий файлов. Это согласуется с вышеприведённой конфигурацией webpack, в частности, для того, чтобы организовать предварительное сжатие всех файлов во время сборки, можно воспользоваться ZopfliPlugin. Это экономит вычислительные ресурсы и позволяет максимизировать сжатие, не теряя в скорости.
Кроме того, мой сайт меняется довольно редко, поэтому я стремился к тому, чтобы его ресурсы кэшировались бы на как можно более длительный срок. Это привело бы к тому, что, посещая мой сайт несколько раз, пользователь не нуждался бы в повторной загрузке всех материалов (в особенности — bundle.js
).
Вот к какой конфигурации сервера (nginx.conf
) я в итоге пришёл.
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/*;
}
Обратите внимание на то, что тут не затронуты все ранее выполненные улучшения, касающиеся настроек TCP, директив gzip и кэширования. Если вы хотите узнать обо всём этом подробности — взгляните на этот материал, посвящённый настройке Nginx.
А вот блок настройки сервера из моего nginx.conf
.
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 изображений, которые можно увидеть, лишь щёлкая по соответствующим им миниатюрам. Эти изображения загружались во время загрузки всего остального содержимого сайта (причина тут в том, что пути к ним находились в тегах ).
Я написал небольшой скрипт для модификации соответствующего атрибута каждого элемента с классом lazyload
. В результате эти изображения теперь загружаются лишь при щелчке по соответствующим им миниатюрам. Вот как выглядит файл lazyload.js
:
$(document).ready(function() {
$("#about").click(function() {
$('#about > .lazyload').each(function() {
// установка src для элемента img на основании data-src
$(this).attr('src', $(this).attr('data-src'));
});
});
$("#articles").click(function() {
$('#articles > .lazyload').each(function() {
// установка src для элемента img на основании data-src
$(this).attr('src', $(this).attr('data-src'));
});
});
});
Этот скрипт обращается к элементам , устанавливая
на основании
, что позволяет загружать изображения тогда, когда они нужны, а не во время загрузки основных материалов сайта.
Дальнейшие улучшения
У меня есть на примете ещё несколько улучшений, которые могут повысить скорость загрузки страницы. Пожалуй, самые интересные из них — это использование сервис-воркера для перехвата сетевых запросов, что позволит сайту работать даже без подключения к интернету, и кэширование данных средствами CDN, что позволит пользователям, расположенным далеко от моего сервера в Сан-Франциско, сэкономить время, необходимое на обращение к нему. Это ценные идеи, однако они не особенно важны в моём случае, так как речь идёт о небольшом статическом сайте, играющем роль онлайнового резюме.
Итоги
Вышеописанные оптимизации позволили уменьшить время загрузки моей страницы с 8 секунд до примерно 350 миллисекунд при первом обращении к ней, и, при последующих обращениях, до просто невероятных 200 миллисекунд. Как видите, для достижения подобных улучшений потребовалось не так уж и много сил и времени. И, кстати, тем, кто заинтересован в оптимизации веб-сайтов, рекомендую почитать вот это.
Уважаемые читатели! Как вы оптимизируете свои сайты?