Главная / Статьи

Синхронизируйте загрузку нескольких вкладок с помощью API Web Locks

Прекратите дублирование возможных загрузок в разных вкладках браузера путем замены методов localStorage и BroadcastChannel на navigator.locks и координатор загрузок в React.

Издатель: REACTAPP.TOP6 мин чтения1360 слов

Источники: Handling concurrency on the Web with Web Locks API

Конкурентность означает возможность одновременной работы над несколькими задачами — или создание такого впечатления — чтобы программное обеспечение оставалось отзывчивым. Повар, который нарезает ингредиенты, пока одна сковорода томит блюдо, а другая запекает его, одновременно выполняет несколько задач, вместо того чтобы готовить блюда строго по порядку.

Раньше работа с фронтендом казалась далекой от классических многопоточных систем с блокировками, семафорами и заеданиями. Современная веб-платформа изменила это. Благодаря технологиям Workers, WebGL, Canvas и Service Workers браузерные приложения теперь сталкиваются с проблемами координации, очень похожими на конкуренцию за общую память.

Проблема

Команде по разработке платформы требовался надежный способ загрузки очень крупных файлов — объемом в десятки гигабайт, до примерно 20 ГБ. Просить пользователей держать вкладку открытой на протяжении всей передачи является нестабильным решением. Целью продукта была возможность возобновления загрузки: закрытие и повторное открытие вкладки должны были позволить продолжить передачу без необходимости вручную ее запускать заново.

Возможность возобновления вводит новый тип сбоев. Если несколько вкладок пытаются возобновить одну и ту же загрузку, каждая из них может начать передачу независимо. В результате хранилище данных получает дублирующиеся потоки данных, тратится пропускная способность, растут затраты на обработку, а интерфейс отображения прогресса может работать нестабильно из-за конкуренции вкладок за одну и ту же задачу.

Проблема в том, что каждая вкладка по отдельности выполняет свои действия разумным образом: восстановление сессии находит неполную запись загрузки, клиент возобновляет многокомпонентную передачу данных, а интерфейс отображает прогресс. Если таких вкладок две, то тот же «разумный» путь приводит к ситуации, когда два процесса пытаются записать данные в один и тот же удаленный объект.

Без координатора обе восстановленные вкладки считают, что им принадлежат данные для загрузки, и обе начинают отправлять байты. Для решения этой проблемы необходимо обеспечить взаимное исключение между вкладками; браузеры предоставляют для этого инструмент под названием Web Locks.

Прежде чем перейти к деталям реализации, стоит вспомнить, для чего нужны блокировки.

Что такое блокировка?

Блокировка сериализует доступ к общему ресурсу, чтобы пользователи видели его состояние в одинаковом виде. В классическом примере банка несколько банкоматов одновременно считывают и записывают данные о балансе счета. Без синхронизации операции снятия и внесения денег выполняются небезопасным образом.

С использованием блокировки первый пользователь, который её получает, выполняет операцию чтения или записи; остальные ждут её освобождения. Та же идея применима к правилу «только одна вкладка может возобновить эту загрузку».

Система координации между вкладками

Несколько старых подходов браузеров пытаются координировать действия вкладок. Они работают до появления ситуаций конкуренции, сбоев или ошибок протокола.

Вариант 1: localStorage + опросы

Распространённый вариант реализации мьютекса хранит флаг в localStorage:

// Simplified example - DON'T use this
if (!localStorage.getItem('upload-lock')) {
  localStorage.setItem('upload-lock', Date.now());
  // Start upload
}

Проблемы проявляются быстро:

  • Ситуации конкуренции: операции чтения и записи не являются атомарными
  • Устаревшие блокировки: сбойная вкладка оставляет флаг установленным навсегда
  • Накладные расходы от опросов: другие вкладки должны постоянно проверять состояние
  • Ограничения событий: события хранения генерируются в других вкладках, а не у той, что выполняет запись

Вариант 2: API Broadcast Channel

Вкладки могут взаимодействовать при передаче сообщений между документами с помощью BroadcastChannel:

const channel = new BroadcastChannel('upload-coordination');
channel.postMessage({ type: 'REQUEST_UPLOAD_PERMISSION' });

Однако остаются проблемы:

  • Отсутствие примитива мьютекса: приходится самостоятельно создавать машину состояний
  • Условия соревнования: перекрывающиеся сообщения по-прежнему вызывают конфликты
  • Сложный протокол: механизмы проверки работоспособности, обнаружения сбоев и правила разрешения конфликтов реализуются вручную
  • Высокая вероятность ошибок: часто встречаются проблемы с порядком обработки и временем выполнения

Появление API Web Locks: встроенный мьютекс браузера

