Дима

Драка с виджетом СДЕКа

Размещал на одном сайте виджет для расчёта доставки.

Сложности начались сразу: виджету для работы нужна серверная прослойка. Заготовка прослойки дана только на ПХП, а у меня Нода. Но это ничего страшного — за десять минут с нейросетью переписал.

Всё подключил, запустил. Заметил, что виджет долго грузится. Открыл запросы и упал со стула:

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

Получается такая конфигурация:

Браузер <-3МБ─── Сервер <-3МБ─── СДЭК

Скачать 3 мегабайта — это само по себе ощутимое время, а так оно умножается на два. И если писать свою реализацию прослойки, то можно по неосторожности нарваться на лишние распаковку и сжатие.

Первая мысль: сделать кэш, чтобы отсечь правую половину трафика. Так я и поступил. Время загрузки снизилось до двух секунд — приемлемо, с этим можно работать, например подгружать заранее.

Браузер <-3МБ─── Сервер

Протестировал с телефона и заметил, что страница с виджетом жутко зависает. Немного порылся под капотом, и, насколько я понял, виджет хранит все 8211 пунктов выдачи (23 мегабайта без сжатия) в прокси-объектах. Учитывая, что каждый пункт выдачи — это объект с 19 примитивами и 7 вложенными объектами, всего получается 131 377 прокси-объектов. Добавим ещё карту, и с такой математикой неудивительно, что вкладка с виджетом легко съедает 900+ мегабайт оперативной памяти даже на телефоне, где её стоит экономить. Телефон при этом потеет и задыхается. Вкладка about:processes не даст соврать:

Стало понятно, что кэшем не отделаться. Столько данных виджету давать просто нельзя:

        -> Виджет ───Все пункты выдачи→ Сервер
х_х ←20×── Виджет ←23МБ── Браузер ←3МБ───────┘

Я решил перехватывать запросы виджета и подставлять туда параметр для фильтра по городу (да, АПИ СДЭКа это поддерживает). Для этого использовал сервис-воркер — скрипт, который работает в фоне и видит запросы.

На странице с виджетом у меня уже лежало поле с выбором города, так что решил фильтровать по нему. В итоге получилось так:

        -> Форма ─Город─┐
        -> Виджет ─Все→ Воркер ───Город→ Сервер
3МБ ←20×── Виджет ←168КБ── Браузер ←16КБ──────┘

Остаётся проблема: если город поменяется, то как заставить виджет запросить новые пункты выдачи? Метод updateLocation не делает ничего кроме перемещения карты. В документации вообще сказано, что «после создания виджета конфигурацию изменить нельзя» — и это при том, что виджет использует тяжёлую реактивную библиотеку.

Получается, единственный способ что-то обновить — создать новый виджет и удалить старый. Встроенный метод destroy продолжает традицию ничего по существу не делать: его вызов убирает виджет из DOM, но не убирает экземпляр из памяти. При этом все события «уничтоженного» виджета вызываются вместе с новыми:

Получается, что если город клиента определился неправильно, и он его поменяет, то все запросы будут выполняться по нескольку раз — это не дело! Разобраться с причиной у меня не хватило времени (создание в другом элементе не помогло, значит коллизия обработчиков событий, вероятно, ни при чём), поэтому я решил пойти не самым элегантным, но эффективным путём:

  • Выдавать виджетам айди, в обработчиках событий пропускать только те, что пришли из последнего виджета;
  • Запросы, которые идут изнутри виджета, ловить сервис-воркером и отпускать на сервер только последний. Остальным отдать маленький фейковый ответ, иначе из-за ошибки в одном виджете упадут все.

Теперь, если клиент меняет город несколько раз, схема такая:

        -> Форма ─Город─┐
        -> Виджет ─Все→ Воркер ───Город→ Сервер
           Виджет ─Все→─┤                     │
           Виджет ─Все→─┤                     │
           Виджет ─Все→─┘                     │
3МБ ←20×── Виджет ←168КБ── Браузер ←16КБ──────┘

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

В сервис-воркере:

  • Принимать код города пользователя в ФИАС и текущее количество виджетов;
  • Слушать запросы на /cdek?action=offices&page=0. Для каждого запроса:
    • Дождаться в течение ~200 мс прихода стольки запросов, сколько есть виджетов, и передать на сервер только последний, вставив туда код ФИАС. Чтобы ничего не менять на сервере, параметр с кодом назвать fias_guid;
    • Остальные отправить обратно с фейковым ответом. Я передаю массив с одним пунктом выдачи, в котором оставил только поля code, name, uuid и location.

В скрипте на странице с виджетом:

  • Подключиться к сервис-воркеру, передать туда код ФИАС и количество виджетов (один);
  • Иметь поле для ввода города с получением его ФИАС-кода;
  • Создать виджет, выдать ему айди (просто поле id в параметрах), записать экземпляр в переменную widget;
  • В событиях сравнивать this.params.id и widget.params.id. Если не совпадают, то событие в старом виджете, и его можно не выполнять;
  • При смене города:
    • Обновить ФИАС-код и счётчик виджетов в сервис-воркере;
    • Пересоздать виджет.

На сервере стоит сделать кэш.


Подытожим:

  • Виджет СДЭКа скачивает мегабайты лишних данных;
  • Если ограничить пункты выдачи одним городом, виджет всё равно скачает их со всего мира.
  • Все ПВЗ, вероятно, хранятся реактивно. Из-за этого вкладка съедает гигабайт оперативной памяти и очень тяжело отрисовывается. На слабом телефоне браузер может лечь;
  • Несмотря на реактивность, обновить параметры виджета нельзя;
  • Удалить его из памяти тоже нельзя;
  • Если пересоздать виджет, старые экземпляры будут реагировать на события в новом;
  • И ещё я не рассказал, что в некоторых крупных городах (Казань, Хабаровск) все ПВЗ исчезают, если ограничить их отображение границами города через fixBounds: 'locality'. То есть этот параметр сломан.

Этот кошмар, для которого пришлось писать кучу обвязок, гордо называется третьей версией. Разработчик знает обо всех проблемах и обещает исправить всё в четвёртой... ну, обещал. Сначала релиз планировали на 30 июня 2024 — он не состоялся. Потом разработчик писал, что срок выпуска — «второй квартал». Какого года — неизвестно, и вряд ли этого.