טעינה מראש ב-WebView

זמן האחזור של הניווט הוא מדד חשוב לחוויית המשתמש. כדי לעזור למפתחים לצמצם את זמן האחזור הזה, WebView מספק ממשקי API לטעינה ספקולטיבית, שמאפשרים לאפליקציה לאחזר או לעבד תוכן לפני שהמשתמש מנווט אליו באופן מפורש.

‫WebView תומך בשלושה סוגים עיקריים של טעינה מראש: Preconnect,‏ Prefetch ו-Prerender.

הטמעה של אסטרטגיית טעינה ספקולטיבית מאפשרת לכם:

  • קיצור משמעותי של זמן הטעינה של תוכן אינטרנט: העברת זמן ההתחלה של הרשת לשלב מוקדם יותר במחזור החיים של האפליקציה.
  • שיעורי הצלחה גבוהים יותר של ניווטים: חימום מראש של הרשת והמטמון מפחית את הסיכוי שהניווטים ייכשלו בגלל בעיות זמניות ברשת.
  • תגובה מהירה יותר: עיבוד מראש, בפרט, מאפשר מעברים מיידיים שגורמים לאפליקציה להרגיש מהירה משמעותית.

בחירה של אסטרטגיית טעינה מראש

ההבדל העיקרי בין השיטות האלה הוא בהיקף שלהן: ה-API של Preconnect מבוסס על מקור, כלומר הוא דורש רק את דומיין היעד. ממשקי ה-API של Prefetch ו-Prerender מבוססים על כתובות URL, כלומר הם דורשים את הנתיב המדויק של דף האינטרנט.

התכונה Preconnect פועלת ברמת המקור, ולכן אפשר להפעיל אותה הרבה יותר מוקדם במחזור החיים של האפליקציה, עוד לפני שאתם יודעים לאיזה תוכן ספציפי או לאיזה דף המשתמש יעבור.

בטבלה הבאה מוצגת השוואה בין שלוש האסטרטגיות האלה, כדי לעזור לכם לבחור את האסטרטגיה המתאימה לתרחיש השימוש שלכם:

תכונה קישור מקדים שליפה מראש (prefetch) עיבוד מראש
יעד ראשי חימום החיבור שמירת HTML בלבד במטמון (ללא JavaScript או CSS) טרום-עיבוד של הדף כולו
היקף ברמת הפרופיל (משותף בכל תצוגות האינטרנט) ברמת הפרופיל (משותף בכל תצוגות האינטרנט) רמת WebView (קשורה ל-WebView ספציפי)
Jetpack WebKit API androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
שיטות ליבה של API preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
Configuration לא רלוונטי PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
שימוש במשאבים נמוכה (רשת) בינוני (רשת, זיכרון) גבוה (CPU, זיכרון, רשת)
מתי להשתמש כשמקור היעד ידוע, אבל כתובת ה-URL הספציפית עדיין לא נקבעה. כשכתובת ה-URL המדויקת ידועה והניווט סביר, עם שמירת נתונים במטמון שמשותפת בין תצוגות WebView. כשכתובת ה-URL המדויקת ידועה והניווט ב-WebView ספציפי מאוד.
היתרונות הגדרה מהירה יותר של חיבור לכל כתובת URL במקור טעינה מהירה יותר של רשת עבור כתובות URL תואמות ניווט מיידי באמת עם ההפעלה

קישור מקדים למקורות

חיבור מראש מזרז טעינות עתידיות על ידי ביצוע מראש של שאילתות DNS ולחיצות יד ב-TCP/TLS למקור שצוין.

בניגוד לשליפה מראש (prefetch) ולטרום-עיבוד (prerender), שדורשים כתובת יעד מדויקת, טרום-חיבור (preconnect) מבוסס אך ורק על מקור. כך אפשר לבצע את הקריאה Preconnect הרבה יותר מוקדם מאשר Prefetch ו-Prerender.

השיטה הזו ברמת הפרופיל, שדורשת מעט משאבים, מקטינה את זמן האחזור הראשוני לכל שיתוף של WebView בפרופיל, בתנאי שהמקור לא נטען כבר. החיבור נשאר פתוח למשך כ-30 שניות, וכך הוא מועיל לכל בקשות ה-HTTP, הניווטים או משאבי המשנה הבאים ממקורות שונים, כי הוא מבטל את התקורה של לחיצת היד.

