Завершение цикла статей про техническое оживление Python Дайджест. В первых трех частях рассказано как был совершен переход с Python 3.4 на Python 3.11 и Django 4, отформатирована вся кодовая база с pre-commit, настроена автоматизация задач на основе Github Actions. В заключительной части расскажу как получить "быстрый" сайт.



Содержание цикла статей



Состояние после трех частей


  • Приложение для сбора новостей про Python работает на публичном домене.
  • Изменения автоматически (с помощью CI) проверяются на аккуратность, прохождение тестов и выкатываются на сервер.
  • Бэкапы делаются и сохраняются на внешнем диске.
  • PageSpeed показывает значения на уровне 70-80.

Задача (часть 4) — вернуть привычные 95-100 баллов на тесте PageSpeed


В Python Дайджест собираются разрозненные ссылки про Python. Контент обновляется не часто — несколько раз в сутки. Нет большой нагрузки на запись или чтение, при этом есть аудитория. Более 5 лет назад я использовал проект как место для экспериментов с кодом: организация кода, CI/CD, оптимизации SQL в Django, ускорение загрузки сайта, функции отложенной публикации без очередей и cron. Было много всякого испробовано и пригодилось в работе.


Отличной скорости для условно статического сайта можно добиться через пред-генерацию html страниц, а затем очень быструю отдачу через nginx/CDN (или сделать Google AMP-only сайт). Когда-то и такой эксперимент для Django приложения сделаю, но сейчас хочется оставаться динамическим сайтом.


PageSpeed — является простым в использовании мерилом качества сайта. Сервис проверяет скорость загрузки, избыточность данных, даже дизайн немного проверяет. Разобравшись от чего зависят несколько десятков параметров, можно заметно и свой проект улучшить.


PageSpeed показывает качество сайта в баллах, чем ближе к 100, тем лучше. Когда-то анализ Python Дайджест показывал около 100 баллов, а сейчас скатился до 70-80. Это же подтверждается ощущением «медленного» сайта.


План работ


Взяв отчет PageSpeed за основу, начал разбираться:


  • Много времени уходит на первое отображение страницы.
  • Загружается лишняя и устаревшая css/js статика.
  • Контент не виден на первом экране при открытии с мобилки.
  • Более старые выпуски новостей грузятся дольше.

Смотря на такой список, можно догадаться, что статика устарела и грузится синхронно, сжатие перестало работать, запросы в БД имеют проблему N+1. С этого и начнем.


Как ускорил загрузку внешней статики


Одна из причин снижения оценки — устаревшие версии bootstrap 3 и jquery, которые грузятся синхронно. Это замедляет первичное отображения страницы. Чтобы ускорить отображение, можно воспользоваться стандартным «костылем» и просить браузер предзагружать статику асинхронно. Выглядит в верстке это так:


<head>
....
 <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
 <noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"></noscript>
...
</head>

Как сжал статику стилей и JavaScript


Сайт Python Дайджест сделан с использованием Bootstrap. Это стабильный фреймворк для создания веб-сайтов, который существует уже больше 10 лет. Для работы с фреймворком достаточно подключить несколько .css файлов в html, нет необходимости тащить весь современный JavaScript для создания сайта.


Редкий сайт использует стандартный Bootstrap. Фреймворк легко расширяется за счет тем, которые порождают дополнительную статику. Все это нужно грузить быстро: сжимать, удалять неиспользуемые настройки. В Django экосистеме для таких задач хорошо использовать пакет django-compressor.


Пакету можно указать список файлов для сжатия (в виде html «импортов»), пакет их превратит в единый файл и сожмет для раздачи в виде кэша. Ограничение у пакета — сжимает локальные файлы. Автоматически скачать и сжать лежащее в CDN не умеет.


{% compress css %}
 {% block styles %}{% endblock %}
 <link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
 <link rel="stylesheet" type="text/css" href="{% static 'css/vs.css' %}">
{% endcompress %}

В дополнение можно взять whitenoise с brotli, который будет сжимать всю статику, а не только ту, что указывается в шаблонах. Как это работает, можно изучить на примере cookiecutter-django.


Как ускорил работы с внешними изображениями


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


Для этого использую пакет django-remdow. Пакет предоставляет фильтры для шаблона по работе с изображениями, в том числе img_local.


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


{{ '<img src="http://placehold.it/350x150">'|img_local }}

Пакет берет на себя скачивание изображений, а при необходимости вы можете дожать копии.

Как ускорил локальные изображения


Изображения — тяжелые объекты для сайта. Если есть возможность какие-то не отдавать — так стоит и сделать. Те, что обязательны — максимально сжать: по размеру (высота, ширина), по формату (jpeg, webp), а дальше указать «время жизни» для объекта, чтобы не отдавать лишний раз (ниже будет про это)


