Files
TestingWebApp/docs/performance-flask-mini-app.md
Константин Лебединский 0229bc250b remove UI split (legacy/modern)
2026-04-30 21:53:49 +05:00

192 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Производительность страниц 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` — коротко «для людей».
**Важно:** жалоба «долго грузится» может относиться к:
1. **Веб-кабинет в браузере** (`tgFlaskForm`, типичный порт локально **3104** в `web_run.py`).
2. **Встроенный WebView в мини-приложении** (Telegram MAX и т.п.) — тот же HTML с того же хоста, но **другая сеть, кэш, DNS, TLS**; воспроизведение обязательно на целевом клиенте.
3. **Переходный контур** `TestingWebApp` на **3107** — проверять отдельно, если пользователи реально ходят туда.
Перед оптимизацией **уточнить 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:3107 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](https://htmx.org/) / **Turbo** — сервер по-прежнему отдаёт HTML, обновляются фрагменты; стек остаётся Python + Jinja.
- Вариант **B:** точечный **React/Vite** только для тяжёлого экрана (остальной кабинет не трогать) — выше стоимость сопровождения.
Выбор между A и B — после фазы 1: если TTFB уже низкий, а больно от полной перезагрузки — имеет смысл A/B; если узкое место всё ещё сервер — сначала дожать фазу 2.
---
## 7. Критерии готовности (Definition of Done)
Задачу по производительности можно закрыть, когда:
1. Есть **замеры до/после** по тем же URL и тем же окружению (или согласованная методика).
2. Задокументировано **узкое место** и **что изменено** (12 абзаца в 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 (по умолчанию 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/коммиты.*