Завершение цикла статей про техническое оживление Python Дайджест. В первых трех частях рассказано как был совершен переход с Python 3.4 на Python 3.11 и Django 4, отформатирована вся кодовая база с pre-commit, настроена автоматизация задач на основе Github Actions. В заключительной части расскажу как получить "быстрый" сайт.
Содержание цикла статей
- 1 часть: Как обновиться с Python 3.4 до Python 3.11, если pip уже сломан
- 2 часть: Как актуализировать всю кодовую базу с помощью pre-commit
- 3 часть: Как сделать CI для OpenSource проекта с Github Actions
-
4 часть: Как ускорить Django проект до (почти) максимума
— вы здесь —
Состояние после трех частей
- Приложение для сбора новостей про 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.