You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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, порт 3108 в TestingWebApp/docker-compose.dev.yml). Может быть медленным по тем же причинам (БД, шаблоны, отсутствие кэша статики), но это не обязательно тот же инстанс, что видят пользователи в проде.

Связанные по смыслу документы (миграция данных, две БД):

  • TestingWebApp/docs/migration-to-tgflaskform.md — технический план.
  • TestingWebApp/docs/migration-to-tgflaskform-plain.md — коротко «для людей».

Важно: жалоба «долго грузится» может относиться к:

  1. Веб-кабинет в браузере (tgFlaskForm, типичный порт локально 3104 в web_run.py).
  2. Встроенный WebView в мини-приложении (Telegram MAX и т.п.) — тот же HTML с того же хоста, но другая сеть, кэш, DNS, TLS; воспроизведение обязательно на целевом клиенте.
  3. Переходный контур TestingWebApp на 3108 — проверять отдельно, если пользователи реально ходят туда.

Перед оптимизацией уточнить URL/контур у тех, кто жалуется.


3. Что такое «страница грузится долго» в технических терминах

Раздели время на части (это основа всей работы):

  1. Сеть до сервера — DNS, TCP/TLS, RTT, прокси (nginx перед Flask).
  2. Время до первого байта (TTFB) — всё, что происходит на сервере до начала ответа: middleware, сессия, запросы к БД, рендер Jinja2, формирование заголовков.
  3. Загрузка тела ответа — размер HTML, сжатие (gzip/brotli).
  4. Параллельная загрузка подресурсов — CSS, JS, шрифты, картинки: их число, размер, кэширование (Cache-Control), HTTP/2.
  5. Выполнение JS на клиенте — если на странице тяжёлый скрипт; для классического SSR-кабинета часто вторично по сравнению с TTFB.

Твоя первая задача — для 2–3 типичных страниц (логин после редиректа, дашборд тестирования, список тестов, прохождение теста) записать: TTFB, DOMContentLoaded, полный LCP (или хотя бы «визуально готово»). Без этого нельзя честно выбрать между «чиним SQL» и «чиним статику».

Инструменты:

  • Chrome DevTools → Network (колонка Time, размер, waterfall), Performance.
  • На сервере: логирование длительности запроса (middleware или reverse proxy request_time в nginx).

4. Как устроена загрузка страницы в tgFlaskForm (ментальная модель)

Упрощённая цепочка для защищённого маршрута, например дашборда тестирования:

  1. Браузер запрашивает URL вида /cabinet/testing/ (blueprint в webApp/interfaces/testing/__init__.py, префикс /cabinet/testing).
  2. Срабатывают глобальные хуки Flask (в т.ч. cabinet access gate в webApp/auth.py: register_cabinet_access_gate — проверка пути, сессии, статуса «Работает»).
  3. Декоратор @login_required на view: редирект на /login или вызов функции.
  4. View (например routes_dashboard.py) вызывает функции из db/queries/testing_queries.py и др., собирает контекст и вызывает render_template(...).
  5. Jinja2 собирает HTML из шаблонов в webApp/templates/ (часто с extends / include — чем больше вложенность и данных в контексте, тем дольше CPU на рендер).
  6. Ответ уходит клиенту; дальше грузятся статические файлы с /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:3108 vs мини-app WebView).
  • Роль пользователя и сценарий (первый заход, каждый клик, только раздел тестирования).
  • Есть ли nginx / CDN перед приложением.

Фаза 1 — измерение (обязательно)

  • Зафиксировать 3–5 URL и для каждого: TTFB, размер HTML, число запросов, суммарный вес.
  • На сервере: время обработки запроса (middleware: before_request timestamp vs after_request).
  • Для самого медленного URL: список вызовов к БД (SQLAlchemy events, логирование, или APM, если есть).

Выход фазы: одно предложение вида: «узкое место — TTFB из-за БД» или «узкое месте — 40 запросов к статике без кэша».

Фаза 2 — правки по приоритету (типичный порядок)

  1. Инфраструктура БД: один engine на процесс; пул; при необходимости индексы (после анализа EXPLAIN самых тяжёлых запросов).
  2. Сократить число round-trips к БД на страницу: объединение запросов, eager loading где уместно, кэш редко меняющихся справочников (с инвалидацией или коротким TTL).
  3. Шаблоны: убрать лишние данные из контекста; упростить самые тяжёлые include.
  4. Статика: fingerprint + Cache-Control: immutable для бандлов; минификация; не тянуть огромные библиотеки на каждую страницу без нужды.
  5. Прод-сервер приложений: 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)

Задачу по производительности можно закрыть, когда:

  1. Есть замеры до/после по тем же URL и тем же окружению (или согласованная методика).
  2. Задокументировано узкое место и что изменено (1–2 абзаца в changelog или в этом файле внизу секция «Итог»).
  3. Для пользовательского сценария выполняется согласованный 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 (пример порта 3108) 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/коммиты.