RTP DesportoRui Borges prepara Sporting para combater intensidade coletiva do LensESPN DeportesContragolpe: Wilfredo Gómez y la fragilidad de los ídolosDaily MaverickCRICKET: Australia on the ascendancy against Proteas in fruitful fourth day of first TestThe Jerusalem PostRoyal Ballet star Natalia Osipova returns to Israel with world premiere by Israeli choreographerESPN49ers' Greenlaw suspended for head-butting helmetless Seahawks LBBillboard‘What Could Be Better?’ See Ariana Grande in New ‘Focker-In-Law’ PostersZDF heuteEntdecken Sie das ZDF-NachrichtenstudioCBS NewsSaudi airport stays shut as Houthi attacks kill 20 people in a weekBBC NewsBritain won't tolerate threats on our soil, Miliband says as he summons Iran ambassadorBBC عربيالحوثيون يعلنون استهداف مطار الرياض وقاعدة خميس مشيط، ويتوعدون الولايات المتحدة "اذا تورطت في العدوان على اليمن"GMA NewsHouthis claim attack targeting Riyadh airport with ballistic missileVanguardContractors have no reason to stay away from project sites – Kano govt
The Daily Newsstand · Free, Always
Monday, October 12, 2026

Переезд с Leaflet на MapLibre: детектив про один рывок и 251 маршрут, проверенный байт в байт

Translate

История начинается с самой неблагодарной формулировки бага, какая бывает:

«Иногда при зуме карту дёргает».

Не «всегда», не «на таком-то действии», не с шагами воспроизведения. Иногда. Причём — цитата — «иногда зум работает хорошо». Любой, кто ловил плавающие баги, сейчас сочувственно поморщился.

Закончилось это тем, что я выбросил картографический движок и переписал под другой всю карту сервиса. Рассказываю, как я туда доехал, что мерил по дороге и чем потом доказывал себе, что не сломал людям сохранённые маршруты.

Это четвёртая статья про мой планировщик походов; предыдущие читать необязательно, здесь всё самостоятельное.

Откуда взялся мост

Карта на сайте была на Leaflet — проверенная, простая, сотни мест в коде на неё завязаны. Но мне понадобился собственный векторный стиль: тропы заметнее дорог, горизонтали, текстуры скал, свои иконки перевалов и родников. Это MapLibre GL, то есть WebGL.

Переписывать всё ради одного слоя было немыслимо, поэтому я подключил официальный мост: MapLibre живёт внутри Leaflet как слой. Leaflet управляет камерой, мост транслирует её в GL-карту. Решение абсолютно стандартное и много где работает.

И оно работало. Кроме зума, который иногда дёргало.

Четыре гипотезы, которые не подтвердились

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

Гипотеза 1: гонка при жесте щипком. В мосте есть обработка пинч-зума, и там логично искать гонку между жестом и анимацией. Написал патч, который учитывает признак жеста. Эффекта — ноль. Причём дёргало и на колесе мыши, где никакого пинча нет; это стоило бы заметить раньше.

Гипотеза 2: браузер рисует без GPU. Классика: если WebGL свалился на программный рендер, всё именно так и выглядит. Проверил — и обнаружил, что Chrome действительно рисует софтом. Момент триумфа. Почистил кэш шейдеров, убедился, что GPU включился по-настоящему... дёргание осталось. Гипотеза подтвердилась как факт, но оказалась не причиной — отдельный жанр ловушки: вы нашли настоящую проблему, она просто не та.

Гипотеза 3: целочисленный зум. Leaflet по умолчанию прыгает по целым уровням, а GL-карта умеет дробные. Похоже на источник рассинхрона? Похоже. Поставил шаг 0.25 — не помогло даже на мелком шаге.

Гипотеза 4: просто тормозит. Её убивала сама формулировка бага: «иногда работает хорошо». Равномерный тормоз не бывает иногда. Это гонка.

