17 KiB
Производительность страниц Flask (кабинет / мини-приложение): рабочий документ
Документ написан так, чтобы человек без контекста проекта мог по нему понять: что за система, где код, что именно оптимизировать, в каком порядке и как понять, что задача сделана.
1. Для кого и зачем этот файл
- Аудитория: ты сам через полгода, новый разработчик, DevOps, тимлид на планировании.
- Проблема от пользователей: «страницы мини-приложения на Flask грузятся долго».
- Цель документа: не угадать решение («перепишем на React»), а зафиксировать процесс: сначала измерить и локализовать узкое место, потом применить исправления с наибольшим эффектом при наименьшем риске.
2. Где живёт проект (карта репозитория)
Рабочая копия — монорепозиторий HR (корень: ClinicProjects/HR или аналог). Для задачи производительности важны в первую очередь два контура:
| Контур | Путь в репозитории | Назначение |
|---|---|---|
| Основной веб-кабинет HR | HR_TG_Bot/tgFlaskForm/ |
Flask-приложение: авторизация, кабинет, разделы в т.ч. тестирование сотрудников. Именно сюда чаще всего относят жалобы «мини-приложение / кабинет на Flask». |
| Отдельный Flask-скелет под тестирование | TestingWebApp/flask_app/ |
Упрощённое приложение того же стека (переходный контур, Docker testing-flask, порт 3107). Может быть медленным по тем же причинам (БД, шаблоны, отсутствие кэша статики), но это не обязательно тот же инстанс, что видят пользователи в проде. |
Связанные по смыслу документы (миграция данных, две БД):
TestingWebApp/docs/migration-to-tgflaskform.md— технический план.TestingWebApp/docs/migration-to-tgflaskform-plain.md— коротко «для людей».
Важно: жалоба «долго грузится» может относиться к:
- Веб-кабинет в браузере (
tgFlaskForm, типичный порт локально 3104 вweb_run.py). - Встроенный WebView в мини-приложении (Telegram MAX и т.п.) — тот же HTML с того же хоста, но другая сеть, кэш, DNS, TLS; воспроизведение обязательно на целевом клиенте.
- Переходный контур
TestingWebAppна 3107 — проверять отдельно, если пользователи реально ходят туда.
Перед оптимизацией уточнить URL/контур у тех, кто жалуется.
3. Что такое «страница грузится долго» в технических терминах
Раздели время на части (это основа всей работы):
- Сеть до сервера — DNS, TCP/TLS, RTT, прокси (nginx перед Flask).
- Время до первого байта (TTFB) — всё, что происходит на сервере до начала ответа: middleware, сессия, запросы к БД, рендер Jinja2, формирование заголовков.
- Загрузка тела ответа — размер HTML, сжатие (gzip/brotli).
- Параллельная загрузка подресурсов — CSS, JS, шрифты, картинки: их число, размер, кэширование (
Cache-Control), HTTP/2. - Выполнение JS на клиенте — если на странице тяжёлый скрипт; для классического SSR-кабинета часто вторично по сравнению с TTFB.
Твоя первая задача — для 2–3 типичных страниц (логин после редиректа, дашборд тестирования, список тестов, прохождение теста) записать: TTFB, DOMContentLoaded, полный LCP (или хотя бы «визуально готово»). Без этого нельзя честно выбрать между «чиним SQL» и «чиним статику».
Инструменты:
- Chrome DevTools → Network (колонка Time, размер, waterfall), Performance.
- На сервере: логирование длительности запроса (middleware или reverse proxy
request_timeв nginx).
4. Как устроена загрузка страницы в tgFlaskForm (ментальная модель)
Упрощённая цепочка для защищённого маршрута, например дашборда тестирования:
- Браузер запрашивает URL вида
/cabinet/testing/(blueprint вwebApp/interfaces/testing/__init__.py, префикс/cabinet/testing). - Срабатывают глобальные хуки Flask (в т.ч. cabinet access gate в
webApp/auth.py:register_cabinet_access_gate— проверка пути, сессии, статуса «Работает»). - Декоратор
@login_requiredна view: редирект на/loginили вызов функции. - View (например
routes_dashboard.py) вызывает функции изdb/queries/testing_queries.pyи др., собирает контекст и вызываетrender_template(...). - Jinja2 собирает HTML из шаблонов в
webApp/templates/(часто сextends/include— чем больше вложенность и данных в контексте, тем дольше CPU на рендер). - Ответ уходит клиенту; дальше грузятся статические файлы с
/static/....
Узкое место может быть на любом шаге; чаще всего в таких приложениях — шаг 4 (БД + много мелких запросов) и шаг 5 (большой шаблон).
5. Гипотезы, специфичные для этого кода (куда смотреть первым делом)
Ниже — не обвинение кода, а чек-лист для проверки после замеров.
5.1. Создание движка БД на каждый вызов сессии
Файл: HR_TG_Bot/tgFlaskForm/db/session.py.
Раньше get_engine() на каждом вызове делал create_engine(...) — новый пул и большие накладные расходы при десятках get_session() из db/queries/*.py (в т.ч. testing_queries.py).
Сделано (код): в db/session.py один потокобезопасный engine на процесс и один переиспользуемый sessionmaker; get_session() по-прежнему возвращает новую ORM-сессию, но поверх общего пула.
Дальше: при необходимости сокращать число отдельных сессий на один HTTP-запрос (§5.2) — это отдельная оптимизация.
5.2. Много открытий/закрытий сессий и запросов на одну страницу
Паттерн в testing_queries.py: почти каждая функция делает s = get_session(), try/finally: s.close(). Одна страница может дернуть несколько таких функций подряд → несколько раундов к БД.
Что сделать: для «тяжёлых» страниц — либо одна сессия на request и передача её вниз, либо один агрегирующий запрос вместо N мелких (устранение N+1). Конкретные места — смотреть по trace конкретного URL.
5.3. Декораторы и before_request
login_required и cabinet_employment_ok_from_session() в основном опираются на сессию, но gate и другие хуки могут добавлять логику. Если туда когда-нибудь добавят тяжёлые проверки в БД на каждый запрос — это сразу ударит по TTFB.
Что сделать: убедиться, что на горячем пути нет лишних запросов к БД без необходимости.
5.4. Шаблоны и статика
- Большие базовые layout’ы, много
include, тяжёлые циклы в Jinja — растёт CPU на рендер. - Статика без длинного кэша — каждый переход визуально «тормозит».
Что сделать: Network → сколько запросов к /static, какие размеры; для продакшена — заголовки кэша и сжатие на nginx (если nginx есть в цепочке — см. HR_TG_Bot/docker-compose*.yml и свою прод-конфигурацию).
5.5. Окружение
web_run.py: в non-production используется встроенный сервер Flask; для нагрузочного теста ближе к прод — waitress / gunicorn (как вTestingWebApp/flask_app/run.pyчерезWEB_USE_WAITRESS).- Сравнение «локально быстро, у пользователей медленно» — почти всегда сеть, БД на другом хосте, холодный пул, отсутствие индексов на прод-данных.
6. План работы (что делать по шагам)
Фаза 0 — уточнение (полдня максимум)
- Точный URL/продукт (кабинет HR vs TestingWebApp:3107 vs мини-app WebView).
- Роль пользователя и сценарий (первый заход, каждый клик, только раздел тестирования).
- Есть ли nginx / CDN перед приложением.
Фаза 1 — измерение (обязательно)
- Зафиксировать 3–5 URL и для каждого: TTFB, размер HTML, число запросов, суммарный вес.
- На сервере: время обработки запроса (middleware:
before_requesttimestamp vsafter_request). - Для самого медленного URL: список вызовов к БД (SQLAlchemy events, логирование, или APM, если есть).
Выход фазы: одно предложение вида: «узкое место — TTFB из-за БД» или «узкое месте — 40 запросов к статике без кэша».
Фаза 2 — правки по приоритету (типичный порядок)
- Инфраструктура БД: один engine на процесс; пул; при необходимости индексы (после анализа
EXPLAINсамых тяжёлых запросов). - Сократить число round-trips к БД на страницу: объединение запросов, eager loading где уместно, кэш редко меняющихся справочников (с инвалидацией или коротким TTL).
- Шаблоны: убрать лишние данные из контекста; упростить самые тяжёлые
include. - Статика: fingerprint +
Cache-Control: immutableдля бандлов; минификация; не тянуть огромные библиотеки на каждую страницу без нужды. - Прод-сервер приложений: waitress/gunicorn, адекватное число воркеров за reverse proxy.
Фаза 3 — если «всё ещё медленно именно при переходах между страницами»
Это уже про полную перезагрузку HTML, а не про «Flask медленный»:
- Вариант A: HTMX / Turbo — сервер по-прежнему отдаёт HTML, обновляются фрагменты; стек остаётся Python + Jinja.
- Вариант B: точечный React/Vite только для тяжёлого экрана (остальной кабинет не трогать) — выше стоимость сопровождения.
Выбор между A и B — после фазы 1: если TTFB уже низкий, а больно от полной перезагрузки — имеет смысл A/B; если узкое место всё ещё сервер — сначала дожать фазу 2.
7. Критерии готовности (Definition of Done)
Задачу по производительности можно закрыть, когда:
- Есть замеры до/после по тем же URL и тем же окружению (или согласованная методика).
- Задокументировано узкое место и что изменено (1–2 абзаца в changelog или в этом файле внизу секция «Итог»).
- Для пользовательского сценария выполняется согласованный SLO (например: TTFB p95 < X ms, полная загрузка ключевой страницы < Y s на 4G) — пороги задаёт продукт/команда, не этот документ.
8. Риски и что не делать
- Не менять стек на SPA «с нуля» без измерений — высокий риск и долгий срок при том, что проблема может быть в пуле БД или кэше статики.
- Не оптимизировать только локально на пустой БД — планы запросов на прод-объёме другие.
- Не кэшировать персональные страницы на CDN без понимания заголовков и кук — риск утечки данных между пользователями.
9. Быстрый указатель файлов
| Тема | Путь |
|---|---|
| Точка входа веба | HR_TG_Bot/tgFlaskForm/web_run.py |
| Регистрация приложения / blueprints | HR_TG_Bot/tgFlaskForm/webApp/__init__.py (и связанные модули) |
| Модуль тестирования (маршруты) | HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/routes_*.py |
| Запросы к БД тестирования | HR_TG_Bot/tgFlaskForm/db/queries/testing_queries.py |
| Сессия и engine | HR_TG_Bot/tgFlaskForm/db/session.py |
| Авторизация и gate | HR_TG_Bot/tgFlaskForm/webApp/auth.py |
| Шаблоны | HR_TG_Bot/tgFlaskForm/webApp/templates/ |
| Переходный Flask + waitress | TestingWebApp/flask_app/run.py, TestingWebApp/flask_app/app/ |
| Docker dev (по умолчанию 3107 legacy) | TestingWebApp/docker-compose.dev.yml |
| Docker dev кабинета | HR_TG_Bot/docker-compose.dev.yml |
10. Секция «Итог» (заполнять по мере работы)
| Дата | Контур | Узкое место | Что сделано | Метрика до → после |
|---|---|---|---|---|
| 2026-04-27 | tgFlaskForm |
Новый SQLAlchemy engine на каждый get_session() |
Singleton get_engine() + кэш sessionmaker в db/session.py |
замерить на стенде |
Документ создан как рабочая инструкция по задаче «медленная загрузка страниц Flask». Обновляй таблицу в §10 и при необходимости добавляй ссылки на PR/коммиты.