Skip to content

perf: reduce webView startup time - #21220

Open
neronguyen wants to merge 1 commit into
ankidroid:mainfrom
neronguyen:performance/issue-20973-investigate-startUpWebView
Open

neronguyen wants to merge 1 commit into
ankidroid:mainfrom
neronguyen:performance/issue-20973-investigate-startUpWebView

Conversation

@neronguyen

@neronguyen neronguyen commented Jun 4, 2026

Copy link
Copy Markdown

This reduces webView startup time by setup webView when app launch

Purpose / Description

Reduces webView startup time by integrating androidx:startUpWebView and setup webView when app launch

Fixes

Approach

I setup webView with new API and make other setups wait until this setup is complete, which already warms up the webView framework for other later usages

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration (SDK version(s), emulator or physical, etc)

Learning (optional, can help others)

Checklist

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@david-allison

david-allison commented Jun 4, 2026

Copy link
Copy Markdown
Member

Thanks! CI/tests are failing

As a general note: aim to scope this change just to the WebView, not a full performance improvement for the general startup process.

This PR looks like it potentially removes onCreate logging of the first activity (for example) due to deferring the addition of activity lifecycle logging

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Jun 4, 2026
private suspend fun setupWebView(): Boolean =
setupSuspend("setupWebView") {
suspendCancellableCoroutine { continuation ->
val executor = Executors.newSingleThreadExecutor()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any source for using this pattern, or could you comment on why it's used here?

@neronguyen neronguyen Jun 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspendCancellableCoroutine to convert callback pattern to coroutines so I can wait for the startUp to complete before other setups

setupSuspend just follow your setup but can apply to suspend functions. Because the setup finishes with callback, so have to convert it to suspend to measure its exec time. Or should implement time measure inside the back, no need to implement a dedicate setupSuspend ?

but as your above review, I should scope down the changes to make it not affect the general startup process

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the coMeasureTime function to measure suspend function. I also flat the code with function startUpWebView inside function setupWebView for readable.

@neronguyen
neronguyen force-pushed the performance/issue-20973-investigate-startUpWebView branch 2 times, most recently from 8251897 to c3ad8a9 Compare June 4, 2026 15:48
@david-allison david-allison removed the Needs Author Reply Waiting for a reply from the original author label Jun 4, 2026
@neronguyen
neronguyen force-pushed the performance/issue-20973-investigate-startUpWebView branch from c3ad8a9 to 13927bf Compare June 18, 2026 15:00
@neronguyen

Copy link
Copy Markdown
Author

hi @david-allison , I just resolved conflicts and updated it with cleaner approach - don't block the main setup flow anymore, could you check it please

@neronguyen
neronguyen force-pushed the performance/issue-20973-investigate-startUpWebView branch from 13927bf to 9ffaed2 Compare July 26, 2026 09:19
@david-allison

Copy link
Copy Markdown
Member

Ok, I'm back, sorry this sat.

This reduces webView startup time by setup webView when app launch

I did a verification run and there's no significant change: We block synchronously on the WebView via setWebContentsDebuggingEnabled, which is called before the code this PR introduces.


I've now done some WebView startup work. The shape of this PR should:

  • Initialize the WebView as early as possible in a background thread
  • Only block on the WebView init if truly required, otherwise run code in a continuation.

Would you be open to a more in-depth refactoring:

  • moving startup functionality so it either runs in a continuation after the WebView loads, or is handled during the asynchronous WebView init
  • moving our debug info dump to show the WebView data in a continuation (if it did not load before the Timber call)
  • Blocking DeckPicker loading on the completion of the WebView load (failure|success|timeout) - it needs to block to warn a user about an outdated WebView.

@neronguyen

Copy link
Copy Markdown
Author

Yes I'm willing to make a small refactoring here.

I come with some ideas to summary before getting into the implementation:

  • Moving WebView functionalities so it runs after the WebView loads:
    setWebContentsDebuggingEnabled, getWebViewInfo
  • startUpWebView exposes a job so other functionalities can join to wait for its completion
  • About blocking DeckPicker in loading to wait for above completion before checking WebView is outdated or not, I will investigate and implement in the later commits

@neronguyen
neronguyen force-pushed the performance/issue-20973-investigate-startUpWebView branch from 9ffaed2 to 1f4590e Compare September 12, 2026 18:24

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the follow-through!

The suspendCancellableCoroutine pattern doesn't sit right with me, given the API added to androidx.webkit was designed around callbacks.

As-of now, we're still blocking on the completion of a WebView, so we're pulling in a negligible performance benefit here

This was meant to be a good first issue (scoping whether the API was useable for us), and I feel we're a little too far into the weeds. Let's re-scope this to a more targeted change: getting setWebContentsDebuggingEnabled executing asynchronously, and deferring the 'hard' startup work for a later PR

Apply this patch: https://github.com/ankidroid/Anki-Android/wiki/Development-guide#applying-a-patch

Make any changes you feel would improve it, then let's get it in

Note

Assisted-by: Claude Fable 5.1

Subject: [PATCH] improvement(startup): WebView startup helper
perf(startup): WebView startup via plain callback
---
Index: AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt	(revision 1f4590eb619a39c86366645323aa647e84615638)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt	(revision af4c56116bcc785b03b10d42f63ba39f556a48cc)
@@ -22,11 +22,7 @@
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ProcessLifecycleOwner
 import anki.collection.OpChanges
-import com.ichi2.anki.AnkiDroidApp.Companion.instance
-import com.ichi2.anki.AnkiDroidApp.Companion.isInitialized
-import com.ichi2.anki.AnkiDroidApp.Companion.makeBackendUsable
 import com.ichi2.anki.AnkiDroidApp.Companion.sharedPreferencesTestingOverride
-import com.ichi2.anki.AnkiDroidApp.Companion.sharedPrefs
 import com.ichi2.anki.analytics.initializeAnalytics
 import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository
 import com.ichi2.anki.common.android.AdaptionUtil
@@ -58,7 +54,6 @@
 import com.ichi2.anki.preferences.SharedPreferencesProvider
 import com.ichi2.anki.reviewreminders.ReminderLogTree
 import com.ichi2.anki.servicelayer.DebugInfoService
-import com.ichi2.anki.servicelayer.StartupWebViewService
 import com.ichi2.anki.servicelayer.ThrowableFilterService
 import com.ichi2.anki.services.NotificationService
 import com.ichi2.anki.settings.Prefs
@@ -72,6 +67,7 @@
 import com.ichi2.utils.LanguageUtil
 import com.ichi2.utils.measureTime
 import com.ichi2.utils.setWebContentsDebuggingEnabled
+import com.ichi2.utils.startUpWebView
 import com.ichi2.widget.DayRolloverAlarm
 import com.ichi2.widget.WidgetNotificationScheduler
 import com.ichi2.widget.cardanalysis.CardAnalysisWidget
@@ -177,7 +173,6 @@
             showThemedToast(this.applicationContext, getString(R.string.user_is_a_robot), false)
         }
 
-        setupWebView()
         setupContextMenus()
 
         setup("makeBackendUsable") { makeBackendUsable(this) }
@@ -189,6 +184,8 @@
         if (!checkWebViewAvailable()) {
             return
         }
+        // after the probe: startUpWebView throws on its executor if the WebView provider is missing (5794)
+        setupWebView()
 
         // Forget the last deck that was used in the CardBrowser
         CardBrowser.clearLastDeckId()
@@ -452,24 +449,19 @@
     }
 
     /**
-     * Start up WebView to speed up its later usages.
-     * Set up WebView functionalities.
+     * Starts asynchronously loading the WebView on a background thread.
+     *
+     * TODO: This only handles a subset of WebView init, and will not produce a performance
+     *  improvement until this pattern is used for all WebView init.
      */
