или почему WebView-пререндер — не костыль, а инвестиция в UX и бизнес
Проблема, с которой сталкивается каждый Android-разработчик
WebView — самый непредсказуемый компонент Android:
• долго инициализируется (особенно при первом вызове);
• потребляет память;
• часто показывает белый экран при загрузке;
• и, самое неприятное — нет очевидного способа измерить, насколько быстро пользователь увидел контент.
На наших экранах WebView мы часто слышали от пользователей и QA:
“Экран "X" иногда открывается моментально, а иногда — секунд через пять.”

Это типичный случай, когда Android и Web сталкиваются в «серой зоне» UX.
Поэтому мы решили не просто оптимизировать, а построить измеримую инфраструктуру.
Что мы сделали?
Мы решили построить полноценную инфраструктуру для:
1. пререндеринга WebView — чтобы экран открывался мгновенно;
2. измерения момента визуальной готовности (TTVR) — чтобы можно было доказать эффект в метриках.
Чуть подробнее:
WebViewPreloader — сервис, который греет и пререндерит WebView заранее (инициатор — что угодно: App Startup, фича, VM, эксперимент).
WebViewReadyDetector — лёгкий детектор визуальной готовности (offscreen draw → «небелые» пиксели).
CoreComposeWebView — контейнер, который умеет: взять готовый инстанс из пула, корректно пересоздать fresh, подключить детектор, управлять cookie-политикой и сам отправить метрики в аналитику
⚙️ Архитектура (в 3 объектах)
1. WebViewPreloader — “фоновый рендеринг без магии”
Небольшой сервис, который создаёт WebView заранее (в App Startup) и прогружает нужные URL ещё до того, как пользователь откроет экран.
Под капотом:
Разогрев Chromium (пустой WebView + инициализация).
prerenderUrlAsync API
использует обычный WebView (никаких приватных API)
создаёт его в невидимом контейнере,
вызывает loadUrl() заранее,
сохраняет экземпляр в памяти в пуле (Map<String, WebView>),
Cookie-политика: • UsePreloaded — можно показать stale-контент (быстро, осторожнее с auth). • DropAndFresh — строгая консистентность (медленнее, зато без рассинхрона).
Когда пользователь реально открывает экран, наш инструмент просто берёт готовый экземпляр из пула — без повторной инициализации и без белого экрана.
Пример вызова при старте приложения в 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 не привязан к AppStartup.
Его можно вызвать в любой момент — из ViewModel, Experiment, Feature или Onboarding.
По сути, он:
Создаёт WebView в невидимом контейнере.
Загружает URL в фоне (prerenderUrlAsync -> cм. дальше).
Сохраняет готовый экземпляр в пул (Map<String, WebView>).
Позже возвращает его при запросе через takePreloaded(url)
Что за prerenderUrlAsync в AndroidX WebKit и как его правильно готовить?
Коротко: это экспериментальный (alpha) API из AndroidX WebKit, который позволяет фоново подготовить рендер страницы до того, как вы откроете экран. Когда пользователь переходит на экран — мы “активируем” подготовленный рендер и показываем страницу без холодного старта WebView.
Модель работы
-
Запрос на пререндер: вы зовёте prerenderUrlAsync(url, options, callback) не имея видимого WebView на экране.
Под капотом движок создаёт изолированный рендер-контекст и начинает загрузку.
Готовность: в колбэк приходит сигнал “готов” (или таймаут/ошибка). Это значит, что движок может отдать первый кадр очень быстро.
-
Активация: когда пользователь реально открывает экран, вы либо:
берёте уже готовый WebView (если API умеет вернуть/передать его),
либо вызываете “активацию” (если API работает как “подготовленный рендер”, а привязка к вашему WebView идёт при показе).
Отмена: если экран не открылся — отменяете задачу, чтобы освободить память.
Важно: в разных версиях Android System WebView (Chromium) поведение может отличаться (что именно исполняется до активации, как ведут себя тяжёлые операции, когда проматываются таймеры и т.д.). Поэтому — замеряйте TTVR в своём окружении и держите fallback.
Почему это лучше, чем просто держать невидимый WebView
Подход |
Плюсы |
Минусы |
Невидимый WebView |
Предсказуемо и работает везде; вы контролируете экземпляр |
Нужен реальный View в иерархии/контексте (даже offscreen); риск утечек; overhead по памяти |
prerenderUrlAsync |
Системный путь: движок сам решает, что и когда готовить; меньше шансов на баги уровня View; потенциально лучше по памяти |
Новое API (alpha), поддержка не везде; поведение зависит от версии WebView; нужно аккуратно кодить fallback |
Практические советы
Таймауты и отмена: ставьте таймаут ~6–10 с. Если не успели — падайте на fallback и логируйте причину (timeout, unsupported, error).
Лимиты: не пререндерьте всё подряд. Заводите список кандидатов (главный WebView-экран, часто посещаемые сцены).
Cookie-политика: если auth-cookie изменился — не используйте старый пререндер (или маркируйте его “stale” и осознанно решайте “UsePreloaded” против “DropAndFresh”).
Метрики: Сохраняйте причину выбора стратегии (API/Fallback/Timeout) — это сильно помогает в анализе регрессий.
QA-панель: добавьте в “debug overlay” строку с текущей стратегией и статусом пререндеринга (API/Fallback/Skipped).
2. WebViewReadyDetector - Как понять, когда страница реально отрисовалась?
Мы написали мини-инструмент, который следит за первыми кадрами и определяет момент, когда контент стал видимым.
Вкратце:
делает offscreen-рендер в Bitmap размером 48×48 пикселей;
анализирует, сколько пикселей не белые;
когда “заполненность” кадра превышает порог (например, 2%) — считает, что контент визуально готов.
3. CoreComposeWebView — умный контейнер
Надстройка над обычным AndroidView(WebView) в Compose, которая:
умеет брать готовый пререндер из WebViewPreloader;
создаёт fresh экземпляр, если нет пререндера;
подключает WebViewReadyDetector;
следит за cookie-политикой (UsePreloaded / DropAndFresh);
шлёт метрики автоматически, без участия экранов.
CoreComposeWebView(
url = "https://be-friendly.com",
providerName = WebViewProviderName.FRIENDLY,
expectedCookie = authCookie,
attachChromeClient = { webView -> /* ... */ },
attachJsBridges = { webView -> /* ... */ },
)
Таким образом, каждый WebView-экран становится самодостаточным компонентом, который:
быстро открывается,
и сам отправляет свою UX-метрику в аналитику.
Типичные грабли и как мы их обошли
Белые UI страницы → false-negative. Решение: порог 2%, 3 подряд кадра, тонкая настройка (и логирование ratio).
Cookie mismatch → «быстро, но не тот контент». Решение: политика DropAndFresh для auth-чувствительных экранов; метрика REASON=COOKIE_MISMATCH.
SPA/лоадеры → быстрые «скелетоны» обманывают восприятие. Решение: фиксированная тёмная заливка фона у контейнера, дополнительный порог по непрозрачности (alpha).
Alpha-API prerenderUrlAsync → на части устройств недоступно/ведёт себя по-разному. Решение: feature-gate + fallback + метрика “почему фолбэк”.
Что получилось?
Теперь у нас в Grafana:
есть метрика TIME_TO_VISUAL_READY_MS для каждого WebView;
видно, насколько пререндер реально ускоряет экран.
Пример из боевых данных:
Source |
Median TTVR |
p90 |
Без пререндеринга |
3100 ms |
4900 ms |
С пререндерингом |
1200 ms |
1900 ms |
? В среднем экран загружается в 2,5 раза быстрее.
И самое главное — теперь это измеримо и прозрачно.
? Как это продать бизнесу?
Для менеджеров и аналитиков мы перевели TTVR в понятный язык:
“Мы экономим пользователю ~2 секунды при каждом открытии WebView.”
Если считать, что WebView-экран открывают 5 млн раз в месяц,
то суммарная экономия времени — ~2800 часов пользовательского внимания.
Плюс — меньше оттока на “белом экране”, выше вовлечённость.
A/B-методология (чтобы было чему верить)
Делим трафик на устройстве (персистентный флаг), чтобы не путать прогрев движка.
Сегментируем: сети (Wi-Fi/Cell, RTT если есть), девайсы (классы по CPU/RAM), платформа (SDK/Chromium/WebView).
Холгоут: часть трафика держим без пререндера даже после раскатки.
Меряем медиану и p90 отдельно — бизнесу важны хвосты.
Не смешиваем: разные провайдеры, разные экраны.
Почему этот подход масштабируется
Любой новый WebView можно включить одной строкой.
Вся аналитика централизована в Clickhouse → Grafana.
Код не зависит от ChromeClient или JS-интеграций.
Итог
Мы перестали “на глаз” судить, быстро ли грузится WebView.
Теперь у нас есть инструмент, метрика и цифры, доказывающие, что UX стал лучше.
Спасибо большое за внимание, за деталями можете обращатся в linkedIn :)