или почему WebView-пререндер — не костыль, а инвестиция в UX и бизнес
Вечный бой: Android-разработчик vs WebView
Если вы когда-нибудь трогали WebView в боевом проекте — вы уже знаете это чувство.
Ты такой всё красиво посчитал: App Startup, TTFB, время рендеринга…
А потом:
“Экран "X" иногда открывается моментально, а иногда — секунд через пять.”
QA в недоумении, пользователи в отзывах пишут про «белый экран», а ты смотришь в логи и не понимаешь — кто вообще виноват: сеть, бэкенд, фронтенд, Chromium или твоя карма.
WebView — самый капризный компонент Android:
долго инициализируется (особенно при первом запуске);
прожорлив по памяти;
любит показывать белый экран;
и коварный момент: сложно поймать ту самую секунду, когда пользователь реально увидел контент, а не просто белое полотно.

Это та самая «серая зона», где Android и Web встречаются и начинают устраивать сюрпризы.
Мы решили с этим завязать и вместо очередного «кажется стало быстрее» сделать нормальную измеримую инфраструктуру.
Что мы хотели получить
Две понятные цели:
Пререндеринг WebView — чтобы экран открывался визуально мгновенно.
Метрика Time To Visual Ready (TTVR) — чтобы можно было не верить ощущениям, а смотреть в числа.
Чтобы это заработало, мы собрали три кирпича:
WebViewPreloader — сервис, который греет и пререндерит WebView заранее.
WebViewReadyDetector — мини-детектор «страница реально отрисовалась».
CoreComposeWebView — умный контейнер над AndroidView(WebView) с пулом, детектором и метриками.
⚙️ Архитектура
1. WebViewPreloader — «разогрей, пока никто не видит»
Идея простая:
пока пользователь куда-то жмёт, мы в фоне уже создаём WebView, инициализируем Chromium и грузим нужный URL.
Когда он доходит до экрана — мы не начинаем всё с нуля, а просто достаём готовый экземпляр.
Под капотом WebViewPreloader делает примерно следующее:
создаёт WebView в невидимом контейнере;
прогревает движок (пустой WebView + инициализация);
вызывает что-то вроде prerenderUrlAsync(url, options, callback);
по завершении сохраняет инстанс в Map<String, WebView>;
дальше этот WebView можно забрать через takePreloaded(url) и вставить на экран.
При этом у нас есть cookie-политика:
UsePreloaded — можно показать немного устаревший контент, зато быстро. Окей для не критичных к auth штук.
DropAndFresh — игнорируем пререндер, если auth-cookie поменялся. Медленнее, но меньше шансов показать «не того пользователя».
Пример вызова при старте приложения в AppStartup Initializer'e
class WebViewPreloadInitializer : Initializer<Unit>, KoinComponent {
override fun create(context: Context) {
val jobs = listOf(
PrerenderJob(url = "https://habr.com", cookies = "кука/куки")
)
webViewPreloader.preloadWebviews(jobs)
}
}
Важно: WebViewPreloader не привязан к App Startup. Его можно запускать:
из ViewModel;
из фичи;
из эксперимента;
из onboarding-флоу.
То есть любой «важный» WebView-экран можно подогреть одной строкой, когда вы понимаете, что пользователь вот-вот туда придёт.
1.1.Что за prerenderUrlAsync в AndroidX WebKit и как его правильно готовить?
prerenderUrlAsync — экспериментальный (alpha) API из AndroidX WebKit.
Он позволяет заранее подготовить рендер страницы, ещё до того, как у вас есть реальный видимый WebView на экране.
Примерно так это живёт:
-
Запрос на пререндер
Вы вызываете prerenderUrlAsync(url, options, callback) где-то в фоне.
WebView на экране ещё нет.
-
Фоновая работа
Движок под капотом создаёт изолированный контекст, начинает грузить страницу, крутит свои таймеры, выполняет часть логики.
-
Готовность
В колбэк прилетает «готово» (или ошибка/таймаут).
Это сигнал: если сейчас показать страницу — первый кадр отрисуется очень быстро.
-
Активация
Когда пользователь открывает экран, вы:
либо используете уже готовый WebView (если API позволяет вернуть его),
либо «подключаете» подготовленный рендер к вашему WebView.
-
Отмена
Пользователь передумал и не дошёл до экрана? Пререндер можно и нужно отменить, чтобы не держать лишнюю память.
Нюанс: поведение зависит от версии Android System WebView / Chromium. Где-то он ведёт себя елегантно, где-то — не очень. Поэтому:
ставим таймауты,
всегда имеем fallback на «обычную» загрузку,
собираем метрики: сработал prerender, был таймаут, не поддерживается и т.д.
Есть два базовых подхода:
Подход |
Плюсы |
Минусы |
Держать невидимый WebView |
Предсказуемо и работает везде; вы контролируете экземпляр |
Нужен реальный View в иерархии/контексте (даже offscreen); риск утечек; overhead по памяти |
prerenderUrlAsync |
Системный путь: движок сам решает, что и когда готовить; меньше шансов на баги уровня View; потенциально лучше по памяти |
Новое API (alpha), поддержка не везде; поведение зависит от версии WebView; нужно аккуратно кодить fallback |
На практике это не «или–или», а «и то, и другое»:
мы смотрим, поддерживается ли prerenderUrlAsync, по фичефлагу включаем его, а при проблемах падаем на проверенный сценарий с обычным прогретым WebView.
Практические советы по пререндеру
То, что бы я себе самому написал в README:
-
Таймаут 6–10 секунд
Если страница не успела подготовиться — не героизируем, логируем и уходим во fallback.
-
Не пререндерим всё подряд
Выбираем топовые экраны: главный WebView, часто посещаемые сцены, критичные флоу.
-
Следим за cookie
Поменялись auth-данные — старый пререндер либо не используем, либо считаем его stale и честно маркируем в метриках.
-
Логируем стратегию
REASON=API, REASON=FALLBACK, REASON=TIMEOUT, REASON=UNSUPPORTED.
Потом это сильно помогает понять: регрессия в сети, во фронте или в самом пререндере.
-
QA-панель
Маленькая debug-строчка «Prerender: API/Fallback/Skipped» в overlay — и сразу ясно, что именно сейчас тестируется.
2. WebViewReadyDetector — как понять, что страница «живая», а не просто белая
А теперь главный вопрос: когда считать, что WebView реально что-то показал?
По onPageFinished() верить нельзя — там всё, что угодно, кроме реальной «готовности визуала».
Поэтому мы сделали простую, но рабочую штуку — WebViewReadyDetector.
Он делает:
offscreen-рендер WebView в маленький Bitmap, условно 48×48;
считает долю не белых пикселей;
как только заполненность превышает порог (например, 2%) несколько кадров подряд — считаем, что контент визуально готов.
Отсюда рождается метрика:
TIME_TO_VISUAL_READY_MS — время от события “открыли экран” до момента, когда WebView перестал быть «просто белой картинкой».ё
Три типичных грабли:
-
Белые UI-страницы
Если у вас дизайн → белый фон, белый skeleton и прозрачные элементы, детектор такой: “ну да, всё ещё белое”.
Решение: небольшой порог (2%), подряд несколько кадров, и тюнинг под реальные скрины + логирование ratio.
-
SPA и лоадеры
Быстрый skeleton создаёт иллюзию скорости, но пользователь всё равно ждёт контент.
Мы делаем фиксированную тёмную заливку фона у контейнера + отдельный порог для «полезного» контента.
-
Разные девайсы / кастомный WebView
Где-то рендерятся шрифты иначе, где-то включена/выключена аппаратная акселерация.
Без логов и парочки feature gate тут никуда.
3. CoreComposeWebView — WebView как нормальный компонент, а не особенный ребёнок
CoreComposeWebView — это наша надстройка над AndroidView(WebView) в Compose, которая делает за экран почти всю грязную работу.
Он умеет:
попробовать взять готовый WebView из WebViewPreloader;
если нет — создать fresh-экземпляр;
подключить WebViewReadyDetector;
аккуратно разрулить cookie-политику (UsePreloaded / DropAndFresh);
самостоятельно отправить метрики в аналитику.
Для экранов это выглядит как обычный composable, а внутри уже:
берётся нужная стратегия (preload / fallback),
замеряется TIME_TO_VISUAL_READY_MS,
всё уезжает в Clickhouse → Grafana.
И в итоге любой WebView-экран:
открывается заметно быстрее;
сам сообщает, насколько он был «быстрым» для пользователя.
Что получилось?
После того, как мы раскатили пререндер и детектор, в Grafana появились нормальные графики по WebView:
для каждого экрана — TIME_TO_VISUAL_READY_MS;
разбивка по устройствам, версиям Android, сети, версиям WebView/Chromium.
Пример из боевых данных:
Source |
Median TTVR |
p90 |
Без пререндеринга |
3100 ms |
4900 ms |
С пререндерингом |
1200 ms |
1900 ms |
? В среднем экран загружается в 2,5 раза быстрее.?
А это уже не “кажется стало лучше”, а вполне себе аргумент для продукта и бизнеса.
Как это продать бизнесу?
Разработчикам достаточно увидеть падение p90 на графике.

