Отметим: когда в ERP-системе складской отчёт зависает на 10 секунд при загрузке 50 000 строк, пользователи начинают использовать Excel. Разработка веб-интерфейса ERP — это не про React из коробки, а про тонкую настройку виртуализации, управление состоянием и разрешение конфликтов доступа. В многопользовательской среде критична блокировка записей при одновременном редактировании. Без proper optimistic concurrency control пользователи перезаписывают данные друг друга. Типичные боли: медленная загрузка таблиц, путаница с правами доступа, потеря данных при offline. Мы решаем их с помощью SPA, виртуализации, оптимистичных обновлений и Permission Guard для ERP. Разрабатываем интерфейсы от MVP с четырьмя модулями до полноценной системы на 15+ модулей. В этом материале разберём ключевые архитектурные решения и практические приёмы, которые помогут избежать типовых ошибок и построить масштабируемый интерфейс.
Выбор стека для ERP-интерфейса
Выбор архитектуры и стека — фундамент, от которого зависит производительность и стоимость поддержки. Рассмотрим два распространённых подхода: SPA и SSR.
| Критерий | SPA | SSR |
|---|---|---|
| Производительность при интенсивном UI | Высокая | Низкая (каждый клик — запрос) |
| Персонализация рабочих мест | Гибкая | Ограничена |
| Офлайн-режим (PWA) | Поддерживается | Невозможен |
| SEO (не критично для ERP) | Плохое | Хорошее |
| Начальная загрузка | Медленнее (bundle) | Быстрее (HTML) |
Для ERP выбор однозначный — SPA. Исключение — если требуется SEO для публичных частей (например, каталог продукции).
Frontend стек: React 18+ с Concurrent Features, TypeScript (strict), TanStack Table для таблиц, TanStack Query для данных, React Hook Form + Zod для форм, Zustand для UI-состояния. Компонентная библиотека — Radix UI + Tailwind (гибкость) или Ant Design (скорость).
Как работать с большими таблицами?
Таблица на 50 000 строк — типичная задача для складского учёта или отчётности. Без виртуализации браузер зависает. Благодаря виртуализации время рендеринга падает до 100 мс даже при 100 000 строк.
import {
useReactTable,
getCoreRowModel,
flexRender,
type ColumnDef,
} from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface VirtualizedTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
rowHeight?: number;
}
export function VirtualizedTable<T>({
data,
columns,
rowHeight = 40,
}: VirtualizedTableProps<T>) {
const parentRef = useRef<HTMLDivElement>(null);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => rowHeight,
overscan: 20,
});
const virtualItems = virtualizer.getVirtualItems();
const totalSize = virtualizer.getTotalSize();
return (
<div ref={parentRef} className="overflow-auto h-full">
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-white z-10 shadow-sm">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
style={{ width: header.getSize() }}
className="text-left px-3 py-2 text-xs font-semibold text-gray-600 border-b"
>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{virtualItems.length > 0 && (
<tr style={{ height: virtualItems[0].start }}>
<td colSpan={columns.length} />
</tr>
)}
{virtualItems.map(virtualRow => {
const row = rows[virtualRow.index];
return (
<tr
key={row.id}
className="hover:bg-gray-50 border-b border-gray-100"
style={{ height: rowHeight }}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-3 py-2 text-sm">
{flexRender(cell.column.columnDef.header, cell.getContext())}
</td>
))}
</tr>
);
})}
{virtualItems.length > 0 && (
<tr style={{ height: totalSize - virtualItems[virtualItems.length - 1].end }}>
<td colSpan={columns.length} />
</tr>
)}
</tbody>
</table>
</div>
);
}
Как ускорить отклик интерфейса?
Оптимистичные обновления — пользователь меняет статус заказа, интерфейс реагирует немедленно, не ждёт ответа сервера. При ошибке происходит откат. Это сокращает воспринимаемое время отклика на 200–500 мс.
const queryClient = useQueryClient();
const updateStatus = useMutation({
mutationFn: (data: { orderId: string; status: OrderStatus }) =>
api.patch(`/orders/${data.orderId}/status`, { status: data.status }),
onMutate: async ({ orderId, status }) => {
await queryClient.cancelQueries({ queryKey: ['orders', orderId] });
const prev = queryClient.getQueryData(['orders', orderId]);
queryClient.setQueryData(['orders', orderId], (old: Order) => ({
...old, status,
}));
return { prev };
},
onError: (_err, { orderId }, context) => {
queryClient.setQueryData(['orders', orderId], context?.prev);
toast.error('Не удалось изменить статус');
},
onSettled: (_, __, { orderId }) => {
queryClient.invalidateQueries({ queryKey: ['orders', orderId] });
},
});
Разрешение конфликтов данных при многопользовательском доступе
Используем стратегию last-write-wins с версионированием сущностей. Каждая запись содержит поле version (integer), которое увеличивается при изменении. Клиент передаёт текущую версию, сервер проверяет совпадение. Если версия устарела — возвращает 409 Conflict, клиент получает актуальные данные и предлагает пользователю разрешить конфликт. Это реализовано поверх TanStack Query через механизм optimistic updates + rollback.
Разграничение доступа к модулям с Permission Guard
Создаём компонент Permission Guard, который проверяет наличие разрешения у пользователя. Если разрешения нет — можно показать заглушку или ничего. Роли и разрешения хранятся на сервере, клиент получает их при аутентификации.
type Permission = 'orders:read' | 'orders:write' | 'stock:read';
function PermissionGuard({ permission, children, fallback = null }: {
permission: Permission;
children: ReactNode;
fallback?: ReactNode;
}) {
const user = useUser();
if (user.permissions.includes(permission)) {
return <>{children}</>;
}
return <>{fallback}</>;
}
Производительность: дополнительные оптимизации
- Code splitting по модулям — пользователь склада не загружает модуль HR, ускоряя загрузку на 40%.
- Дебаунс для поиска и фильтров — отправляем запрос не чаще одного раза в 300 мс, снижая нагрузку на сервер в 3 раза.
- Мемоизация тяжёлых вычислений — отчёты с агрегацией в браузере через
useMemo.
Следим за показателями Core Web Vitals — ключевыми метриками производительности, которые напрямую влияют на UX. MDN Web Docs рекомендует ориентироваться на LCP < 2.5 с, FID < 100 мс, CLS < 0.1. Используем TanStack Query для управления серверным состоянием.
Процесс работы
Этапы разработки ERP-интерфейса:
| Этап | Длительность | Результат |
|---|---|---|
| Аналитика | 2–4 недели | Описание бизнес-процессов, спецификация модулей |
| Проектирование | 2–4 недели | Архитектура, прототипы ключевых экранов |
| Реализация | 4–6 недель на модуль | Работающий функционал с тестами |
| Тестирование | 2–3 недели | Отчёт о нагрузочном тестировании, исправление багов |
| Деплой | 1–2 недели | CI/CD, документация, обучение |
- Аналитика — изучаем бизнес-процессы, определяем модули и приоритеты.
- Проектирование — создаём архитектуру, выбираем стек, прототипируем ключевые экраны.
- Реализация — итеративная разработка с демо каждые две недели.
- Тестирование — unit, integration, e2e тесты, нагрузочное тестирование.
- Деплой — настройка CI/CD, миграция данных, обучение пользователей.
На каждом этапе готовим документацию: техническое задание, архитектурные решения, API docs, пользовательские инструкции.
Сроки
- MVP с четырьмя-пятью модулями: 6–8 месяцев для команды из трёх-четырёх разработчиков.
- Полноценная система (15+ модулей): 1,5–2 года при той же команде.
Итеративный подход — запуск минимального набора, обратная связь от пользователей, постепенное расширение. Попытка сделать всё сразу ведёт к провалу.
Что входит в работу
- Техническое задание и архитектурная документация.
- Разработка и настройка CI/CD.
- Покрытие кода тестами (unit, integration).
- Документация API и пользовательская документация.
- Обучение администраторов и пользователей.
- Гарантийная поддержка 6 месяцев после запуска.
Свяжитесь с нами для оценки вашего проекта. Получите консультацию по архитектуре и срокам.