Встроенные Web Locks предоставляют атомарный мьютекс браузера. Среди полезных свойств — следующие:

  • Атомарное получение блокировки — избегает проблем соревнования при использовании localStorage
  • Автоматическая очистка — закрытие вкладки или её сбой приводят к освобождению блокировки
  • Неблокирующие опции — позволяют сохранять отзывчивость интерфейса
  • Простой API — отсутствие собственного протокола обмена сообщениями
  • Совместимость с браузерами — указана в таблице совместимости MDN для текущих движков
  • Наше решение: useUploadCoordinator

    Специальный хук React оборачивает логику управления процессом загрузки.

    Интерактивные запуски загрузки следует отличать от автоматического восстановления. Когда пользователь явно выбирает файл, предпочтительнее использовать неблокирующий способ получения доступа, чтобы интерфейс не ждал в другой вкладке. Если доступ свободен, его следует занять и начать загрузку; если он уже занят, решение принимается в соответствии с правилами продукта, а не путем замораживания страницы.

    При восстановлении загрузок необходимо соблюдать более строгие правила: продолжение передачи данных должно осуществляться только первой вкладкой, которая вновь получает контроль. Последующие вкладки, у которых недоступен доступ к файлу, должны оставить текущего владельца в покое, а не открывать второй поток передачи данных.

    // 1. Fresh uploads (user-initiated): Non-blocking
    // Try to get the lock, but proceed regardless
    navigator.locks.request(LOCK_NAME,
      { ifAvailable: true },
      async (lock) => {
        if (lock) {
          // Hold the lock until upload completes
          await holdLockUntilComplete(abortController);
        }
        // Upload proceeds either way - never block the user
      }
    );
    
    // 2. Restored uploads: Conditional
    // Only proceed if we get the lock
    navigator.locks.request(LOCK_NAME,
      { ifAvailable: true },
      async (lock) => {
        if (lock) {
          uploader.emit("restore-confirmed"); // Proceed with restore
          await holdLockUntilComplete(abortController);
        }
        // If no lock, skip silently - another tab is handling it
      }
    );
    

    Основные детали реализации

    Удерживайте блокировку до завершения передачи или её отмены:

    const holdLockUntilComplete = (abortController: AbortController) => {
      return new Promise<void>((resolve) => {
        const cleanup = () => {
          // Remove all listeners and resolve (releasing the lock)
          uploader.off("complete", cleanup);
          uploader.off("error", cleanup);
          uploader.off("cancel-all", cleanup);
          resolve();
        };
    
        // Release lock on any terminal event
        uploader.on("complete", cleanup);
        uploader.on("error", cleanup);
        uploader.on("cancel-all", cleanup);
    
        // Also release if component unmounts
        abortController.signal.addEventListener("abort", cleanup);
      });
    };
    

    Обнаруживайте наличие поддержки и безопасно переходите на резервный режим при отсутствии API:

    const checkWebLocksAvailable = (): boolean => {
      return typeof navigator !== 'undefined' && 'locks' in navigator;
    };
    
    // Fallback behavior
    if (!checkWebLocksAvailable()) {
      // Allow restores to proceed (accept risk of duplicates)
      uploader.emit("restore-confirmed");
      return;
    }
    

    Практические преимущества

    После внедрения координатора команда отметила следующее:

    1. Отсутствие дублирующихся загрузок из-за конкуренции при восстановлении через несколько вкладок
    2. Улучшенный пользовательский опыт — координация не замедляла работу интерфейса
    3. Снижение затрат на инфраструктуру — меньше дублирующихся передач и хранимых объектов
    4. Упрощение кодовой базы — отсутствие циклов опроса или ручно созданных машин состояний

    Только та вкладка, которая получает блокировку, продолжает загрузку; остальные обнаруживают, что блокировка занята, и прекращают действия.

    Аспекты, связанные с реальными условиями использования

    Когда использовать эту схему:

    • Загрузка файлов, которые восстанавливаются после перезагрузки
  • Синхронизация на фоне, которая должна выполняться один раз
  • Любые задачи типа singleton, которые не должны выполняться в параллельных вкладках
  • Влияние на производительность:

    • Минимальное — отсутствует цикл опроса
    • Автоматическая очистка ограничивает количество незавершённых операций
    • Неблокирующие запросы сохраняют отзывчивость интерфейса

    Стратегия тестирования:

    • Проверять каждый поддерживаемый браузер, который вы используете
    • Имитировать внезапное закрытие вкладок при наличии блокировки
    • Подтверждать поведение в режиме фолбэка при недоступности Web Locks

    Заключение

    Координация между вкладками не требует хруплого пользовательского протокола. Используйте встроенный механизм mutex Web Locks платформы: он автоматически очищает ресурсы при закрытии вкладки и поддерживает неблокирующее получение блокировок. Подробности API описаны в документации Web Locks.

    Практические правила, которые сработали в производственных условиях:

    1. Никогда не блокируйте действия пользователя — предпочитайте неблокирующие механизмы блокировки для действий, инициированных пользователем
    2. Обеспечивайте безопасное отклонение ошибок — снижайте качество работы вместо того, чтобы полностью разрушать процесс загрузки
    3. Сохраняйте простоту — используйте встроенные инструменты платформы вместо их повторного создания

    Эти три правила важнее любого конкретного названия вспомогательного инструмента. Сложный координатор, который блокирует выполнение действий при клике, будет работать хуже, чем простой альтернативный вариант, который время от времени просит пользователя попробовать снова. Аналогично, умный протокол обмена сообщениями, который игнорирует необходимость устранения сбоев, вновь создаст проблему устаревших блокировок, из-за которых использование localStorage становилось сложным.

    Тот же принцип применяется всякий раз, когда табы должны выбрать единого владельца для совместной работы, а не только при загрузке крупных файлов. Очереди фоновой синхронизации, инструменты обновления данных в автономном режиме и скрипты однократной миграции все они выгодно используют ту же схему «один победитель, множество вежливых проигравших».