Optimiser le démarrage de WebView

Lorsque votre application utilise un WebView pour la première fois, le système effectue des tâches de démarrage spécifiques. Ce processus de démarrage est lourd. Par défaut, cela se produit de manière implicite sur le thread d'UI la première fois que l'application appelle de nombreuses API dans les packages android.webkit ou androidx.webkit, ou qu'elle développe une mise en page contenant une balise WebView.

Pourquoi est-ce important ?

Comme ce démarrage implicite se produit entièrement sur le thread principal, il empêche votre application de traiter les entrées utilisateur et augmente considérablement le risque d'erreurs "Application ne répond pas" (ANR). Pour en savoir plus sur la façon dont Android gère le modèle d'exécution à thread unique, consultez Présentation des processus et des threads.

Déclencheurs pour le démarrage implicite

Le démarrage implicite peut être déclenché de différentes manières :

  • De manière programmatique : en appelant des API telles que WebSettings.getUserAgentString().
  • Utilisation de mises en page : appel de setContentView() ou layoutInflater.inflate() sur une ressource XML qui inclut un <WebView>.

Le démarrage implicite peut également avoir un impact négatif sur vos métriques d'activité, telles que le temps de démarrage de l'application et le délai avant le premier affichage. Si l'initialisation implicite n'est pas optimale pour votre application, utilisez plutôt startUpWebView.

Cette page explique comment optimiser les performances de démarrage de WebView à l'aide de l'API startUpWebView.

Contrôler le démarrage de WebView

Pour améliorer les performances et minimiser les ANR, utilisez l'API startUpWebView disponible dans la bibliothèque Jetpack Webkit. Cette API vous permet de contrôler explicitement le moment où WebView démarre. Il transfère une partie importante de la charge de travail de démarrage vers un thread d'arrière-plan et permet d'effectuer par blocs toute tâche qui doit être effectuée sur le thread UI, plutôt que sous la forme d'un seul bloc monolithique. Cela libère votre thread d'UI pour qu'il puisse gérer d'autres tâches critiques de l'application en parallèle, ce qui réduit le risque de bloquer l'expérience utilisateur.

L'API utilise le rappel androidx.webkit.WebViewOutcomeReceiver, ce qui vous permet de suivre les initialisations réussies.

Pour utiliser cette API, ajoutez la bibliothèque Jetpack Webkit à votre fichier build.gradle. Assurez-vous d'utiliser la version 1.16.0 ou ultérieure :

dependencies {
    implementation("androidx.webkit:webkit:1.16.0")
}

Utiliser l'API startUpWebView

La façon dont vous optimisez votre flux de démarrage dépend du moment où votre application doit réellement afficher la WebView.

Lorsque WebView n'est pas sur le chemin critique

Si votre application n'a pas besoin de charger immédiatement un WebView, vous pouvez masquer complètement le coût d'initialisation. Appelez startUpWebView tôt dans le cycle de vie de votre application et attendez que le rappel de réussite se déclenche.

Idéalement, vous devez attendre le rappel avant d'appeler d'autres API WebView. Si vous déclenchez startUpWebView, mais que vous n'attendez pas la fin de l'initialisation avant de toucher d'autres composants WebView, le système bloque le thread d'UI en attendant la fin de l'initialisation. Votre application peut bénéficier d'une amélioration des performances grâce au travail en arrière-plan déjà effectué, mais pas au maximum.

Lorsque WebView se trouve sur le chemin critique

Si le parcours utilisateur principal de votre application nécessite immédiatement un WebView, vous ne pouvez probablement pas vous permettre d'attendre la fin du démarrage de WebView. Dans ce cas, vous devez toujours appeler startUpWebView le plus tôt possible dans le cycle de vie de l'application (par exemple, dans Application.onCreate), mais n'attendez pas le déclenchement du rappel. Utilisez plutôt directement les API WebView lorsque cela est nécessaire.

Pour tirer le meilleur parti du démarrage asynchrone, il est essentiel de différer l'instanciation d'une WebView ou l'appel des API WebView jusqu'à ce qu'il ne reste plus d'opérations critiques sur le thread d'UI (comme l'inflation des hiérarchies de mise en page, l'initialisation d'autres SDK ou le dessin du frame initial).