Гипотеза 5, решающая. Выключил анимацию зума у Leaflet целиком — дёргание исчезло полностью. Вот и причина.

Механизм такой. Во время анимации зума Leaflet берёт снимок содержимого и растягивает его средствами CSS — дёшево и плавно. А настоящий GL-кадр подменяет снимок только по завершении анимации. Пока содержимое — обычные тайлы-картинки, разницы не видно. Но растянутый снимок WebGL-канваса и честно отрисованный кадр — это заметно разные картинки, и момент подмены виден как рывок. «Иногда» — потому что зависит от того, успел ли GL отрисоваться к моменту подмены.

То есть дело не в производительности и не в настройках. Дело в шве между двумя моделями отрисовки.

Развилка

Вариантов было два, и оба неприятные.

Первый: оставить анимацию зума выключенной. Одна строчка. Но это опция карты, а не слоя — плавность зума пропадает на всём сайте, для всех. Меняем рывок на вечную рваность. Так себе сделка.

Второй: переписать мост так, чтобы GL-камера синхронизировалась непрерывно. Правка вендорного кода, который потом придётся сопровождать самому и при каждом обновлении библиотек проверять заново.

Есть и третий, о котором я до этого старался не думать: убрать мост. Оставить один движок. Не «Leaflet, внутри которого MapLibre», а просто MapLibre.

Именно шов был причиной бага. Шва не будет — не будет и класса багов, который из него растёт. Плюс длинные треки на GL рисуются быстрее, а вся моя карта и так уже была стилем MapLibre — Leaflet, по сути, остался в роли рамки с кнопками зума.

Я выбрал третий.

Как переезжать так, чтобы потом спать

Вот тут начинается часть, которая мне самому нравится больше всего, потому что она про страх.

На сервисе живые люди: На тот момент 427 пользователей, 251 сохранённый маршрут. У каждого — сегменты, метки, цвета, разбивка по дням. Это данные, которые человек собирал руками и не сможет восстановить, если я их испорчу. Переписывание всего слоя работы с картой — ровно тот случай, когда что-то ломается тихо и обнаруживается через месяц.

Схему базы я не трогал принципиально: переезд был только в отрисовке. Но «я вроде ничего не менял» — это не доказательство, это самоуспокоение.

Поэтому я сделал сверку на свежей копии боевой базы. Старая версия сайта и новая открывают каждый из 251 маршрута, правят, сохраняют и экспортируют — и результаты сравниваются байт в байт. Не «глазами похоже», а файл против файла; слепки вышли по 175 МБ и совпали целиком.

Отдельно — точка отката, подготовленная до выкладки, а не после начала пожара:

  • метка в git на последнем коммите старой версии;

  • копия базы на сервере, проверенная на целостность;

  • копия конфига веб-сервера.

Откат — одна команда. Стоило это полчаса, и полчаса за спокойный сон в ночь после выкладки большой правки — это, по-моему, лучший курс обмена в индустрии.

Что дал переезд, в цифрах

Дёргание ушло — как и ожидалось, вместе со швом. Но были и приятные побочные эффекты.

Километровые метки рисует карта, а не HTML. Раньше каждая отметка «5 км» была обычным элементом страницы, который браузер позиционировал поверх карты. Пока меток десяток — нормально. На треке 47 км в трёхмерном режиме кадр занимал 65 мс. После переноса меток в слой карты — 12 мс.

Свои обработчики мыши молчат, пока двигают камеру. Были провалы кадра до 572 мс — это когда карта уже поехала, а мой код всё ещё пересчитывает, что там под курсором. Теперь не пересчитывает.

Сетка рельефа берётся с 12-го зума вместо 14-го: 27 → 12 мс.

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

И объёмные здания — причём они оказались бесплатными. Высоты уже лежали в архиве карты: замер на Москве показал 4145 зданий на экране, у 80% высота есть, медиана 14 м. Никакой пересборки тайлов, никакого трафика, кадры не изменились (78 против 82 при вращении камеры).