Бизнесу нужен перевод на понятный язык
(цифры, ресурсы, GMV, конверсии)
Мы говорим так:
“Мы экономим пользователю около 2 секунд при каждом открытии WebView-экрана.”
Если экран открывается, скажем, 5 млн раз в месяц — это уже:
~2 800 часов сэкономленного пользовательского времени;
меньше оттока на «белом экране»;
выше конверсия в целевые действия (оплаты, авторизации, заполнение форм и т.д.).
И это уже звучит не как «поигрались с WebView», а как оптимизация ключевого флоу.
A/B-методология (чтобы потом не спорить на митингах)
Чтобы не спорить в стиле «ну у нас выборка другая», мы сделали нормальный A/B-сетап:
-
Делим трафик на устройстве
Персистентный флаг, чтобы не спутать прогрев движка между экспериментом и контролем.
-
Сегментируем
по сети (Wi-Fi / Cellular, RTT, если есть),
по устройству (классы CPU/RAM),
по версии SDK / WebView / Chromium.
-
Холгоут
Часть трафика оставляем без пререндера даже после раскатки, чтобы видеть регрессии и долгосрочный эффект.
-
Смотрим медиану и p90 отдельно
Медиана нужна, чтобы понимать «типичный» UX, p90 — чтобы видеть, что происходит в хвосте, где как раз и живёт боль.
-
Не смешиваем яблоки и апельсины
Разные провайдеры, регионы, типы экранов — отдельно. Иначе всё усредняется до ничего.
Почему этот подход масштабируется
Новый WebView-экран включается одной строкой.
Вся аналитика централизована в Clickhouse → Grafana.
Код не зависит от конкретного ChromeClient или JS-интеграций.
-
В любой момент можно:
подкрутить детектор;
изменить cookie-политику;
отключить пререндер для проблемных девайсов.
По сути, WebView из «нестабильного монстра» превращается в обычный компонент, у которого:
есть чёткие метрики;
понятная архитектура;
контролируемый UX;
Итог
Мы перестали на глазок гадать, быстро ли грузится WebView.
Теперь у нас есть:
WebViewPreloader — чтобы подогреть WebView заранее;
WebViewReadyDetector — чтобы честно понимать момент отображения контента;
CoreComposeWebView — чтобы экраны не думали про всё это вообще;
TIME_TO_VISUAL_READY_MS — одна цифра, которой можно объяснить и разработчику, и аналитику, и продакту, что именно мы улучшили.
А главное — у нас появились цифры, которые показывают, что WebView-пререндер — это не костыль, а вложение в UX и, как следствие, в бизнес.
Спасибо, что дочитали до этого места ?
Если хотите обсудить детали, обменяться кодом или графиками — пишите мне в LinkedIn :)
Комментарии (6)