Si vous appelez startUpWebView et que vous invoquez immédiatement les API WebView par la suite sur le thread principal, le thread UI se bloque en attendant que l'initialisation rattrape son retard. Dans ce scénario, il n'y a aucun avantage en termes de performances.

Si l'utilisation de WebView peut devenir critique, mais que vous ne souhaitez pas démarrer WebView entièrement, vous pouvez choisir d'exécuter sélectivement les tâches de démarrage de WebView qui peuvent s'exécuter sur un thread d'arrière-plan, libérant ainsi le thread UI pour d'autres tâches critiques de l'application. Pour ce faire, vous pouvez utiliser shouldRunUiThreadStartUpTasks(false).

Plus tard dans le cycle de vie de votre application, vous pouvez appeler à nouveau startUpWebView avec shouldRunUiThreadStartUpTasks(true) pour terminer les tâches de démarrage restantes sur le thread UI. Le fait d'attendre ou non le rappel à ce moment-là dépend de l'utilisation de WebView sur le chemin critique.

Exemple de mise en œuvre

L'API utilise le rappel androidx.webkit.WebViewOutcomeReceiver, ce qui vous permet de suivre les initialisations réussies ou de gérer les échecs de diagnostic.

Vous pouvez appeler startUpWebView plusieurs fois à partir de différentes parties de votre application. Nous vous recommandons d'éviter d'implémenter une boucle de nouvelle tentative naïve.

L'exemple de code suivant montre comment utiliser l'API WebViewCompat.startUpWebView pour l'initialisation asynchrone.

Kotlin

import android.content.Context
import android.util.Log
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewOutcomeReceiver
import androidx.webkit.WebViewStartUpConfig
import androidx.webkit.WebViewStartUpResult
import androidx.webkit.WebViewStartupException
import java.util.concurrent.Executors

fun initializeWebView(context: Context) {
    // 1. Create a startup configuration specifying the background thread
    // that WebView will use to run its initialization tasks.
    val startUpConfig = WebViewStartUpConfig.Builder(
        Executors.newSingleThreadExecutor()
    ).build()

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
        context,
        startUpConfig,
        object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {

            override fun onResult(result: WebViewStartUpResult) {
                // Success: The WebView has finished its background initialization.
                // This callback is guaranteed to be invoked on the UI thread.
                setupWebView()
            }

            override fun onError(error: WebViewStartupException) {
                // Failure: The initialization encountered a startup exception.
                Log.e("WebViewStartup", "Failed to initialize WebView", error)
            }
        }
    )
}

Java

import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewOutcomeReceiver;
import androidx.webkit.WebViewStartUpConfig;
import androidx.webkit.WebViewStartUpResult;
import androidx.webkit.WebViewStartupException;
import java.util.concurrent.Executors;

public void initializeWebView(Context context) {
    // 1. Create the startup configuration specifying the background thread pool
    // to handle internal non-UI initialization processes.
    WebViewStartUpConfig startUpConfig = new WebViewStartUpConfig.Builder(
            Executors.newSingleThreadExecutor()
    ).build();

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
            context,
            startUpConfig,
            new WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException>() {

                @Override
                public void onResult(@NonNull WebViewStartUpResult result) {
                    // Success: The WebView has finished its background initialization.
                    // This callback is invoked directly on the UI thread.
                    setupWebView();
                }

                @Override
                public void onError(@NonNull WebViewStartupException error) {
                    // Failure: Handled using the concrete WebViewStartupException
                    Log.e("WebViewStartup", "Failed to initialize WebView", error);
                }
            }
    );
}

Déboguer les problèmes de démarrage asynchrone