-    private fun setupWebView() {
+    private fun setupWebView() =
         setup("setupWebView") {
-            applicationScope.launch {
-                val result = StartupWebViewService.startUpWebView(this@AnkiDroidApp)
-                if (result.isSuccess) {
-                    setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled)
-                } else {
-                    val e = result.exceptionOrNull() ?: return@launch
-                    fatalInitializationError = FatalInitializationError.WebViewError(e)
-                    sendExceptionReport(e, "setupWebView")
-                    Timber.e(e, "setupWebView")
-                }
-            }
+            startUpWebView(
+                context = this,
+                onSuccess = { setWebContentsDebuggingEnabled(Prefs.isWebDebugEnabled) },
+                onFailure = { e -> Timber.w(e, "startUpWebView") },
+            )
         }
-    }
 
     /**
      * @return the app version, OS version and device model, provided when syncing.
Index: AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt
--- a/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt	(revision 1f4590eb619a39c86366645323aa647e84615638)
+++ b/AnkiDroid/src/main/java/com/ichi2/utils/WebViewUtils.kt	(revision af4c56116bcc785b03b10d42f63ba39f556a48cc)
@@ -27,13 +27,17 @@
 import androidx.appcompat.app.AlertDialog
 import androidx.core.content.pm.PackageInfoCompat
 import androidx.webkit.WebViewCompat
+import androidx.webkit.WebViewOutcomeReceiver
+import androidx.webkit.WebViewStartUpConfig
+import androidx.webkit.WebViewStartUpResult
+import androidx.webkit.WebViewStartupException
 import com.ichi2.anki.R
 import com.ichi2.anki.common.crashreporting.CrashReportService
-import com.ichi2.anki.servicelayer.StartupWebViewService
 import com.ichi2.anki.utils.openUrl
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import timber.log.Timber
+import java.util.concurrent.Executors
 
 @JvmInline
 value class WebViewVersion(
@@ -172,18 +176,15 @@
  *
  * @return A [WebViewInfo] object with WebView package details.
  */
