Разработка веб-интерфейса ERP: стек, виртуализация, производительность

Когда складской отчёт в ERP зависает на десятки секунд, сотрудники уходят в Excel. Мы разрабатываем веб-интерфейсы ERP, которые не тормозят даже на больших объёмах данных: используем SPA, виртуализацию таблиц и оптимистичные обновления. Наша команда реализует проект под ключ — от архитектуры до поддержки, обеспечивая стабильную работу и защиту от потери данных при одновременном редактировании.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка веб-интерфейса ERP: стек, виртуализация, производительность
Сложный
от 2 недель до 3 месяцев
Часто задаваемые вопросы

Отметим: когда в 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, документация, обучение
  1. Аналитика — изучаем бизнес-процессы, определяем модули и приоритеты.
  2. Проектирование — создаём архитектуру, выбираем стек, прототипируем ключевые экраны.
  3. Реализация — итеративная разработка с демо каждые две недели.
  4. Тестирование — unit, integration, e2e тесты, нагрузочное тестирование.
  5. Деплой — настройка CI/CD, миграция данных, обучение пользователей.

На каждом этапе готовим документацию: техническое задание, архитектурные решения, API docs, пользовательские инструкции.

Сроки

  • MVP с четырьмя-пятью модулями: 6–8 месяцев для команды из трёх-четырёх разработчиков.
  • Полноценная система (15+ модулей): 1,5–2 года при той же команде.

Итеративный подход — запуск минимального набора, обратная связь от пользователей, постепенное расширение. Попытка сделать всё сразу ведёт к провалу.

Что входит в работу

  • Техническое задание и архитектурная документация.
  • Разработка и настройка CI/CD.
  • Покрытие кода тестами (unit, integration).
  • Документация API и пользовательская документация.
  • Обучение администраторов и пользователей.
  • Гарантийная поддержка 6 месяцев после запуска.

Свяжитесь с нами для оценки вашего проекта. Получите консультацию по архитектуре и срокам.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060