Si startUpWebView ne produit pas les avantages attendus en termes de performances, c'est souvent parce que WebView est initialisé de manière implicite ailleurs dans votre application avant l'exécution de votre appel. Cela peut être dû aux raisons suivantes :

  • Bibliothèques tierces ou SDK initialisés tôt dans le cycle de vie de l'application.

  • ContentProviders injecté dans votre APK qui déclenche les API WebView au démarrage de l'application.

  • Gonflements de mise en page ou appels programmatiques (comme la récupération de chaînes d'agent utilisateur) qui se produisent de manière inattendue trop tôt.

Pour vous aider à identifier où et pourquoi ces initialisations inattendues se produisent, l'objet WebViewStartUpResult fournit des fonctionnalités d'audit intégrées :

  • getUiThreadBlockingStartUpLocations() : renvoie une liste d'objets StartUpLocation représentant les emplacements où les tâches de démarrage de WebView ont bloqué le thread d'UI principal.

  • getNonUiThreadBlockingStartUpLocations() : renvoie des sites d'appel spécifiques où les tâches de démarrage en cours ont bloqué les threads d'arrière-plan.

Chaque StartUpLocation contient une trace de la pile que vous pouvez consigner ou inspecter pour trouver la classe et la méthode exactes qui ont déclenché l'initialisation.

Exemple de mise en œuvre

Vous pouvez inspecter ces emplacements dans votre rappel onResult pour auditer votre chemin de démarrage :

override fun onResult(result: WebViewStartUpResult) {
    // Check if WebView startup was blocked on the UI thread prior to or during initialization
    val uiBlockingLocations = result.getUiThreadBlockingStartUpLocations()
    if (!uiBlockingLocations.isNullOrEmpty()) {
        for (location in uiBlockingLocations) {
            // Log the stack trace of the call site that triggered the UI-blocking startup
            Log.w("WebViewDebug", "WebView startup blocked the UI thread here:", location.getStack())
        }
    } else {
        Log.i("WebViewDebug", "Excellent! No UI-blocking WebView startup detected.")
    }

    // Check where background initialization tasks were executed
    val backgroundLocations = result.getNonUiThreadBlockingStartUpLocations()
    backgroundLocations?.forEach { location ->
        Log.d("WebViewDebug", "WebView background startup occurred at: ${location.getStack()}")
    }

    setupWebView()
}

Utiliser ces données lors d'un audit

Lorsque vous auditez le démarrage de la WebView de votre application, utilisez les stratégies suivantes pour analyser les données de diagnostic et résoudre les problèmes de performances :

  • Recherchez les traces de pile inattendues : si getUiThreadBlockingStartUpLocations() n'est pas vide, examinez les traces de pile imprimées. Si vous voyez des classes appartenant à des SDK tiers ou des composants inattendus, vous avez trouvé un goulot d'étranglement d'initialisation implicite.

  • Vérifiez l'ordre des appels : si les sorties de vos journaux indiquent qu'une initialisation implicite s'est produite avant votre appel startUpWebView manuel, vous devez déplacer l'initialisation startUpWebView plus tôt dans votre application ou configurer le SDK concerné pour qu'il retarde ses tâches dépendantes de WebView.

Migrer à partir d'anciennes solutions de contournement

Auparavant, vous avez peut-être utilisé des solutions de contournement explicites pour forcer l'initialisation de WebView sur un thread d'arrière-plan, par exemple en récupérant la chaîne de l'agent utilisateur.

Ces solutions de contournement sont considérées comme des pratiques non prises en charge, et leur comportement sous-jacent peut changer dans les prochaines versions. Si votre application repose sur des solutions de contournement explicites et non documentées pour déclencher ou gérer le démarrage de WebView, nous vous recommandons d'utiliser l'API startUpWebView à la place. L'API startUpWebView fonctionne sur toutes les versions d'Android et de WebView compatibles avec la bibliothèque Jetpack Webkit.

L'utilisation de l'implémentation Jetpack Webkit permet de garantir un comportement cohérent dans l'ensemble de l'écosystème Android. L'un des principaux avantages de cette API est sa résilience : sur les anciens appareils où les optimisations plus récentes ne sont pas disponibles, l'API maintient la parité des performances avec les solutions de contournement manuelles. Cela vous permet d'adopter des avantages modernes pour les start-ups sur les appareils récents sans pénaliser les performances sur les appareils plus anciens.

Si vous rencontrez des problèmes ou avez des commentaires concernant l'API startUpWebView, signalez un bug sur l'outil public Issue Tracker.