Spyman
12.11.2025 23:41Идея любопытная, но я так понимаю работает, только если есть пулл страниц которые нужно открывать и они заранее известны (и если скорость там критична - их бы перевести на натив по возможности).
Так ещё и памяти кушает скорее всего немерянно (держать комплект открытых webView в мапе недёшево).
В таком сценарии интересно было бы копнуть в другую сторону - если ресурс который надо открыть контролируем мы, то попробовать подгрузить заранее js/css/картинки со всех страниц в кэш браузера, а дальше уже запрос ограничивать чисто страницей.

timkaopensoul Автор
12.11.2025 23:41Js динамичный же. Можно чисто прогреть движок конечно, сильного ускорения обычно это не дает

Spyman
12.11.2025 23:41Что вы подразумеваете? У вас импорты js файлов меняются в реальном времени в зависимости от окружения? Даже если и так - нет проблемы просто загрузить их все - не нейросетью же они на лету пишутся) Или у вас страница рендерится на клиенте (html собирается js-ом из ajax запросов без рендеринга на сервере для первого открытия)? Если так - то логичнее эту проблему сначала решить т.к. она тормоза у всех клиентов, включая настольных, вызывает.
Когда я последний раз изучал вопрос - самые долгие операции в загрузке страницы были пачки запросов ресурсов (из за пингов). Условно загрузка страницы 100мс, там пак из 20-30 файлов (css/js/шрифты/иконки/картинки), ещё 200мс на загрузку, и уже отрисовка - 40мс.
Вот выкинув второй этап (правильно настроив кэши) результат получается впечатляющий
Hubr2025
То ли я еще не проснулся и тороможу, то ли статья - лютая нейронка без редактирования. А тема то интересная...
timkaopensoul Автор
Тема очень интересная. Пока создавал инфраструктуру для такой задачи не нашел похожих статей вот и решил написать. Работая с prerenderUrlAsync съел куча собак вот и решил поделиться
Hubr2025
Перечитал ещё раз. Беру свои слова назад.
Статья хорошая и по делу - это я тогда не проснулся.
Просто присутствие ГПТ-шного стиля, и отсутствие ссылки на исходники навело на мысль о простом словошлепстве. Но вижу, что это не так.