Фоновые задачи с обновлением прогресса: UI-паттерны, которые работают
Изучите практические паттерны для фоновых задач с обновлением прогресса: очереди, модель статусов, сообщения в UI, отмена и повтор, а также отчёты об ошибках.

Почему пользователи застревают, когда задачи выполняются в фоне
Долгие операции не должны блокировать интерфейс. Люди переключают вкладки, теряют связь, закрывают ноутбук или просто ждут и не понимают, идёт ли работа. Когда экран кажется «замороженным», пользователи начинают угадывать — а угадывания приводят к повторным кликам, дубликатам и обращениям в поддержку.
Хорошая фоновая работа — это прежде всего уверенность. Пользователям нужно три вещи:
- Чёткий статус (queued, running, done)
- Понятие времени (даже грубая оценка)
- Очевидное следующее действие (ждать, продолжать работу, отменить или вернуться позже)
Без этого задача может выполняться нормально, но опыт будет казаться сломанным.
Одно распространённое заблуждение — считать медленный запрос настоящей фоновой задачей. Медленный запрос — это всё ещё один веб-вызов, который заставляет пользователя ждать. Фоновая задача отличается: вы стартуете работу, сразу даёте подтверждение, а тяжёлая обработка происходит отдельно, пока интерфейс остаётся отзывчивым.
Пример: пользователь загружает CSV для импорта клиентов. Если UI блокируется, он может обновить страницу, загрузить файл снова и получить дубликаты. Если импорт пойдёт в фоне и интерфейс покажет карточку задачи с прогрессом и безопасной кнопкой Cancel, пользователь сможет продолжать работу и вернуться к понятному результату.
Основные блоки: задания, очереди, воркеры и статус
Когда говорят о фоновых задачах с обновлением прогресса, обычно имеют в виду четыре взаимосвязанных компонента.
Задача (job) — единица работы: «импортировать этот CSV», «сгенерировать отчёт» или «отправить 5 000 писем». Очередь — это линия ожидания, где задания хранятся до обработки. Воркеры берут задания из очереди и выполняют их (по одному или параллельно).
Для UI самая важная часть — это жизненный цикл задания. Делайте состояния небольшими и предсказуемыми:
- Queued: принято, ждёт воркера
- Running: активно обрабатывается
- Done: успешно завершено
- Failed: остановлено с ошибкой
Каждому заданию нужен уникальный job ID. Когда пользователь нажимает кнопку, возвращайте этот ID мгновенно и показывайте строку «Task started» в панели задач.
Затем нужен способ спросить: «Что происходит сейчас?» Обычно это статус-эндпоинт (или любой метод чтения), который по job ID возвращает состояние и детали прогресса. UI использует это, чтобы показать процент, текущий шаг и сообщения.
Наконец, статус должен жить в надёжном хранилище, а не только в памяти. Воркеры падают, приложения перезапускаются, пользователи обновляют страницы. Надёжное хранилище делает прогресс и результаты стабильными. Минимум, что стоит сохранять:
- текущее состояние и метки времени
- значение прогресса (процент или счётчики)
- краткое резюме результата (что создано или изменено)
- детали ошибок (для отладки и понятных сообщений пользователю)
Если вы строите решение на платформе вроде AppMaster, относитесь к таблице статусов как к любой другой модели данных: UI читает запись по job ID, а воркер обновляет её по мере выполнения.
Выбор паттерна очереди, соответствующего нагрузке
Выбор структуры очереди влияет на то, насколько «справедливо» и предсказуемо ведёт себя приложение. Если задача застряла за кучей другой работы, пользователи воспринимают задержку как случайную, даже когда система здорова. Поэтому выбор очереди — это и UX-решение, а не только инфраструктурное.
Простая очередь в базе данных часто достаточна при небольшой нагрузке, коротких задачах и терпимости к случайным повторным попыткам. Её легко настроить, удобно инспектировать, и всё можно держать в одном месте. Пример: админ запускает ночной отчёт для маленькой команды — если он один раз перезапустится, это не критично.
Выбор выделенной очереди нужен, когда растёт Throughput, задачи тяжелеют или надёжность становится обязательной. Импорты, обработка видео, массовые уведомления и рабочие процессы, которые должны выполняться через рестарты, выигрывают от изоляции, видимости и аккуратной политики повторов. Это важно для прогресса, потому что пользователи замечают пропадание обновлений и зависшие состояния.
Структура очереди также влияет на приоритеты. Одна очередь проще, но смешивание быстрых и медленных задач делает быстрые действия «тяжёлыми». Отдельные очереди помогают, когда интерактивные пользовательские операции должны казаться мгновенными, а пакетная работа может ждать.
Устанавливайте лимиты конкурентности осознанно. Слишком много параллельных задач может перегрузить базу и сделать прогресс «скачуистым». Слишком мало — будет медленно. Начинайте с небольшой, предсказуемой конкурентности на очередь и повышайте только когда сможете сохранить стабильность времени выполнения.
Проектирование модели прогресса, которую можно показать в UI
Если модель прогресса туманна, UI будет выглядеть туманно. Решите, что система честно может отчитывать, как часто это меняется и что пользователи должны делать с этой информацией.
Простая схема статуса, которую поддерживает большинство задач:
- state: queued, running, succeeded, failed, canceled
- percent: 0–100, когда можно измерить
- message: одно короткое предложение, понятное пользователю
- timestamps: created, started, last_updated, finished
- result_summary: счётчики типа processed, skipped, errors
Далее определите, что именно значит «прогресс».
Процент работает, когда есть реальный знаменатель (строки в файле, письма для отправки). Он вводит в заблуждение, если работа непредсказуема (ожидание третьей стороны, переменные вычисления, тяжёлые запросы). В таких случаях прогресс по шагам вызывает больше доверия — он двигается вперёд понятными кусками.
Практическое правило:
- Используйте percent, когда можно сказать честно «X из Y».
- Используйте steps, когда длительность неизвестна (Validate file, Import, Rebuild indexes, Finalize).
- Используйте indeterminate прогресс, когда ни одно из двух не подходит, но держите сообщение свежим.
Сохраняйте частичные результаты во время выполнения. Так UI может показать полезную информацию до завершения, например текущий счёт ошибок или превью изменений. Для импорта CSV сохраняйте rows_read, rows_created, rows_updated, rows_rejected и последние ошибки.
Это фундамент доверия: UI остаётся спокойным, числа двигаются, и краткое «что произошло?» готово, когда задача закончилась.
Доставка обновлений прогресса: polling, push и гибрид
Доставить прогресс от бэкенда до экрана — здесь многие реализации дают сбой. Выбирайте метод доставки в зависимости от частоты изменений и числа пользователей, которые будут наблюдать.
Polling — самый простой: UI запрашивает статус каждые N секунд. Хороший дефолт — 2–5 секунд, пока пользователь смотрит страницу, затем замедление по времени. Если задача длится дольше минуты, переходите на 10–30 секунд. Если вкладка в фоне — ещё медленнее.
Push (WebSocket, server-sent events или мобильные уведомления) помогает, когда прогресс меняется быстро или пользователям важна «сейчас». Push даёт ощущение мгновенности, но нужен fallback на случай потери соединения.
Гибридный подход часто оптимален: быстрое опрос в начале (чтобы UI видел переход queued → running), затем замедление по мере стабилизации. Если добавляете push, оставляйте медленный polling как страховку.
Если обновления перестают приходить, рассматривайте это как отдельное состояние. Показывайте «Last updated 2 minutes ago» и предлагайте обновить. На бэкенде помечайте задания как stale, если heartbeat не поступал.
UI-паттерны для долгих задач, которые выглядят понятно
Понятность строится из двух вещей: небольшой набора предсказуемых состояний и текста, который говорит пользователю, что будет дальше.
Пишите названия состояний в UI, а не только в бэкенде. Задача может быть queued (ждёт очереди), running (работает), waiting for input (нужен выбор), completed, completed with errors или failed. Если пользователь не различает эти состояния, он подумает, что приложение зависло.
Дайте рядом с индикатором прогресса простую и полезную фразу. «Importing 3,200 rows (1,140 processed)» однозначно лучше, чем «Processing». Добавьте одно предложение, отвечающее на вопрос: можно ли уйти и что будет дальше? Например: «You can close this window. We'll keep importing in the background and notify you when it's ready.»
Где размещать прогресс должно соответствовать контексту пользователя:
- Модал хорош, когда задача блокирует следующий шаг (например, генерация PDF счёта прямо сейчас).
- Toast подходит для быстрых задач, которые не должны прерывать работу.
- Встроенный прогресс в строке таблицы — для операций по элементам.
Для всего, что дольше минуты, добавьте простую страницу Jobs (или панель Activity), чтобы люди могли найти работу позже.
Явный UI для долгих задач обычно включает метку статуса с временем последнего обновления, полосу прогресса (или шаги) с одной строкой детали, безопасную отмену и область результатов с резюме и следующими действиями. Делайте завершённые задания доступными — пользователю не должно казаться, что он должен ждать на одном экране.
Как сообщать о «завершено с ошибками», не пугая пользователя
«Завершено» — не всегда победа. Когда фоновая задача обработала 9 500 записей и 120 упали, пользователю нужно понять, что произошло, не копаясь в логах.
Рассматривайте частичный успех как отдельный исход. В основной строке статуса показывайте обе стороны: «Imported 9,380 of 9,500. 120 failed.» Это повышает доверие — система честно говорит, что часть работы сохранена.
Дайте небольшой свод ошибок, по которым можно действовать: «Missing required field (63)» и «Invalid date format (41)». В итоговом состоянии «Completed with issues» часто понятнее, чем просто «Failed», потому что не создаёт впечатления, что ничего не работает.
Экспортируемый отчёт об ошибках превращает непонимание в список задач: идентификатор строки или элемента, категория ошибки, человекочитаемое сообщение и имя поля при необходимости.
Сделайте следующее действие очевидным и расположите его рядом с резюме: исправить данные и повторить попытку для упавших элементов, скачать отчёт об ошибках или обратиться в поддержку, если кажется, что это проблема системы.
Надёжные действия: отмена и повтор
Отмена и повтор кажутся простыми, но быстро подрывают доверие, если UI говорит одно, а система делает другое. Определите, что значит Cancel для каждого типа задач, и честно отображайте это в интерфейсе.
Обычно существуют два корректных режима отмены:
- «Stop now»: воркер часто проверяет флаг отмены и быстро завершает работу.
- «Stop after this step»: текущий шаг завершается, затем задача останавливается перед следующим.
В UI показывайте промежуточный статус «Cancel requested», чтобы пользователь не кликал повторно.
Сделайте отмену безопасной, проектируя работу повторяемой. Если задача записывает данные, предпочитайте идемпотентные операции (безопасно запускать дважды) и делайте очистку при необходимости. Например, для импорта CSV сохраняйте job-run ID, чтобы можно было просмотреть изменения в прогоне #123.
Retry требует той же ясности. Повтор того же экземпляра задачи подходит, если она может продолжить; создание нового экземпляра безопаснее, если нужен чистый прогон с новым временем и аудиторским следом. В любом случае объясните, что будет и что не будет повторено.
Ограждения, которые делают отмену и повтор предсказуемыми:
- Ограничьте число попыток и показывайте счётчик.
- Блокируйте Retry, пока задача выполняется.
- Просите подтверждение, если повтор может привести к дубликатам (письма, оплаты, экспорты).
- Показывайте последнюю ошибку и последний успешный шаг в панели деталей.
Пошаговый пример: от клика до завершения
Хороший end-to-end поток начинается с одного правила: UI никогда не должен ждать выполнения работы — только job ID.
Поток (от клика до финального состояния)
-
Пользователь запускает задачу, API отвечает быстро. Когда пользователь нажимает Import или Generate report, сервер сразу создаёт запись job и возвращает уникальный job ID.
-
Помещаем работу в очередь и ставим первый статус. Кладём job ID в очередь и помечаем статус queued с прогрессом 0%. Это даёт UI реальную информацию ещё до того, как воркер подхватит задачу.
-
Воркеры запускают и отчитываются о прогрессе. Когда воркер стартует, ставьте статус running, сохраняйте время начала и обновляйте прогресс небольшими честными шагами. Если процент измерить нельзя, используйте шаги: Parsing, Validating, Saving.
-
UI ориентирует пользователя. UI опрашивает или подписывается на обновления и рендерит понятные состояния. Покажите короткое сообщение (что происходит сейчас) и только те действия, которые уместны в текущем состоянии.
-
Финализируйте с надёжным результатом. По завершении сохраняйте время окончания, вывод (ссылка на скачивание, созданные ID, сводные счётчики) и детали ошибок. Поддерживайте состояние finished-with-errors как отдельный исход, а не расплывчатый успех.
Правила для Cancel и Retry
Cancel должен быть явным: запрос отмены отправляется, воркер подтверждает и помечает задачу как canceled. Retry чаще всего создаёт новый job ID, сохраняя оригинал как историю, и объясняет, что будет переобработано.
Пример: импорт CSV с прогрессом и частичными ошибками
Распространённый кейс — импорт CSV. Представьте CRM, где sales ops загружает customers.csv с 8 420 строками.
Сразу после загрузки UI должен переключиться из состояния «я нажал кнопку» в «задача создана, можно уйти». Простая карточка в странице Imports подойдёт:
- Upload received: «File uploaded. Validating columns...»
- Queued: «Waiting for an available worker (2 jobs ahead).»
- Running: «Importing customers: 3,180 of 8,420 processed (38%).»
- Wrapping up: «Saving results and building a report...»
Пока идёт импорт, показывайте одну надёжную метрику (строки обработаны) и одну короткую строку статуса (что делает сейчас). Если пользователь ушёл, сохраняйте задачу в разделе Recent jobs.
Добавим частичные ошибки. По завершении избегайте пугающего баннера Failed, если большинство строк импортировано. Используйте «Finished with issues» и ясное разделение:
Imported 8,102 customers. Skipped 318 rows.
Объясните основные причины простыми словами: неверный формат email, пропущенные обязательные поля или дубликаты внешних ID. Позвольте скачать или просмотреть таблицу ошибок с номером строки, именем клиента и полем, требующим правки.
Retry должен быть безопасным и понятным. Основное действие — Retry failed rows: создаётся новое задание, которое повторно обрабатывает только 318 пропущенных строк после правки CSV. Оригинальная задача остаётся в режиме read-only для истории.
Наконец, делайте результаты легко доступными позже: у каждого импорта должна быть стабильная сводка — кто запустил, когда, имя файла, счётчики (imported, skipped) и ссылка на отчёт об ошибках.
Частые ошибки, приводящие к путанице с прогрессом и ретраями
Самый быстрый способ потерять доверие — показывать нереальные числа. Полоса прогресса, которая 2 минуты стоит на 0% и потом прыгает на 90%, выглядит как гадание. Если вы не знаете истинного процента, показывайте шаги (Queued, Processing, Finalizing) или «X of Y items processed».
Ещё одна проблема — хранение прогресса только в памяти. Если воркер перезапускается, UI «забывает» задачу или прогресс сбрасывается. Сохраняйте состояние в надёжном хранилище и делайте UI зависимым от единого источника правды.
UX повторов ломается, когда пользователь может запустить ту же задачу несколько раз. Если кнопка Import CSV остаётся активной, кто‑то кликнет дважды и создаст дубликаты. Тогда непонятно, какой прогон исправлять.
Повторяющиеся ошибки, которые встречаются часто:
- фальшивый процент, не соответствующий реальной работе
- технические дампы ошибок для пользователей (stack traces, коды)
- отсутствие обработки таймаутов, дубликатов и идемпотентности
- retry, который создаёт новое задание без объяснения последствий
- cancel, который меняет только UI, а не поведение воркера
Маленькая, но важная деталь: отделяйте сообщение для пользователя от деталей для разработчика. Показывайте пользователю «12 rows failed validation», а технический трассинг храните в логах.
Быстрая чек-лист перед релизом фоновых задач
Перед выпуском пройдитесь по вещам, которые видит пользователь: понятность, доверие и восстановление.
Каждая задача должна открывать снимок, который можно показать в любом месте: state (queued, running, succeeded, failed, canceled), progress (0–100 или шаги), короткое сообщение, метки времени (created, started, finished) и pointer на результат (где лежит вывод или отчёт).
Сделайте состояния UI очевидными и последовательными. Пользователям нужно одно надёжное место для текущих и прошлых задач и ясные метки при возвращении ("Completed yesterday", "Still running"). Панель Recent jobs часто предотвращает повторные клики и дубли.
Определите правила Cancel и Retry простыми словами. Решите, что значит Cancel для каждого типа, разрешён ли retry и что будет переиспользовано (входные данные или новый job ID). Протестируйте крайние случаи, например отмену прямо перед завершением.
Рассматривайте частичные ошибки как реальный исход. Показывайте краткое резюме («Imported 97, skipped 3») и давайте отчёт об ошибках, который пользователь сможет исправить.
Планируйте восстановление. Задания должны переживать рестарты, а зависшие задания — переводиться в понятное состояние с советом ("Try again" или "Contact support with job ID").
Следующие шаги: реализуйте один рабочий поток и расширяйте
Выберите один сценарий, по которому пользователи уже жалуются: импорт CSV, экспорт отчётов, массовая рассылка или обработка изображений. Начните с малого и докажите базу: задача создаётся, выполняется, отчитывается и пользователь может найти её позже.
Простой экран истории задач часто даёт самый большой прыжок качества. Люди получают место, куда вернуться, вместо того чтобы смотреть на спиннер.
Выберите сначала один метод доставки прогресса. Polling подходит для версии один. Настройте интервал так, чтобы он был щадящим для бэкенда и в то же время «живым» для пользователя.
Практический порядок построения, который помогает избежать переделок:
- реализовать состояния задач и переходы первым делом (queued, running, succeeded, failed, finished-with-errors)
- добавить экран истории задач с базовыми фильтрами (за последние 24 часа, только мои задачи)
- показывать числа прогресса только когда вы можете их держать честными
- добавлять отмену только после гарантии консистентной очистки
- добавлять retry только после уверенности в идемпотентности шагов
Если вы строите это без написания кода, no-code платформа вроде AppMaster поможет смоделировать таблицу статусов (PostgreSQL) и обновлять её из workflow, а затем рендерить статус в веб и мобильном UI. Для команд, которые хотят единое место для бэкенда, UI и фоновой логики, AppMaster (appmaster.io) предназначен для полноценных приложений, а не только форм и страниц.
Вопросы и ответы
Фоновая задача создаётся быстро и сразу возвращает идентификатор задания (job ID), чтобы UI оставался интерактивным. Медленный запрос заставляет пользователя ждать завершения одного веб-вызова, из-за чего люди обновляют страницу, кликают повторно и отправляют дубликаты.
Держите набор состояний простым: queued, running, done и failed, а также canceled, если поддерживаете отмену. Добавьте отдельный результат вроде «done with issues», когда большая часть работы выполнена, но некоторые элементы упали — это предотвращает ложное впечатление, что всё потеряно.
Сразу после старта действия возвращайте уникальный job ID, а затем отображайте строку или карточку задачи, основанную на этом ID. UI должен читать статус по job ID — тогда пользователь может обновлять страницу, переключать вкладки или вернуться позже и не потерять задачу.
Храните статус задания в надёжной базе данных, а не только в памяти. Сохраняйте текущее состояние, метки времени, значение прогресса, короткое сообщение для пользователя и итог/ошибки — тогда UI всегда сможет восстановить один и тот же вид после перезапуска.
Используйте процент только когда можно честно сказать «X из Y» обработано. Если нет достоверного знаменателя, показывайте шаги: «Validating», «Importing», «Finalizing» и т. п., чтобы пользователь видел постепенное движение вперёд.
Polling (опрос) — самый простой вариант: начните с 2–5 секунд, пока пользователь смотрит, затем замедляйте для долгих задач или фоновых вкладок. Push (WebSocket/SSE/уведомления) даёт моментальные обновления, но всегда нужен fallback. Для большинства приложений polling в первую версию — нормально.
Не притворяйтесь, что всё в порядке: показывайте, что обновления устарели, например «Last updated 2 minutes ago», и предлагайте обновить вручную. На бэкенде детектируйте отсутствие heartbeat и переводите задачу в понятное состояние с инструкцией (повторить или связаться с поддержкой по job ID).
Сделайте очевидным следующий шаг: можно ли продолжать работать, уйти со страницы или безопасно отменить задачу. Для задач длиннее минуты полезен отдельный экран Jobs/Activity, чтобы пользователь мог вернуться к результатам и не смотреть на спиннер.
Рассматривайте «завершено с ошибками» как отдельный результат и показывайте обе части: например, «Imported 9,380 of 9,500. 120 failed.» Затем дайте короткий и действующий список причин и возможность скачать отчёт по ошибкам — технические детали оставьте в логах.
Опишите, что означает Cancel для каждого типа задачи, показывайте промежуточный статус «cancel requested», делайте шаги идемпотентными, ограничивайте число ретраев и объясняйте, будет ли повторение делать ту же работу или создаст новый job ID с чистым следом аудита.


