Optimizing WebView load time in Helpshift’s SDK X

Sumeet Devidan
Dec 22, 2021 · 4 min read

WebViews in Android can be quite a pain when measured on performance and speed. It often takes a lot of time to fetch, cache and load the web content. To top it off, the caching mechanism provided by Android does not work consistently across OS versions and different device manufacturers.

With SDK X, we moved the Help Center and Chat sections to WebViews, to update features over the air. We also wanted to ensure optimal performance in terms of speed and smooth UX. That required us to develop our own caching mechanism to load static web content instantly after the first fetch.

Why do we need to write custom cache in WebView ?

The default caching options in Android do not work consistently across OS versions and various device OEMs.

Our testing with Websettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK). did not give us satisfactory results across device/OS range.

We were hitting many inconsistencies regarding content being loaded, cache expiry and fetching expired/updated content.

Given these problems, we decided to write our own caching mechanism to suite our needs.

Caching Mechanism

A good caching mechanism needs crisp answers to the following questions:

  1. What to cache?
  2. How to cache?
  3. How to invalidate the cache?

What to cache?

Since we have decided to develop our own caching mechanism, why not take it to the next step and dynamically update what is to be cached?

We fetch a configuration from our backend to dictate which web urls can be cached and the corresponding expiry time as well.

This configuration itself is updated every 24 hours, this allows us to update the web urls to be cached over the air. That essentially means that we can tune caching mechanism from backend.

Config example:

How to cache?

All network requests made by a webview in Android can be intercepted by registering a WebViewClient.

Overriding the “shouldInterceptRequest” method gives us an interception point for all outgoing network requests. We hook in this place to check if the request urls match the patterns provided in the configuration file. Note that all the invocations of “shouldInterceptRequest” method are on non-UI thread, meaning we can do a synchronous network call on this thread.

We only intercept GET requests, all other request go through without any manipulation.

Following gist helps understand the caching mechanism:

Following is the caching algorithm in brief:

  1. Read config file from disk, if available. Fetch the config again and store on disk if it is expired.
  2. When we intercept “shouldInterceptRequest”, check if it is a GET request and the url path exists in our config.
  3. Check if the file against this url path is already available on disk and the cache interval is not expired. If not expired, then serve this cached file as the response. We are done !
  4. If cache is expired or file isnt available then download this file. While downloading this file, also store the mimeType and the headers in the response. These will be used later to create a WebResourceResponse object.
  5. Update the corresponding ttl values for the matched url if the file is successfully fetched and stored.
  6. Always fallback to network if anything during this process fails. This way we make sure to load webview with correct content, no matter what.

How to invalidate the cache ?

Invalidating cache is quite tricky sometimes.

For us, invalidation works in 3 stages:

  1. The file itself is deleted from app’s sandbox.

— The user could delete the app’s cache manually to save space. This means that the cached file is deleted and we need to refetch it.

2. The ttl for that cache resource has expired.

— Each cached resource also has a ttl, which means at some point of time the resource will expire and we will fetch it again, for sure.

3. The request url has upgraded wrt path/query params.

— It could also happen that a new version of that cached resource is available. For this case, we store the ttl for a given url against its entire path including the query params. That helps us invalidate the cache as soon as the version for the url changes.

For example:

Cache entry “/content/abc.js?v=1.1” expires when we get “/content/abc.js?v=1.2” in the url path to be loaded in the webview.

This is because we will not have any cache entry for “/content/abc.js?v=1.2” and hence we will have to fetch it mandatorily.

How to clean up invalidated cached files?

On Application start, delete files from the cache folder which ever are not modified in last 1 month. Keeping it simple.

Keeps cached files from accumulating over time!

Results

Without caching: Loads in ~6 seconds

With caching: Loads in ~2 seconds.

Simple caching techniques gives us 65–70% better loading time !

helpshift-engineering

Engineering blog for Helpshift