[Перевод] Оптимизация статического сайта: десятикратное ускорение

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

image

На сайте не применялось никаких динамических механизмов — там было немного анимации, он был создан с применением методов отзывчивого дизайна, но содержимое ресурса практически всегда оставалось неизменным. Автор статьи говорит, что то, что он увидел, быстро проанализировав ситуацию, буквально привело его в ужас. События DOMContentLoaded пришлось ждать около 4-х секунд, на полную загрузку страницы ушло 6.8 секунды. В процессе загрузки было выполнено 20 запросов, общий объём переданных данных составил около мегабайта. А ведь речь идёт о статическом сайте. Тут Джонлука понял, что он раньше считал свой сайт невероятно быстрым лишь потому, что привык к гигабитному интернет-соединению с низкой задержкой, используя которое, он, из Лос-Анджелеса, работал с сервером, расположенным в Сан-Франциско. Теперь же он оказался в Италии и воспользовался интернет-соединением на 8 Мбит/с. А это совершенно поменяло картину происходящего.

В этой статье Джонлука Де Каро расскажет о том, как ему удалось ускорить свой статический сайт в десять раз.

Обзор


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

46638c83625867d91ef066ac6f8f6c2a.png


Данные неоптимизированного сайта

Когда я всё это увидел, я впервые занялся оптимизацией своего сайта. До этого момента, если нужно было добавить на сайт библиотеку или что-то ещё, я просто взял бы и загрузил то, что нужно, с помощью конструкции src="…". Я совершенно не обращал внимания на производительность, не думал ни о чём, что может на неё повлиять, включая кэширование, встраивание кода, ленивую загрузку.

Тогда я начал искать людей, которые столкнулись с чем-то подобным. К несчастью, публикации, посвящённые оптимизации статических сайтов, очень быстро устаревают. Например, рекомендации, датированные 2010–2011 годами, посвящены использованию библиотек, а в некоторых из них сделаны предположения, касающиеся того, что библиотеками при разработке сайта не пользуются вообще. Анализируя эти материалы, я столкнулся и с тем, что некоторые из них попросту снова и снова повторяют одни и те же наборы правил.

Однако, я всё же нашёл два отличных источника информации: ресурс  High Performance Browser Networking и публикацию Дэна Луу. Хотя я и не пошёл в деле оптимизации так же далеко, как Дэн, разбирая содержимое сайта и средства его форматирования, мне удалось ускорить загрузку моей страницы примерно в десять раз. Теперь ждать DOMContentLoaded приходилось примерно пятую часть секунды, а на полную загрузку страницы было нужно 388 мс (надо сказать, что эти результаты нельзя назвать абсолютно точными, так как на них отразилась ленивая загрузка, о которой речь пойдёт ниже).

ea879085fa150bd5b81b92a1351a1c9b.png


Результаты оптимизации

Теперь поговорим о том, как удалось достигнуть таких результатов.

Анализ сайта


Первым шагом процесса оптимизации стало профилирование сайта. Мне хотелось понять, что именно требует больше всего времени, и как лучше всего распараллелить выполнение задач. Я, для профилирования сайта, использовал различные инструменты и проверил то, как сайт загружается из разных мест Земли. Вот список ресурсов, которыми я пользовался:

  • 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 шрифтов, хотя использовал лишь один из них).

2bd7a92c912a4e9cae8b5b9d78ba67f9.png


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

Мне хотелось оптимизировать всё, на что я могу повлиять — от наполнения сайта и скорости выполнения скриптов, до настроек веб-сервера (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% по ширине и высоте от его исходного размера. Это — не просто недостаточная оптимизация. Такие вещи демонстрируют почти полное безразличие разработчика к использованию полосы пропускания интернет-канала пользователя.

128a79110915daa7658043a697577d4f.png


Оптимизация фотографии

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

Уважаемые читатели! Как вы оптимизируете свои сайты?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru