Документ описывает, что уже исправлено в ветке feat/back-button-and-optimization, и что ещё имеет смысл сделать для скорости, поддерживаемости и роста.
- Браузерная кнопка «Назад» теперь работает. В
design/src/app/App.tsxтри отдельных state-переменных (selectedCompanyInn,page,selectedArticleId) заменены однимroute: Routeс синхронизацией через History API:navigate(next)делаетhistory.pushStateпри переходе.- Слушатель
popstateвосстанавливаетrouteизwindow.locationпри нажатии «Назад/Вперёд» в браузере. - URL-схема:
/(рейтинг),/?company=INN(карточка ПКО, совместимо со старой «копировать ссылку»),/thematic(рейтинги-исследования),/article/ID(статья). - При загрузке по прямой ссылке роут парсится из URL — это сразу даёт share-friendly URL для карточек компаний и статей.
- На все
<img>логотипов компаний (RatingTable, CompareModal, CompareFloatingBar, FinanceTable) добавленыloading="lazy"иdecoding="async". Логотипы ниже первого экрана теперь не блокируют LCP. - Hero-картинка и иллюстрация в статье помечены
fetchPriority="high"+decoding="async". - Превью статей в Sidebar/Thematic-сетке/Related тоже lazy.
- В
index.htmlдобавлены<link rel="preload">для hero JPG и кириллической подмножества Inter — браузер начинает грузить их параллельно с парсингом JS.
- Удалён неиспользуемый
design/src/app/data/mockData.ts(431 строка).
pnpm buildпроходит, ошибок типов/линкера нет.- Bundle: 1 089 KB raw / 239 KB gzip — без изменений по размеру, потому что dead-code-elimination и так выкидывал
mockData.ts. Основной размер берут зависимости (см. §2).
Сейчас всё приложение использует ровно одну вещь из «UI-кита» — хук useIsMobile() (design/src/app/components/ui/use-mobile.ts, 20 строк). Всё остальное в components/ui/ (48 файлов, ~1500 строк) и связанные с ним зависимости в package.json не импортируются ни одним продакшен-компонентом.
- Перенести
useIsMobileвdesign/src/app/hooks/useIsMobile.ts. - Перебить импорты (9 файлов):
from './ui/use-mobile'→from '../hooks/useIsMobile'. - Удалить целиком
design/src/app/components/ui/иdesign/src/app/components/figma/(он тоже нигде не используется).
Заведомо не используется (проверено grep -rln):
@emotion/react @emotion/styled @mui/icons-material @mui/material
@popperjs/core react-popper react-slick canvas-confetti
react-dnd react-dnd-html5-backend react-responsive-masonry
react-router motion date-fns
Используется только косвенно в ui/ (то есть после шага 1 тоже не нужно):
все @radix-ui/* — 30 пакетов
class-variance-authority clsx tailwind-merge tw-animate-css
embla-carousel-react react-day-picker react-hook-form
input-otp vaul cmdk next-themes recharts sonner
react-resizable-panels
Останутся только:
react, react-dom, lucide-react
+ dev: vite, @vitejs/plugin-react, @tailwindcss/vite, tailwindcss
Ожидаемый эффект: install-time с ~57 с до ~10 с; bundle ~239 KB gzip → оценочно 80–110 KB gzip (после удаления Radix-зависимостей, которые сейчас тянутся через tw-animate-css и pivot-импорты).
Реальные компоненты приложения не используют ни одного className= — всё через inline style={…}. Tailwind подключён через PostCSS только для ui/. После шага 1:
- Удалить
@tailwindcss/viteиtailwindcssизdevDependencies. - Удалить
design/src/styles/tailwind.cssи@import './tailwind.css'изindex.css. - Из
vite.config.tsубрать плагинtailwindcss(). - Удалить
design/src/styles/theme.css(это переменные shadcn/ui — для inline-стилей не нужны). - В
index.cssостанется только@import './fonts.css'.
Сейчас:
import { ArrowLeft, ArrowUp } from 'lucide-react';lucide-react уже tree-shake'ится Vite-ом, но иконки тащатся как один объект. Безопаснее:
import ArrowLeft from 'lucide-react/dist/esm/icons/arrow-left';…или, как минимум, посчитать иконки (grep -roh "from 'lucide-react'" -A0 src | wc -l) и при числе <30 — оставить как есть; при больших количествах перейти на subpath-импорты.
Сейчас 4 файла данных импортируются прямо в JS-бандл:
| Файл | Размер | Когда нужно |
|---|---|---|
ratingData.ts |
561 строка | первый экран — нужно сразу |
logoMap.ts |
445 строк | первый экран |
companyDetails.ts |
593 строки | только на карточке компании |
financeDynamic.ts |
551 строка | только на карточке (динамика) |
articlesData.ts |
195 строк | только на /thematic и /article |
Это значит, что при заходе на главную браузер парсит 2300+ строк JSON-в-JS, из которых на первом экране нужны только две таблицы.
- Скрипту
scripts/update_data.pyпараллельно генерировать JSON-файлы вdesign/public/data/:public/data/rating.json public/data/logo-map.json public/data/company-details.json # или по одному файлу на ИНН public/data/finance-dynamic.json public/data/articles.json - На клиенте заменить
import { ratingData }на async fetch:В компоненте —const res = await fetch('/data/rating.json'); const ratingData: RatingCompany[] = await res.json();
useEffect+useState, либо более чистый кастомный хукuseJson<T>(url). - Для CompanyCard данные подтягиваются только при открытии карточки — экономия 1100+ строк на первом заходе.
- Тип
RatingCompany,CompanyDetailsи т.д. оставляем в TS-файлах рядом — но без значений, толькоexport interface. - Кэшируется автоматически — Vercel выставит
Cache-Control: public, max-age=…для статики, и при следующих заходах файлы тянутся из disk-cache.
Ожидаемый эффект: первый JS-чанк уменьшается на 60–80 KB gzip; данные грузятся параллельно, а не в составе main-бандла, который блокирует first paint.
- Логотипы (
public/logos/, 6.1 MB). Перегнать PNG → WebP скриптом:Вcd design/public/logos for f in *.png; do cwebp -q 85 "$f" -o "${f%.png}.webp"; done
logoMap.tsоставить PNG-имена, а в<img>рендерить<picture>с двумя<source>(WebP + PNG-фоллбэк). Реальное падение веса — 60–80%. hero-architecture.jpg(261 KB) — пересохранить как WebP, отдать через<picture>. Размер пересчёта обычно ~50–80 KB.article-1.jpg(290 KB) — то же самое. Если автор подкладывает RAW-фотки — добавить вupdate_data.pyшаг конвертации.- Размеры в HTML. Сейчас
<img>не имеютwidth/height— браузер пересчитывает layout при загрузке. Добавить атрибуты или задать через CSSaspect-ratioдля контейнеров (это уже сделано для article preview, проверить остальные).
В fonts.css 56 @font-face-объявлений на 15 файлов. На русскоязычном сайте реально используются 1–2 кириллические подгруппы (вес 400 и 600/700). Остальные (greek, vietnamese, latin-ext) тащатся «на всякий случай» — браузер их не загрузит при unicode-range-фильтрации, но 56 правил всё равно парсятся.
- Минимум — оставить только
cyrillic+latinподмножества для весов 400, 500, 600, 700 → ~8 правил. Можно вообще обойтись только cyrillic-подмножеством. - Альтернатива — убрать самохостинг и переехать обратно на
fonts.googleapis.com(преимущество: shared cache между сайтами).
Если данные останутся в JS — хотя бы разнести страницы:
const CompanyCard = lazy(() => import('./components/CompanyCard'));
const ArticlePage = lazy(() => import('./components/ArticlePage'));
const CompareModal = lazy(() => import('./components/CompareModal'));В vite.config.ts можно явно задать chunks:
build: {
rollupOptions: {
output: {
manualChunks: {
'data-company': ['./src/app/data/companyDetails.ts', './src/app/data/financeDynamic.ts'],
},
},
},
},Сейчас:
src/app/
App.tsx ← 480 строк, и роутинг, и thematic-сетка inline
components/ ← всё вперемешку
data/ ← данные
Лучше:
src/
app/ ← App + роутинг
pages/ ← RatingPage.tsx, ThematicPage.tsx, ArticlePage.tsx, CompanyPage.tsx
features/ ← rating-table, compare, filters
shared/ui/ ← Avatar, NumberCell, Tag, BackLink — переиспользуемые «атомы»
shared/hooks/ ← useIsMobile, useJson, useRoute
data/ ← типы + загрузка
styles/ ← глобал стили
Сейчас в App.tsx вкручен render thematic-сетки на 100+ строк. Это надо вынести в pages/ThematicPage.tsx.
Решить один раз: либо inline style={}, либо CSS Modules / Tailwind. Текущий микс «всё inline + Tailwind, который не используется» — это ловушка для будущего разработчика. Рекомендую CSS Modules:
- Никаких runtime cost.
- Стили видны в DevTools нормальными классами.
- Inline остаётся только для динамических значений (вычисляемая ширина и т.п.).
Простейший минимум — выделить design tokens в один файл:
// src/shared/theme.ts
export const colors = {
bg: '#0a0f15',
card: '#111920',
accent: '#0DF0E6',
border: 'rgba(255,255,255,0.06)',
textMuted: 'rgba(255,255,255,0.4)',
};
export const fontFamily = "Inter, -apple-system, 'Segoe UI', sans-serif";И импортировать вместо строковых литералов, разбросанных по 14 файлам.
В App.tsx 4 раза подряд встречается:
<div style={{ minHeight:'100vh', background:'#0a0f15', ... }}>
<SiteHeader … />
<div style={{ maxWidth:'1100px', … }}>{children}</div>
<Footer />
</div>Это просится в <PageLayout activeTab>{children}</PageLayout>. Сокращает App.tsx на 30–40 строк и убирает источник расхождений.
ratingCompanies(filter+sort) сейчас пересчитывается на каждом ререндере App — IIFE безuseMemo. При 530 строк это пока неощутимо, но при добавлении более тяжёлых фильтров обернуть вuseMemo(() => …, [searchQuery, ratingFilters]).RatingTableрендерит сразу все строки (PAGE_SIZE = 100, но всё равно). При расширении до полного списка стоит подумать оreact-virtuosoили@tanstack/react-virtual.- Inline-функции в
onMouseEnter/Leaveхэндлерах создают новые closures на каждый ререндер карточек статей. Для длинных списков завернуть вReact.memo+ стабильные обработчики.
- Включить TypeScript strict mode. Сейчас в
tsconfig.jsonего, скорее всего, нет (надо проверить — файл я не открывал)."strict": trueотловитselectedCompanyInn-style проблемы на этапе компиляции. - Добавить ESLint + Prettier. Хотя бы:
eslint-config-airbnb-typescript eslint-plugin-react-hooks ← особенно полезен eslint-plugin-jsx-a11y ← полезен для img alt и т.п. - CI / GitHub Actions. Простейший workflow на 10 строк:
pnpm install && pnpm build && pnpm typecheck. Это уже отловит 80% багов до мержа.
- На карточках компаний сейчас нет уникального
<title>/<meta description>/ OG-тегов. После того как роутинг даёт уникальные URL — имеет смысл управлятьdocument.titleиз эффекта вCompanyCardиArticlePage. - Для соц-сетей (
og:image,og:title) полноценный SSR не нужен — достаточно прероллированных мета-тегов через простейший Vercel Edge-функцию или статически сгенерированные страницы (для топ-30 компаний — это 30 HTML-файлов).
Если интересна реальная скорость у пользователей — добавить веб-вайталы:
import { onCLS, onFID, onLCP } from 'web-vitals';
[onCLS, onFID, onLCP].forEach(fn => fn(m => sendBeacon('/vitals', m)));И смотреть p75 LCP по реальным сессиям, а не только в Lighthouse.
- Не переходить на
react-routerради текущих 4 URL-ов — History API даёт всё что нужно за 30 строк.react-router(он уже в зависимостях, кстати) станет оправданным, когда страниц будет 10+. - Не «улучшать» данные на лету в
update_data.pyтак, чтобы клиент потом транспонировал. Лучше один раз отдать готовый JSON. - Не вводить state-менеджмент (Redux/Zustand) — в текущей модели 6 state-переменных в
App.tsxэто перебор.
Если делать поэтапно (рекомендую такой порядок — каждый шаг — отдельный PR):
| Шаг | Эффект | Сложность |
|---|---|---|
| Браузерный «назад» (сделано) | UX-баг исправлен | низкая |
| Lazy/preload картинок (сделано) | LCP −200…400 ms | низкая |
Удалить components/ui/ + figma/ + мусор из package.json |
-50% size+deps | средняя (механика, риск 0) |
| Убрать Tailwind | -10 KB CSS, -1 dev plugin | средняя |
| Конвертировать логотипы в WebP | -4–5 MB трафика на холодном кэше | низкая (скрипт) |
| Данные через fetch JSON | -60–80 KB gzip JS | средняя |
| Code splitting CompanyCard / ArticlePage | -50–80 KB gzip с первого экрана | низкая |
| TS strict + ESLint + CI | защита от регрессий | низкая |
Каждый из этих пунктов — изолированный PR на 30–200 строк диффа, легко ревьюится и легко откатывается.