Но с ними есть тонкость, которая стоит отдельного абзаца.

Порядок слоёв решает больше, чем кажется

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

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

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

А ещё была прекрасная в своей бессмысленности ошибка со второй картой в режиме сравнения. Я скопировал на неё источник и слой километровых меток — они не появились. Слой есть, источник есть, данные есть, меток нет.

Разгадка: плашки меток не лежат в спрайте, их рисует canvas по событию «нужной картинки нет». А обработчик этого события подписан на конкретный экземпляр карты. Вторая карта его не унаследовала и молча рисовала символы без картинок.

Вторая карта MapLibre не наследует ничего, что вы повесили на первую. Скопировать источники и слои — мало.

Там же рядом — мой любимый вид ошибок: вспомогательная функция добавления источника принимает карту третьим аргументом, а соседняя функция добавления слоя — четвёртым. Вызвал по образцу соседней, получил TypeError ... reading 'sources' в фоновом таймере, то есть в консоли и без внятного стека. Слой «не появился». Мораль: когда слой не появился — сначала проверьте, создался ли источник.

Тепловая карта и лучи через полрайона

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

Причина — в форме данных. У меня есть фильтр, который выбрасывает подозрительно длинные прямые перегоны (человек не идёт 1,8 км по идеальной прямой, это значит, что маршрут нарисован, а не проложен). Фильтр требовал, чтобы между точками была промежуточная геометрия. А нарисованная вручную прямая лежит как одно ребро между двумя кликами — промежуточной геометрии у неё нет по определению. Такие отрезки фильтр не выбрасывал никогда.

Замер на боевой базе: 46 рёбер длиннее 800 м в 18 маршрутах, самое длинное — 1795 м. После починки — 0. Линий на карте стало 621 → 645, то есть ни один нормальный маршрут не потерялся.

Добавил заодно защиту от «рисования»: если больше 70% длины маршрута — прямые перегоны, в тепловую карту он не идёт вовсе. Рано или поздно кто-нибудь напишет треком слово, и пусть он напишет его себе, а не всем. Есть же такие приколисты которые любят где не попадя писать слово йух , только наоборот. Ладно признаюсь честно, я сам когда то таким был :D

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

Что удалили

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

Были слои погоды: облачность со спутника и осадки. Сделаны, работали, отображались — и «отображается непонятно как». Осадки поверх походной карты без легенды и без времени съёмки — это не информация, это цветные пятна. Удалил вместе с серверным прокси.

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

Маленькая грабля на выкладке

Уже после переезда я заметил, что веб-сервер отдаёт наружу файлы самопроверок рядом с обычными скриптами. Не секрет, но и не то, что стоит публиковать.

Добавил правило-запрет — и оно не сработало. Потому что побеждает первое подошедшее регулярное правило, а у меня выше уже стояло общее правило для скриптов. Переставил запрет выше — заработало.

Универсальное: запрет, который стоит после разрешения, — это не запрет, а украшение конфига.

Итог

Три вещи, которые я забираю с собой:

1. Ищите шов. Когда баг «иногда», подозревайте не код, а границу между двумя системами, у которых разные представления о времени и об отрисовке. Мой рывок жил ровно на стыке CSS-анимации и WebGL-кадра, и никакая настройка его убрать не могла, потому что настройки были не при чём.

2. Подтвердившаяся гипотеза — ещё не причина. Я нашёл, что Chrome рисует софтом. Это была правда. Это была не та правда.

3. Перед большой правкой сделайте себе способ доказать, что ничего не сломалось. Не «проверил глазами десяток случаев», а прогон всех живых данных через старую и новую версию со сравнением байт в байт. Это заняло у меня меньше времени, чем детектив с зумом, и дало то, чего детектив не даёт вовсе, — спокойствие.

Сервис живёт по адресу putemer.ru. Карта там теперь на одном движке, зум не дёргается, а дома в трёхмерном режиме стоят под подписями, а не поверх.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.