Иногда в разработке случается баг, который не просто не даёт спать, а заставляет пересмотреть свои жизненные ценности.
У меня это произошло со скромным всплывающим окном внутри WebView.
Задача была максимально скучной: на экране с WebView пользователь нажимает кнопку “Exit”, а веб-страница показывает попап подтверждения. На старом коде — всё идеально. На браузере — идеально. На новом инфраструктурном слое WebView — попап появляется на миг и тут же закрывается сам, как будто кто-то тайно играет в “крестики” за пользователя.

Никаких ошибок. Никаких исключений.
Просто “блип”… и пустота.
То самое чувство, когда ты нажимаешь кнопку, а мир делает вид, что “ничего не было”.
И это — пролог к истории о том, как два дня моей жизни сгорели на алтаре WebView.
Немного контекста
Экран устроен просто: сверху нативный контейнер, внутри WebView, загружающий веб-витрину.
В вебе есть кнопка “Exit”.
Она открывает попап “Точно выйти?”.
Пользователь выбирает — всё счастливы.
На старой реализации (через Accompanist WebView) всё работало.
Но проект рос, экранов с WebView становилось больше, и мы решили сделать нормальный инфраструктурный слой:
единый контейнер
пререндер WebView
реюз экземпляров
метрики — время до первого кадра и т.д
аккуратные JS-бриджи;
предсказуемый жизненный цикл.
Сверху всё выглядело более чем прилично.
Пока мы не нажали ту самую кнопку “Exit”.
Симптом: попап живёт меньше секунды
Сценарий:
старая реализация → попап висит, можно нажимать
новая реализация → попап мелькает и исчезает сам.
Без крэшей. Без ошибок.
Ни тебе stacktrace, ни даже “undefined is not a function”.
Тот самый случай, когда ощущаешь себя не разрабом, а шаманом с бубном.

Подозрение №1: cookies — стандартный злодей
Первое, что приходит в голову: конечно же, куки.
Ну кто ещё?
Если где-то пропала авторизация — веб обычно начинает творить странные вещи.
Проверили всё:
куки на месте
домены совпадают
значения совпадают
WebView их видит
браузер видит
старая версия видит
Куки невиновны.
Подозрение №2: JS-мосты
Если не куки — значит, сломали что-то в порядке навешивания бриджей:
JSBridge
NativeBridge
WebViewClient
ChromeClient
детектор рендера
Мы проверили:
все интерфейсы доступны
сообщения приходят
в консоли нет ошибок
порядок навешивания корректный
JS-мост жив-здоров -- Баг тоже)
Подозрение №3: “Это точно пререндер”
Хорошо. Выключаем пререндер.
Оставляем максимально простой сценарий:
new WebView
loadUrl
никаких кешей
никаких магических оптимизаций.
Попап всё равно умирает, как будто у него срок годности истёк.
Когда нативные логи уже не помогают
Мы начали логировать всё: размеры, прикрепление к окну, моменты loadUrl, настройки, cookie, debug-информацию.
Логи выглядели примерно так:
POST: size=1080x2165, attached=true
POST: loadUrl(…)
То есть WebView успевает встроиться в дерево, получает размер, и только потом начинаем загрузку.
Всё корректно.
С точки зрения Android — вообще идеальная картинка.
Но попап всё равно продолжает исчезать.
И тут мы попросили web-разработку включить свои логи
Это был поворотный момент.
На веб-стороне мы включили логирование компонента, который отвечает за попап.
И увидели следующее:
ContextMenu rendering (hidden=false)
ContextMenu mounted
ContextMenu hidden state changed (hidden=false)
ContextMenu rendering…
ContextMenu rendering placement=‘top’
ContextMenu rendering placement=‘top’
ContextMenu is out of viewport → closing
ContextMenu handleClose
popup closed
Вот оно. Фронтенд САМ закрывает попап, потому что считает, что он…
вылетел за пределы вьюпорта.
Почему старый WebView считался “вьюпортно правильным”, а новый — нет?
Фронтенд обычно проверяет попадание элемента в экран через getBoundingClientRect и window.innerHeight.
И вот тут — магия WebView:
нативный контейнер стал другим
изменились Insets
состав обёрток вокруг WebView сменился
высота вычисляется чуть иначе
браузерный layout внутри WebView получает другую “геометрию”
Разница может быть даже в 1–2 пикселя.
Но если логика попапа строгая — она решает “элемент частично вне экрана” → закрыть.
Что с этим делать
Варианты фиксов на стороне веба:
-
Сделать проверку менее строгой.
Например, разрешить на 10–20 px вылет, если это безопасно.
-
Добавить fallback-режим для WebView.
Если User-Agent содержит android-webview — использовать другой алгоритм вычисления.
Отключить проверку “вьюпорта” для критичных попапов.
Использовать другой метод позиционирования (не getBoundingClientRect).
Финал
Этот баг стал отличным напоминанием:
WebView — это не “браузер внутри приложения”,
а два разных мира, которые живут рядом и каждый со своими правилами.
Если ваша команда когда-нибудь поймает похожий баг — надеюсь, эта история сэкономит вам пару суток и немного нервных клеток.