В верстке, зачастую, предполагается конкретный размер изображения, например, логотип сайта. Поэтому можно не стесняться и превращать большие изображения в миниатюры. В Python для этого хорошо подходит sorl-thumbnail


{% load thumbnail %}
{% thumbnail object.image "350" as im %}
 <link rel="image_src" href="http://pythondigest.ru{{ im.url }}">
 <meta name="twitter:image" content="http://pythondigest.ru{{ im.url }}"/>
 <meta property="og:image" content="http://pythondigest.ru{{ im.url }}" />
{% endthumbnail %}

И на уровне nginx тоже можно включить сжатие, а также указать «срок годности» для статики в 365 дней.


    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/x-javascript text/xml application/xml application/xml+rss text/javascript;

    location ~* ^(/media|/static) {
        # by default reed from root/<folder>
         access_log        off;
         log_not_found     off;
         expires           365d;
    }

Как уменьшил отдаваемый размер с GZip и кэшем


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


В Django есть несколько Middleware, которые помогают с этим:


MIDDLEWARE = [
    "django.middleware.gzip.GZipMiddleware",
    "django.middleware.cache.UpdateCacheMiddleware",
    ....
    "django.middleware.cache.FetchFromCacheMiddleware",
]

А также есть @cache_page, который можно использовать для кэширования страниц. Его можно обернуть в mixin и подмешивать в View класс. Это позволит делать 0 запросов в БД, если страница запрашивается часто.


В Дайджесте нет развесистой структуры базы данных, все весьма плоско — есть выпуски, есть категории новостей, есть сами новости, есть дополнительная метаинформация. Категорий меньше 10, выпусков уже почти 500, новостей тысячи. В такой комбинации можно натолкнуться на проблему N+1 запроса и извлекать данные в цикле, а не пачками.


Как анализировал и оптимизировал SQL


В Django стандартным средством для отладки является django-debug-toolbar, который позволяет находить тормоза кода, запросов и прочего. Так что включаю пакет и логгирование SQL в настройках проекта.


django-debug-toolbar хорошо дополнить инструментами django-silk и sentry. Их легко и приятно использовать для отладки на рабочем компьютере, не требуют больших изменений в коде для запуска.


  • django-silk предоставляет middleware для сбора и визуализации статистики по HTTP запросам (время выполнения, количество запросов в БД).
  • sentry интегрируется с Django глубже и умеет отслеживать весь путь выполнения запроса с опорой на конкретные строчки кода и используемых библиотек. Sentry — это SaaS, для микропроектов есть бесплатные тарифы, для корпоративного использования стоит посчитать собственную инсталляцию (Sentry версий <10 запускается легко, >=10 — заметно сложнее).

После активации django-debug-toolbar открывал поочередно страницы сайта, и по тем же методам, что и в Django Admin с миллионами записей — 11 практик оптимизаций для начинающих чистил запросы:


  • Убирал N+1 запросы с помощью prefetch_related и select_related.
  • Оставлял только необходимые поля моделей в запросах с помощью only и defer.
  • Улучшал условия в filter и exclude для QuerySet.
  • Предрассчитывал запросы и использовал их как кэш результатов.
  • Добавил недостающих индексов для таблиц.

Для работы с SQL хорошо использовать современные инструменты от DBA. Например, Explain PostgreSQL от Тензор. Это инструмент, который по EXPLAIN запроса может определить не оптимальность выполнения: отсутствие или переизбыток индексов в таблице, недостаточность рабочей памяти у БД и в целом не оптимальность структуры хранения данных.


Что получил в итоге


И в итоге получили 100 в PageSpeed. Теперь все переходы на сайте выглядят «мгновенным». При этом его можно динамически менять.



Что не стал делать или отложил на будущее


  • С помощью django-distill можно сгенерировать статический сайт и его раздавать через nginx.
  • Добавить поддержку Яндекс.Турбо и Google AMP, а также PWA версию сайта.
  • Добавить модуль nginx pagespeed для автоматической оптимизации по рекомендациям PageSpeed



Выводы


  • Если вы можете свести свое приложение к статичному, без большого количества пишущей нагрузки, то сжатие данных и кэши позволят получить очень быстрый сайт. Поисковикам это нравится.
  • Самые первые (простые и быстрые) решения дают максимальное улучшение, дальше каждый процент выгрызается тяжело.
  • Достойный мониторинг, инструменты APM (Application Performance Monitoring), Tracing System, позволяют быстрее находить изъяны проекта, но можно начать и с простых.
  • Performance management позволяет лучше разобраться в проекте.



НЛО прилетело и оставило здесь промокод для читателей нашего блога:
15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

Комментарии (0)