Драка с виджетом СДЕКа
Размещал на одном сайте виджет для расчёта доставки.
Сложности начались сразу: виджету для работы нужна серверная прослойка. Заготовка прослойки дана только на ПХП, а у меня Нода. Но это ничего страшного — за десять минут с нейросетью переписал.
Всё подключил, запустил. Заметил, что виджет долго грузится. Открыл запросы и упал со стула:
Оказалось, что виджет скачивает все пункты выдачи независимо от того, какие из них будут видны на карте. Есть параметр для фильтрации пунктов по городу, но он косметический и на запрос не влияет.
Получается такая конфигурация:
Браузер <-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
.
- Дождаться в течение ~200 мс прихода стольки запросов, сколько есть виджетов, и передать на сервер только последний, вставив туда код ФИАС. Чтобы ничего не менять на сервере, параметр с кодом назвать
В скрипте на странице с виджетом:
- Подключиться к сервис-воркеру, передать туда код ФИАС и количество виджетов (один);
- Иметь поле для ввода города с получением его ФИАС-кода;
- Создать виджет, выдать ему айди (просто поле
id
в параметрах), записать экземпляр в переменнуюwidget
; - В событиях сравнивать
this.params.id
иwidget.params.id
. Если не совпадают, то событие в старом виджете, и его можно не выполнять; - При смене города:
- Обновить ФИАС-код и счётчик виджетов в сервис-воркере;
- Пересоздать виджет.
На сервере стоит сделать кэш.
Подытожим:
- Виджет СДЭКа скачивает мегабайты лишних данных;
- Если ограничить пункты выдачи одним городом, виджет всё равно скачает их со всего мира.
- Все ПВЗ, вероятно, хранятся реактивно. Из-за этого вкладка съедает гигабайт оперативной памяти и очень тяжело отрисовывается. На слабом телефоне браузер может лечь;
- Несмотря на реактивность, обновить параметры виджета нельзя;
- Удалить его из памяти тоже нельзя;
- Если пересоздать виджет, старые экземпляры будут реагировать на события в новом;
- И ещё я не рассказал, что в некоторых крупных городах (Казань, Хабаровск) все ПВЗ исчезают, если ограничить их отображение границами города через
fixBounds: 'locality'
. То есть этот параметр сломан.
Этот кошмар, для которого пришлось писать кучу обвязок, гордо называется третьей версией. Разработчик знает обо всех проблемах и обещает исправить всё в четвёртой... ну, обещал. Сначала релиз планировали на 30 июня 2024 — он не состоялся. Потом разработчик писал, что срок выпуска — «второй квартал». Какого года — неизвестно, и вряд ли этого.
6 мая 2025 Указатель