Технологии

Как ускорить WebView в Android и доказать это цифрами

или почему 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), Cookie-политика: • UsePreloaded — можно показать stale-контент (быстро, осторожнее с auth). • DropAndFresh — строгая консистентность (медленнее, зато без рассинхрона). Когда пользователь реально открывает экран, наш инструмент просто берёт готовый экземпляр из пула — без повторной инициализации и без белого экрана. Пример вызова при старте приложения в AppStartup Initializer'e class WebViewPreloadInitializer : Initializer, 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). Позже возвращает его при запросе через 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 :)

Фильтры и сортировка