или почему WebView-пререндер — не костыль, а инвестиция в UX и бизнес

Проблема, с которой сталкивается каждый Android-разработчик

WebView — самый непредсказуемый компонент Android:
• долго инициализируется (особенно при первом вызове);
• потребляет память;
• часто показывает белый экран при загрузке;
• и, самое неприятное — нет очевидного способа измерить, насколько быстро пользователь увидел контент.

На наших экранах WebView мы часто слышали от пользователей и QA:

“Экран "X" иногда открывается моментально, а иногда — секунд через пять.”

Это типичный случай, когда Android и Web сталкиваются в «серой зоне» UX.

Поэтому мы решили не просто оптимизировать, а построить измеримую инфраструктуру.

Что мы сделали?

Мы решили построить полноценную инфраструктуру для:
1. пререндеринга WebView — чтобы экран открывался мгновенно;
2. измерения момента визуальной готовности (TTVR) — чтобы можно было доказать эффект в метриках.

Чуть подробнее:

  1. WebViewPreloader — сервис, который греет и пререндерит WebView заранее (инициатор — что угодно: App Startup, фича, VM, эксперимент).

  2. WebViewReadyDetector — лёгкий детектор визуальной готовности (offscreen draw → «небелые» пиксели).

  3. 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.

По сути, он:

  1. Создаёт WebView в невидимом контейнере.

  2. Загружает URL в фоне (prerenderUrlAsync -> cм. дальше).

  3. Сохраняет готовый экземпляр в пул (Map<String, WebView>).

  4. Позже возвращает его при запросе через takePreloaded(url)

Что за prerenderUrlAsync в AndroidX WebKit и как его правильно готовить?

Коротко: это экспериментальный (alpha) API из AndroidX WebKit, который позволяет фоново подготовить рендер страницы до того, как вы откроете экран. Когда пользователь переходит на экран — мы “активируем” подготовленный рендер и показываем страницу без холодного старта WebView.

Модель работы

  1. Запрос на пререндер: вы зовёте prerenderUrlAsync(url, options, callback) не имея видимого WebView на экране.

    Под капотом движок создаёт изолированный рендер-контекст и начинает загрузку.

  2. Готовность: в колбэк приходит сигнал “готов” (или таймаут/ошибка). Это значит, что движок может отдать первый кадр очень быстро.

  3. Активация: когда пользователь реально открывает экран, вы либо:

    • берёте уже готовый WebView (если API умеет вернуть/передать его),

    • либо вызываете “активацию” (если API работает как “подготовленный рендер”, а привязка к вашему WebView идёт при показе).

  4. Отмена: если экран не открылся — отменяете задачу, чтобы освободить память.

Важно: в разных версиях 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 :)

Комментарии (0)