docs: миграция на tgFlaskForm и производительность Flask; контур flask_app; UI без лишних описаний
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# Производительность страниц 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](https://htmx.org/) / **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/коммиты.*
|
||||
Reference in New Issue
Block a user