-suspend fun getWebViewInfo(context: Context): WebViewInfo {
-    StartupWebViewService.startUpWebView(context)
-    return withContext(Dispatchers.Main) {
-        val packageInfo =
-            runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull()
+suspend fun getWebViewInfo(context: Context): WebViewInfo =
+    withContext(Dispatchers.Main) {
+        val packageInfo = runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull()
         WebViewInfo(
             userAgent = getWebviewUserAgent(context),
             packageName = packageInfo?.packageName,
             versionCode = runCatching { packageInfo?.let { PackageInfoCompat.getLongVersionCode(it) } }.getOrNull(),
         )
     }
-}
 
 private fun showOutdatedWebViewDialog(
     context: Context,
@@ -235,3 +236,34 @@
         // android.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed
         Timber.w(e, "setWebContentsDebuggingEnabled")
     }
+
+/**
+ * Starts the WebView on a background thread, so later WebView usage is faster.
+ *
+ * Exactly one of [onSuccess] or [onFailure] is called, on the main thread, once startup completes.
+ *
+ * Background work runs on a dedicated thread, released once startup completes.
+ *
+ * @see WebViewCompat.startUpWebView
+ */
+fun startUpWebView(
+    context: Context,
+    onSuccess: () -> Unit,
+    onFailure: (Throwable) -> Unit,
+) {
+    val executor = Executors.newSingleThreadExecutor()
+    val config = WebViewStartUpConfig.Builder(executor).build()
+    val callback =
+        object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
+            override fun onResult(result: WebViewStartUpResult?) {
+                executor.shutdown()
+                onSuccess()
+            }
+
+            override fun onError(e: WebViewStartupException) {
+                executor.shutdown()
+                onFailure(e)
+            }
+        }
+    WebViewCompat.startUpWebView(context, config, callback)
+}
Index: AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt
deleted file mode 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/StartupWebViewService.kt	(revision 1f4590eb619a39c86366645323aa647e84615638)
+++ /dev/null	(revision 1f4590eb619a39c86366645323aa647e84615638)
@@ -1,64 +0,0 @@
-package com.ichi2.anki.servicelayer
-
-import android.content.Context
-import androidx.webkit.WebViewCompat
-import androidx.webkit.WebViewOutcomeReceiver
-import androidx.webkit.WebViewStartUpConfig
-import androidx.webkit.WebViewStartUpResult
-import androidx.webkit.WebViewStartupException
-import com.ichi2.anki.common.coroutines.applicationScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import java.util.concurrent.Executors
-import kotlin.coroutines.resume
-
-object StartupWebViewService {
-    private var startUpWebViewDeferred: Deferred<Result<Unit>>? = null
-    private val mutex = Mutex()
-
-    /**
-     * Ensure only one startup at the same time.
-     * Fatal error if fails.
-     * So no need for another wait for completion function or reset the deferred.
-     */
-    suspend fun startUpWebView(context: Context): Result<Unit> {
-        val deferred =
-            mutex.withLock {
-                startUpWebViewDeferred?.let { return@withLock it }
-
-                val newJob =
-                    applicationScope.async {
-                        startUpWebViewPrivate(context)
-                    }
-
-                startUpWebViewDeferred = newJob
-                newJob
-            }
-
-        return deferred.await()
-    }
-
-    private suspend fun startUpWebViewPrivate(context: Context): Result<Unit> =
-        suspendCancellableCoroutine { continuation ->
-            val executor = Executors.newSingleThreadExecutor()
-            val config = WebViewStartUpConfig.Builder(executor).build()
-
-            val callback =
-                object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {
-                    override fun onResult(result: WebViewStartUpResult?) {
-                        executor.shutdown()
-                        continuation.resume(Result.success(Unit))
-                    }
-
-                    override fun onError(e: WebViewStartupException) {
-                        executor.shutdown()
-                        continuation.resume(Result.failure(e))
-                    }
-                }
-
-            WebViewCompat.startUpWebView(context, config, callback)
-        }
-}

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Sep 13, 2026
@neronguyen

Copy link
Copy Markdown
Author

I see, the hard work like make startUpWebView synchronized from all threads and make use of this approach for all WebView init will be on later PR.

I will apply the patch above and commit it with you as co-author. I feel the patch fit with what we have discussed here. So sorry for my bad habit avoiding using callbacks outside Compose codes.

Thank you much for supporting me so far. I'm willing to work on this next WebView PR or the one you suggest.

Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
@neronguyen
neronguyen force-pushed the performance/issue-20973-investigate-startUpWebView branch from 1f4590e to 7c7a305 Compare September 15, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Author Reply Waiting for a reply from the original author Needs Review New contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: Investigate androidx: startUpWebView

2 participants