הטמעה

כדי ליזום חיבור מראש, מתקשרים אל preconnect(String url) במכונת Profile צריך להפעיל את ה-API הזה בשרשור UI, והוא דורש תמיכה ב-WebViewFeature.PRECONNECT.

ה-API פועל על המקור, אבל כדי שיהיה נוח יותר, אפשר לספק כתובת URL מלאה (למשל https://www.example.com/index.html). המערכת מתייחסת לזה באופן אוטומטי כקריאה למקור (למשל, https://www.example.com). אפשר לקשר כמה מקורות על ידי קריאה ל-API הזה כמה פעמים.

Kotlin

// Must be called on the @UiThread
if (WebViewFeature.isFeatureSupported(WebViewFeature.PRECONNECT)) {
    profile.preconnect("https://www.example.com/index.html")
    // This initiates a connection to the origin https://www.example.com
}

Java

// Must be called on the @UiThread
if (WebViewFeature.isFeatureSupported(WebViewFeature.PRECONNECT)) {
    profile.preconnect("https://www.example.com/index.html");
    // This initiates a connection to the origin https://www.example.com
}

הגדרה נפוצה: PrefetchParameters ו-PrerenderParameters

גם Prefetch וגם Prerender משתמשים ב-PrefetchParameters או ב-PrerenderParameters כדי להתאים אישית את הבקשה. המחלקות האלה מאפשרות לספק כותרות והצעות נוספות להתאמה של כתובות URL, כמו הגדרות No-Vary-Search.

Kotlin

// Isolated configuration specifically for Cache-Level Prefetching
val prefetchParams = PrefetchParameters.Builder()
    .addAdditionalHeader("X-Custom-Client", "Android-App-V2")
    .setExpectedNoVarySearchHeader(
        NoVarySearchHeader.varyExcept(true, listOf("session_id", "click_ref"))
    )
    .build()

Java

PrefetchParameters prefetchParams = new PrefetchParameters.Builder()
    .addAdditionalHeader("X-Custom-Header", "value")
    /**
     * Hint to ignore specific query parameters during cache matching.
     * This allows the cache to match even if the tracking_id differs.
     */
    .setExpectedNoVarySearchHeader(
        NoVarySearchHeader.varyExcept(true, Arrays.asList("tracking_id"))
    )
    /**
     * Determines if Client Hints are sent.
     * NOTE: This is ignored for Prerendering API requests, which default to
     * the WebView's WebSettings.getJavaScriptEnabled() value.
     */
    .setJavaScriptEnabled(true)
    .build();

שליפה מראש של תוכן

שליפה מראש מורידה את משאב ה-HTML הראשי של כתובת URL ומאחסנת אותו במטמון הרשת של הפרופיל. ב-WebView, רכיב Profile משמש כמאגר לנתוני הדפדפן, כולל קובצי Cookie, מטמון HTTP ועובדי שירות. מכיוון ש-prefetch היא פעולה ברמת הפרופיל, כל WebView שמשויך לפרופיל הזה יכול להשתמש בתגובה שנשמרה במטמון.

הטמעה

כדי להפעיל אחזור מראש, קוראים ל-prefetchUrlAsync() במופע Profile. הפעולה הזו תומכת רק בסכימת HTTPS.

Kotlin

profile.prefetchUrlAsync(
    url,
    prefetchParams,
    cancellationSignal,
    executor,
    object : WebViewOutcomeReceiver<PrefetchResult, PrefetchException> {
        override fun onResult(result: PrefetchResult) {
            if (result.wasDuplicate()) {
                // URL and No-Vary-Search permutations already exist in the cache layer
            } else {
                // The HTML payload has been successfully secured in the HTTP cache
            }
        }

        override fun onError(error: PrefetchException) {
            when (error) {
                is PrefetchNetworkException -> {
                    // Isolates network layer or server-side HTTP anomalies
                    val code = error.httpStatusCode
                    // Facilitates rapid diagnosis of 4xx or 5xx server responses
                }
                else -> {
                    // Catches generalized execution failures and system constraints
                }
            }
        }
    }
)

Java

profile.prefetchUrlAsync(
    url,
    prefetchParams,
    cancellationSignal,
    executor,
    new WebViewOutcomeReceiver<PrefetchResult, PrefetchException>() {
        @Override
        public void onResult(PrefetchResult result) {
            if (result.wasDuplicate()) {
                // URL and No-Vary-Search permutations already exist in the cache layer
            } else {
                // The HTML payload has been successfully secured in the HTTP cache
            }
        }

        @Override
        public void onError(PrefetchException error) {
            if (error instanceof PrefetchNetworkException) {
                // Isolates network layer or server-side HTTP anomalies
                int code = ((PrefetchNetworkException) error).httpStatusCode;
                // Facilitates rapid diagnosis of 4xx or 5xx server responses
            } else {
                // Catches generalized execution failures and system constraints
            }
        }
    }
);

מחזור החיים של חטיפת מסירה

בקשת השליפה מראש של WebView משנה את התזמון והאופן שבהם מופעלת shouldInterceptRequest() הקריאה החוזרת. ההבנה של מחזור החיים הדו-שלבי הזה היא קריטית, כי הוא משפיע ישירות על השימוש המוצלח בתוכן שאוחזר מראש.

דיאגרמה שמציגה את מחזור החיים של חסימת טעינה מראש של WebView בשני שלבים
  במהלך השלבים של חיזוי וניווט.
איור 1. מחזור החיים של יירוט בשני שלבים לבקשות ולניווטים של טרום-אחזור של WebView.

1. השלב הספקולטיבי (בקשת שליפה מראש)

כשמפעילים את prefetchUrlAsync(), ‏ WebView מוריד את משאב ה-HTML הראשי ברקע. הבקשה ברקע הזו מדלגת לחלוטין על shouldInterceptRequest(). לוגיקה מותאמת אישית, טוקנים של הרשאות או הוספות של כותרות, שבדרך כלל מטופלים בתוך ה-interceptor, לא חלים על משאב ה-HTML שנשלף מראש.

2. שלב הניווט (הפעלה על ידי המשתמש)

כשהאפליקציה עוברת באופן מפורש לכתובת ה-URL (למשל, באמצעות loadUrl()) או כשהמשתמש לוחץ על קישור תואם, WebView קובע אם אפשר להשתמש במטמון שאוחזר מראש:

  • הערכת ה-HTML הראשי: בשלב הזה, WebView יפעיל את shouldInterceptRequest() עבור ה-HTML הראשי. כדי שהדף יוצג בהצלחה ממטמון האחזור המקדים, ה-interceptor צריך להחזיר את הערך null. אם מחזירים WebResourceResponse מותאם אישית, WebView מכבד את ה-interceptor ומדלג לחלוטין על מטמון האחזור מראש.

  • הערכת משאבי משנה: אחרי שמתקבל אישור לשימוש ב-HTML שנשלף מראש, shouldInterceptRequest() מופעל בדרך כלל לכל משאבי המשנה הבאים (כמו תמונות, סקריפטים ו-CSS) שנדרשים כדי לסיים את הרינדור של הדף.

התנהגויות מרכזיות

המאפיינים התפעוליים הבאים ובדיקות הזכאות קובעים איך WebView יוזם ומנהל בקשות לאחזור מראש:

  • בטיחות בשרשור: אפשר לשלוח בקשות מכל שרשור.
  • דרישות הסף: לפני הפעלת האחזור, WebView מוודא שהבקשה בטוחה ומתאימה להקשר על ידי בדיקת הנקודות הבאות:
    • קובצי Cookie קיימים: כדי להגן על פרטיות המשתמשים ולמנוע תופעות לוואי דומות ל-CSRF, יכול להיות ש-WebView ידלג על אחזור מראש אם הבקשה דורשת קובצי Cookie מאומתים ספציפיים שיכולים להפעיל שינוי מצב בשרת.
    • קובץ שירות (service worker) קיים: אם קובץ שירות כבר שולט בטווח של כתובת ה-URL, רכיב WebView יכול להעביר את הטיפול למטפל בשליפה של קובץ השירות, במקום ליזום שליפה מראש רגילה מהרשת.
    • זמינות של שרת proxy: כדי למנוע כשל בבקשות ספקולטיביות בהגדרות רשת מורכבות, WebView מוודא שנתיב הרשת הנוכחי (כולל שרתי proxy מוגדרים) יציב.
  • אם לא מתבצעת אחזור מראש (גם אם הפרמטרים תקינים), זה קורה בדרך כלל כי WebView קבע שבקשת רקע עלולה להפריע לסשן הנוכחי של המשתמש או למצב האבטחה.
  • ביטול: משתמשים ב-CancellationSignal כדי להפסיק בקשה שנמצאת בתהליך ולמנוע את השמירה שלה במטמון.

עיבוד מראש של דפים

הרינדור המקדים יוצר "תוכן אינטרנט" מוסתר כדי לרנדר דף באופן מלא ברקע, כולל הפעלת סקריפטים ואחזור משאבי משנה. הטכנולוגיה של עיבוד מראש מסתמכת על אותה תשתית בסיסית כמו שליפה מראש (prefetch). אם אפליקציה מתחילה טרום-עיבוד, WebView מבצע קודם אחזור מראש של התגובה כדי להציג את הניווט של הטרום-עיבוד, וכך נמנעת פעילות מיותרת ברשת.

הטמעה

טרום-עיבוד הוא פעולה ברמת מופע WebView. התקשרות אל prerenderUrlAsync() באמצעות WebViewCompat משרשור UI.

Kotlin

WebViewCompat.prerenderUrlAsync(
    webView,
    url,
    cancellationSignal,
    executor,
    params,
    object : PrerenderOperationCallback {
        override fun onPrerenderActivated() {
            // Called when the user navigates to the URL and the hidden page is swapped in
        }

        override fun onError(exception: Throwable) {
            // exception is an instance of PrerenderException
            // Handle prerender failure (for example, memory pressure or disallowed JavaScript APIs)
        }
    }
)

Java

WebViewCompat.prerenderUrlAsync(webView, url, cancellationSignal, executor, params, new PrerenderOperationCallback() {
    @Override
    public void onPrerenderActivated() {
        // Called when the user navigates to the URL and the hidden page is swapped in.
    }

    @Override
    public void onError(@NonNull Throwable exception) {
        // Handle prerender failure (for example, resource constraints or disallowed APIs).
    }
});

גם אחזור מראש וגם טרום-עיבוד הם אסינכרוניים לחלוטין. אפשר לקרוא ל-prefetchUrlAsync() מכל שרשור, אבל צריך להפעיל את prerenderUrlAsync() משרשור UI.

מגבלות טכניות

כדי לאזן בין ניווט מיידי לבין תקינות המערכת, WebView אוכף את האילוצים הבאים בזמן הריצה:

  • עומס על הזיכרון: אם במכשיר יש מעט זיכרון RAM,‏ WebView מבטל כתובות URL שעברו טרום-עיבוד.
  • ממשקי API אסורים: כל ניסיון של JavaScript לגשת לממשקי API מסוימים (לדוגמה, הפעלת אודיו, התראות) בהקשר של רקע יגרום להפסקת הטרום-עיבוד באופן מיידי.
  • מגבלת מופעים: יש מגבלה על מספר כתובות ה-URL הפעילות שעברו טרום-עיבוד שמותרות לכל WebView.

התאמה של כתובות URL ו-No-Vary-Search ‏ (NVS)

רכיב WebView דורש אלגוריתם אמין להתאמה כדי לוודא שמשאב שנטען מראש מוצג רק בניווט המיועד שלו.

התאמה מדויקת לעומת התאמה לגרסה קרובה

כברירת מחדל, כדי להשתמש בטכנולוגיות של אחזור מראש וטרום-עיבוד, צריך שתהיה התאמה מדויקת של כתובת ה-URL. אם כתובת ה-URL שאליה מועברים זהה לכתובת ה-URL שנטענה מראש, היא מוצגת מיד מהמטמון. אם פרמטרים של שאילתות שונים, WebView משתמש בכללי No-Vary-Search (NVS) הבאים:

  • הרמז: המפתחים מספקים רמז מסוג setExpectedNoVarySearchHeader() במהלך ההפעלה. אם כתובת ה-URL שאליה מנווטים תואמת לכתובת ה-URL של הבקשה, ללא הפרמטרים שצוינו, WebView נחסם לזמן קצר כדי להמתין לכותרות בפועל של השרת.
  • כותרת השרת: הכותרת של תגובת ה-NVS מהשרת היא הסמכות העליונה. אם השרת מאשר שצריך להתעלם מההבדלים בשאילתה, התאמה מוגשת מהמטמון. אם לא, WebView חוזר לטעינת רשת קרה.

התכונה No-Vary-Search (NVS) מיועדת לשימוש מתקדם, ורוב המפתחים לא צריכים אותה כי הם מעבירים את אותה כתובת URL בדיוק גם לשליפה מראש וגם ל-loadUrl(). ההנחיות האלה נחוצות רק אם יש הבדלים בפרמטרים של השאילתה בין כתובת ה-URL של האחזור המקדים לבין כתובת ה-URL של loadUrl().

הגדרות כלליות

אפשר לשנות את ההתנהגות של טעינה ספקולטיבית ברמת הפרופיל על ידי הגדרת PrefetchCache מגבלות ומספר מקסימלי של טעינות מראש. אפשר גם לאפס את המגבלות המותאמות אישית של טעינה מראש בחזרה לברירות המחדל של המערכת:

Kotlin

// Configure prefetch cache limits
profile.prefetchCache.setMaxPrefetches(10)
profile.prefetchCache.setPrefetchTtlSeconds(60)

// Reset to system defaults when needed
profile.prefetchCache.clearMaxPrefetches()

// Configure maximum active prerenders
profile.setMaxPrerenders(2)

Java

// Configure prefetch cache limits
PrefetchCache prefetchCache = profile.getPrefetchCache();
prefetchCache.setMaxPrefetches(10);
prefetchCache.setPrefetchTtlSeconds(60);

// Reset to system defaults when needed
prefetchCache.clearMaxPrefetches();

// Configure maximum active prerenders
profile.setMaxPrerenders(2);

טיפול בשגיאות ובחריגות

בפעולות ספקולטיביות נעשה שימוש ב-OutcomeReceiverCompat או ב-PrerenderOperationCallback כדי לדווח על התוצאות.

חריגים ראשיים

כשפעולת טעינה מראש נכשלת, המטפל בשגיאות מדווח על אחד מסוגי החריגים העיקריים הבאים כדי לעזור לכם לאבחן תרחישי כשל ספציפיים:

  • PrefetchException: מחלקת הבסיס לכל השגיאות של טעינה מראש אסינכרונית.
  • PrefetchNetworkException: מציין כשל ברמת הרשת או השרת. הוא יכול לכלול שדה httpStatusCode (למשל 404 או 503) שיעזור לאבחן בעיות בצד השרת.
  • PrerenderException: מחלקת העל של כל השגיאות שקשורות לעיבוד מראש, כמו כשלים בגלל עומס על הזיכרון או שימוש בממשקי API אסורים (כמו הפעלת אודיו) ברקע.

אסטרטגיות אופטימיזציה

כדי למקסם את היתרונות של טעינה מראש ולחסוך במשאבי המערכת, מומלץ לפעול לפי ההמלצות הבאות:

  • התחלה מוקדמת: כדאי להתחיל את האחזור מראש במהלך הפעלת האפליקציה או ברגע שסביר להניח שיוצג יעד ניווט.
  • שיטה משולבת: אם מבצעים טרום-עיבוד של כתובת URL שכבר נמצאת במטמון של טרום-השליפה, הניווט של טרום-העיבוד מוגש מהמטמון הזה, וכך נמנעות בקשות רשת מיותרות.
  • מעקב אחרי מכסות: טרום-עיבוד צורך הרבה משאבים. מומלץ להשתמש באחזור מראש של כמה מועמדים סבירים ולשמור את הטכניקה של טרום-עיבוד לניווט הסביר ביותר.
  • תמיכה בסכימה: חשוב לוודא שכל כתובות ה-URL משתמשות בסכימת HTTPS שהיא חובה. תוכניות לא תקינות או קלט ריק מפעילים IllegalArgumentException סינכרוני.

מקורות מידע נוספים

למידע נוסף על ניפוי באגים באפליקציות אינטרנט, אופטימיזציה של ביצועי ההפעלה של WebView וטיפול בסיום של תהליך הרינדור, אפשר לעיין במקורות המידע הבאים: