Public number: Byte array hope to help you 🤣🤣

In order to meet the requirements of cross-platform and dynamic, Hybrid is now adopted by many apps to meet the changing business requirements. Hybrid is also called Hybrid development, that is, half Native and half H5. It realizes business requiring high flexibility through WebView, and realizes interaction between both ends through JsBridge when it is necessary to interact with Native or invoke specific platform capabilities

There are many reasons to adopt Hybrid solution: to achieve cross-platform and dynamic update, to maintain the unification of business and logic between each end, to meet the needs of rapid development; There’s only one reason to abandon Hybrid: Performance is much worse than Native. WebView is criticized for its poor performance compared with Native. It often takes a period of time to load, resulting in poor user experience. Once the basic business requirements are met, developers need to further optimize the user experience. At present, there are many general methods to optimize the time and performance cost of WebView showing the first screen page, and these optimization methods are not limited to a platform, for Android and IOS are mostly common, of course, this can not be separated from the front-end and server support. This article will make a summary of these optimization schemes, hoping to help you 🤣🤣

1. Performance bottlenecks

If you want to optimize a WebView, you need to know what performance bottlenecks are limiting the WebView

Baidu APP once counted the 80-minute data of the first screen display speed of the landing page of the whole network users on a certain day, and it took approximately 2600 ms from click to display on the first screen (the first picture was loaded)

Baidu’s developers divided the process into four stages and calculated the average time it took to complete each

  • It took 260 ms to initialize the Native App component. The main job is to initialize the WebView. The average WebView creation time is 500 ms for the first time, and much faster for the second time
  • It took 170 ms to initialize Hybrid. The main tasks are as follows: it takes roughly 100 ms to verify the Hybrid template delivered locally according to the related parameters passed in the call-up protocol; The webview.loadURL execution triggers the resolution of the Hybrid template header and Body
  • It took 1400 ms to load the body data and render the page. The main work is: Load the JS file required to parse the page, and initiate the request for the body data through the ABILITY of THE JS calling end. After the client gets the data from the Server, it sends it back to the front end in the way of JsCallback. The front end needs to parse the JSON format body data from the client and construct the DOM structure. This triggers the kernel’s rendering process; This process involves the JS request, loading, parsing, execution and a series of steps, and there are terminal ability call, JSON parsing, DOM construction and other operations, which are time-consuming
  • It took 700 ms to load the image. The main work is: In the previous step, the front-end access to the body of the data set contains the landing page picture address, after the complete text rendering, need front pictures again request side ability, client side receives the order request to the server after image address set, after the download is complete, the client will invoke a IO will file is written to the cache, At the same time, the local address of the corresponding image is sent back to the front end, and finally the kernel initiates another IO operation to obtain the image data stream for rendering

As can be seen, the most time-consuming is loading the body data and rendering the page and loading the image two stages, requiring multiple network requests, JS calls, IO read and write; Secondly, initialization of WebView and loading of template file are two stages, which take similar time. Although network request is basically not required, initialization of browser kernel and template file is involved, and some unavoidable time costs exist

This leads to the most basic optimization direction:

  • Can the initialization time be faster? For example, can webViews and template files be initialized with less time? Can you finish these tasks ahead of time?
  • Could there be fewer front-loading tasks to complete the first screen? For example, can network requests, JS calls, IO reads and writes be made less often? Can these tasks be consolidated or completed ahead of schedule?
  • Can resource files load faster? For example, can images, JS, CSS files be requested less often? Can I use local cache directly? Can the network request speed be faster?

2. WebView preloading

Creating a WebView is a time-consuming operation, especially if it takes a few hundred milliseconds to create for the first time due to the need to initialize the browser kernel. Creating a WebView again is much faster, but still takes tens of milliseconds. In order to avoid the need to wait for the completion of WebView creation synchronously every time we use it, we can choose to preload the WebView at an appropriate time and store it in the cache pool. When we need to use it, we can directly retrieve it from the cache pool, thus shortening the time for displaying the first screen page

To preload, think about how to solve two problems:

  • How to choose the trigger time?

    Since creating a WebView is a time-consuming operation, we may also slow down the main thread during preloading, which will just be a time-consuming operation ahead of time. We need to make sure that the preloading does not affect the main thread task

  • How do you choose Context?

    Webviews need to be bound to the Context, and each WebView should correspond to a specific instance of the Activity Context. You cannot create a WebView using Application directly. We need to ensure consistency between the preloaded WebView Context and the final Context

The first problem can be solved by IdleHandler. Tasks submitted by the IdleHandler will only be executed if the MessageQueue associated with the current thread is empty, so pre-creation by the IdleHandler ensures that the current main thread task will not be affected

The second problem can be solved with MutableContextWrapper. As the name suggests, MutableContextWrapper is a system-provided Context wrapper class that contains a baseContext. All internal methods of MutableContextWrapper are implemented by baseContext. And MutableContextWrapper allows its baseContext to be replaced externally, so we can start with Application as baseContext, Wait until the WebView and Activity are actually bound

The general logic for the final pre-loading of the WebView is as follows. PrepareWebView () can be preloaded by calling prepareWebView() when PageFinished or exiting WebViewActivity, and dynamically added to the layout file from the cache when needed

/ * * *@Author: leavesC
 * @Date: 2021/10/4 18:57
 * @Desc: * @ public number: byte array */
object WebViewCacheHolder {

    private val webViewCacheStack = Stack<RobustWebView>()

    private const val CACHED_WEB_VIEW_MAX_NUM = 4

    private lateinit var application: Application

    fun init(application: Application) {
        this.application = application
        prepareWebView()
    }

    fun prepareWebView(a) {
        if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) {
            Looper.myQueue().addIdleHandler {
                log("WebViewCacheStack Size: " + webViewCacheStack.size)
                if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) {
                    webViewCacheStack.push(createWebView(MutableContextWrapper(application)))
                }
                false}}}fun acquireWebViewInternal(context: Context): RobustWebView {
        if (webViewCacheStack.isEmpty()) {
            return createWebView(context)
        }
        val webView = webViewCacheStack.pop()
        val contextWrapper = webView.context as MutableContextWrapper
        contextWrapper.baseContext = context
        return webView
    }

    private fun createWebView(context: Context): RobustWebView {
        return RobustWebView(context)
    }

}
Copy the code

This solution does not reduce the time required to create a WebView, but it can shorten the time to complete the first screen page. It should be noted that the WebView cache adopts the method of exchanging space for time, and the small operation of low-end models should be considered

Third, rendering optimization

If you want to optimize the rendering speed of the first screen, you should first look at the link of the whole page access request. Borrowing a picture of Alibaba Tao System technology, the following is the link of H5 page access on the conventional end

This whole process needs to complete multiple network requests and IO operations. After loading the basic HTML and CSS files, WebView obtains the body data from the server through JS. After getting the data, it also needs to complete a series of time-consuming operations such as parsing JSON, constructing DOM and applying CSS styles. Finally, the kernel renders the screen

The system version, processor speed and memory size of the mobile terminal are completely out of our control, and are easily affected by network fluctuations. The time consuming of network links is very long and uncontrollable. If the WebView goes through all the above steps repeatedly every time it renders, then the user experience is completely out of control. In this case, you can try to optimize it through the following methods

Preset offline package

  • Multiple sets of template files can be generated according to the service type. The latest template file is preset to the client when packaging each set of template file. Each set of template file has a specific version number. In this way, the total time is reduced by avoiding network requests for each use
  • Generally, the WebView will load the JS and CSS files in HTML after loading the main HTML, which requires several IO operations successively. We can inline the JS and CSS as well as some images into a file, so that the template load only needs one IO operation. It also greatly reduces the problem of template loading failure caused by IO loading conflicts

Parallel requests

  • When H5 loads the template file, the Native terminal requests the body data, and then the Native terminal sends the body data to H5 through JS, so as to realize the parallel request and shorten the total time

preload

  • When the template and body data are separated, since the WebView uses the same template file every time, we do not need to load the template when the user enters the page. We can directly preload the WebView and let it warm up and load the template. In this way, we only need to send the body data to H5 every time. H5 can render the page directly after receiving the data
  • For Feed streams, there is a policy to preload body data so that when a user clicks to see details, the cached data can ideally be used directly, protected from network impact

Lazy loading

  • The more dependencies required to display the first screen, the longer the waiting time of the user. Therefore, the operations to be performed before the completion of the first screen should be reduced as much as possible. For some network requests, JS calls, and buried point reports that are not necessary for the first screen, they can be performed after the first screen display

The page comes out statically

  • Although the concurrent request for body data can shorten the total time, it still needs to complete a series of time-consuming operations such as parsing JSON, constructing DOM, applying CSS styles, etc., before it can be handed to the kernel for rendering on the screen. In this case, the operation of assembling HTML is time-consuming. In order to further shorten the total time, it can be changed to integrate the text data and front-end code by the back end to directly display the content on the first screen. The straight HTML file already contains the content and style required for the first screen display, without secondary processing, and the kernel can directly render. Other dynamic content can be loaded asynchronously after rendering the first screen
  • Since the client may provide users with options to control WebView font size and night mode, in order to ensure the accuracy of first-screen rendering results, the HTML directly output by the server needs to reserve some placeholders for subsequent dynamic backfill. The client uses regular matching to find these placeholders before loadUrl. Mapping to end information by protocol. The HTML content that has been backfilled by the client has all the conditions for displaying the first screen

Reuse the WebView

  • A further step is to try to reuse webViews. Since the template file used by WebView is fixed, we can add the logic of reusing WebView on the basis of WebView preloading cache pool. When the WebView is used up, all its body data can be emptied and stored in the cache pool again. The next time you need it, you can directly inject new body data for reuse, reducing the overhead of frequently creating webViews and preheating template files

Visual optimization

After the implementation of the above optimization scheme, the page display speed has been very fast, but in actual development, there will still be a problem that the H5 page cannot be rendered during the Activity switch, resulting in a visual blank screen phenomenon, which can be verified by slowing down the animation time in developer mode

As you can see from the following figure, there is a noticeable white screen during the Activity switch

Through the study of the system source can know, in the system version is greater than or equal to 4.3, less than or equal to 6.0 between ViewRootImpl in the process of View drawing, Will pass a Boolean variable mDrawDuringWindowsAnimating Window animation in the implementation of the process to control whether to allow for rendering, the field defaults to false, the way we can use reflection to manually modify this property, avoid the bad effect

This solution is basically only available for Android version 6.0, and is rarely adapted for lower versions

/** * Render the page normally during the activity Transition animation */
fun setDrawDuringWindowsAnimating(view: View) {
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
        || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1
    ) {
        // This problem does not exist when the value is smaller than 4.3 and greater than 6.0
        return
    }
    try {
        val rootParent: ViewParent = view.rootView.parent
        val method: Method = rootParent.javaClass
            .getDeclaredMethod("setDrawDuringWindowsAnimating".Boolean: :class.javaPrimitiveType)
        method.isAccessible = true
        method.invoke(rootParent, true)}catch (e: Throwable) {
        e.printStackTrace()
    }
}
Copy the code

The effect after optimization

Http cache policy

In the last step of rendering optimization, network request optimization is involved, including reducing the number of network request, parallel execution of network request, network request pre-execution, etc. Network requests are unavoidable for applications, but you can set a caching policy to avoid repeating network requests, or you can make non-initial network requests at a lower cost, which is related to Http caching

WebView supports the following four cache policies. The default one is LOAD_DEFAULT, which belongs to the Http cache policy

  • LOAD_CACHE_ONLY: only local cache is used, no network requests are made
  • LOAD_NO_CACHE: does not use the local cache, only requests over the network
  • LOAD_CACHE_ELSE_NETWORK: use as long as there is a local cache, otherwise request over the network
  • LOAD_DEFAULT: Determines whether to make network requests based on the Http protocol

Take a static file on the request network for example, and look at the response header. The cache-Control, Expires, Etag, last-Modified, and other information in the response header defines the specific Cache policy

Cache-control, Expires

Cache-control is a new Http 1.1 header that defines resource caching policies. It consists of instructions that define when, how, and for how long a response resource should be cached. No-cache, no-store, only-if-cached, max-age, etc. For example, in the figure above, max-age is used to set the maximum valid time of a resource in seconds

Expires is an Http 1.0 field that has a similar meaning to cache-Control. However, cache-Control is currently used because Expires can invalidate a Cache due to client and server time inconsistencies. Cache-control is also higher in priority

Cache-control is also a generic Http header field. It can be used in request headers and response headers respectively and has different meanings. Take max-age as an example:

  • Request header: Used by the client to tell the server that it wants to receive a resource with a validity period of max-age or less
  • Response header: The server notifies the client that the resource is valid within the max-age period after the request is initiated. The 2592000 seconds shown in the preceding figure is 30 days. The client does not need to send requests to the server within 30 days after the first request is initiated and can directly use the local cache

If LOAD_DEFAULT is used in a WebView, the Http cache policy is followed and the WebView uses the local cache directly during the validity period

ETag, last-modified

Cache-control prevents the WebView from requesting resources repeatedly within the validity period. After the validity period expires, the WebView still needs to request the network again. However, at this time, the resources on the server may not change, and the WebView can still use the local Cache. The client then relies on the ETag and Last-Modified headers to confirm to the server that the resource is still usable

When the resource is first requested, the response header contains ETag and Last-Modified, which are used to uniquely identify the resource file

  • ETag: Unique identifier of a resource
  • Last-modified: Records the time when a resource was Last Modified

After the client detects that the max-age has expired, it carries these two headers to execute network requests. The server uses these two identifiers to determine whether the client’s cache resources can be used

As shown in the figure below, after the expiration date, the client carries in the if-none-match request header the ETag value received during the first network request. In fact, only one ETag and last-Modified can be used, and only ETag is used here; If last-modified is passed, the corresponding request header is if-modified-since

If the server determines that the resource has expired, it will return a new resource file. This is equivalent to the first request for the resource file, and the subsequent operations are the same as the beginning. If the server resources has not yet expired, will return a status code of 304, to inform the client can continue to use the local cache, the client update Max – age value at the same time, repeat the first cache invalidation rule, so that the client can use very low cost to complete the network request, the bigger the requested resource file is particularly useful when

However, there are some problems with the Http cache policy, namely how to ensure that users can immediately sense and re-download the latest resources when they are updated. Assume that the server updates the resource content within the validity period of the resource. In this case, the client is still in the Max-age phase and cannot detect the resource update immediately. As a result, the update fails. A better solution is: requests the service side when each update resource files are generated a new name for it, you can use hash value or random number naming, and resource files based on master file each time you release a reference to the latest resource file path, to ensure that the client can immediately perceived resource has been updated, to ensure timely update. In this way, it is possible to set a very large max-age value for the resource file, try to make the client only use the local cache, and ensure that the client can be updated every time the release is made

Therefore, by reasonably setting the Http cache policy, on the one hand, it can obviously reduce the server network bandwidth consumption, reduce the pressure and overhead of the server, on the other hand, it can also reduce the client network delay, avoid repeated requests for resource files, and speed up the page opening speed. After all, loading local cache files is much cheaper than loading them from the network anyway, right

Intercept request and shared cache

Nowadays, WebView pages are often mixed with pictures and texts. Pictures are an important form of information applications. There are two traditional schemes for WebView to obtain image resources:

  • H5 itself downloads resources through network requests. Advantages: Simple implementation, each end can only focus on their own business. Disadvantages: The cache between the two ends cannot be shared, resulting in repeated resource requests and traffic waste
  • H5 terminal obtains resources by invoking Native image download and caching capabilities. Advantages: Cache sharing can be realized between the two ends. Disadvantages: Native execution needs to be actively triggered by the H5 terminal, the timing is relatively delayed, and resource transfer needs to be completed through multiple JS calls, resulting in efficiency problems

Both of the above schemes have some disadvantages, such as inability to share the cache or efficiency problems. Here is another improvement scheme:

In fact, the WebViewClient provides a shouldInterceptRequest method to support external interception of requests. The WebView calls back this method every time it requests a web resource. The method entry contains the Url, Header and other request parameters. The return value WebResourceResponse represents the obtained resource object. The default value is null, which means the browser kernel completes the network request

We can use this method to actively intercept and complete the loading operation of the picture, so that we can not only make the resource file at both ends can be shared, but also avoid the efficiency problems caused by multiple JS calls

The rough implementation looks like this, where I proxy the network request through OkHttp

/ * * *@Author: leavesC
 * @Date: 2021/10/4 18:56
 * @Desc: * @ public number: byte array */
object WebViewInterceptRequestProxy {

    private lateinit var application: Application

    private val webViewResourceCacheDir by lazy {
        File(application.cacheDir, "RobustWebView")}private val okHttpClient by lazy {
        OkHttpClient.Builder().cache(Cache(webViewResourceCacheDir, 100L * 1024 * 1024))
            .followRedirects(false)
            .followSslRedirects(false)
            .addNetworkInterceptor(
                ChuckerInterceptor.Builder(application)
                    .collector(ChuckerCollector(application))
                    .maxContentLength(250000L)
                    .alwaysReadResponseBody(true)
                    .build()
            )
            .build()
    }

    fun init(application: Application) {
        this.application = application
    }

    fun shouldInterceptRequest(webResourceRequest: WebResourceRequest?).: WebResourceResponse? {
        if (webResourceRequest == null || webResourceRequest.isForMainFrame) {
            return null
        }
        valurl = webResourceRequest.url ? :return null
        if (isHttpUrl(url)) {
            return getHttpResource(url.toString(), webResourceRequest)
        }
        return null
    }

    private fun isHttpUrl(url: Uri): Boolean {
        val scheme = url.scheme
        log("url: $url")
        log("scheme: $scheme")
        if (scheme == "http" || scheme == "https") {
            return true
        }
        return false
    }

    private fun getHttpResource(
        url: String,
        webResourceRequest: WebResourceRequest
    ): WebResourceResponse? {
        val method = webResourceRequest.method
        if (method.equals("GET".true)) {
            try {
                val requestBuilder =
                    Request.Builder().url(url).method(webResourceRequest.method, null)
                val requestHeaders = webResourceRequest.requestHeaders
                if(! requestHeaders.isNullOrEmpty()) {var requestHeadersLog = ""
                    requestHeaders.forEach {
                        requestBuilder.addHeader(it.key, it.value)
                        requestHeadersLog = it.key + ":" + it.value + "\n" + requestHeadersLog
                    }
                    log("requestHeaders: $requestHeadersLog")}val response = okHttpClient.newCall(requestBuilder.build())
                    .execute()
                val body = response.body
                if(body ! =null) {
                    val mimeType = response.header(
                        "content-type", body.contentType()? .type ).apply { log(this)}val encoding = response.header(
                        "content-encoding"."utf-8"
                    ).apply {
                        log(this)}val responseHeaders = mutableMapOf<String, String>()
                    var responseHeadersLog = ""
                    for (header in response.headers) {
                        responseHeaders[header.first] = header.second
                        responseHeadersLog =
                            header.first + ":" + header.second + "\n" + responseHeadersLog
                    }
                    log("responseHeadersLog: $responseHeadersLog")
                    var message = response.message
                    val code = response.code
                    if (code == 200 && message.isBlank()) {
                        message = "OK"
                    }
                    val resourceResponse =
                        WebResourceResponse(mimeType, encoding, body.byteStream())
                    resourceResponse.responseHeaders = responseHeaders
                    resourceResponse.setStatusCodeAndReasonPhrase(code, message)
                    return resourceResponse
                }
            } catch (e: Throwable) {
                log("Throwable: $e")}}return null
    }

    private fun getAssetsImage(url: String): WebResourceResponse? {
        if (url.contains(".jpg")) {
            try {
                val inputStream = application.assets.open("ic_launcher.webp")
                return WebResourceResponse(
                    "image/webp"."utf-8", inputStream
                )
            } catch (e: Throwable) {
                log("Throwable: $e")}}return null}}Copy the code

The benefits of adopting this scheme are:

  • OkHttp Cache is not limited to specific file types. It can be used for images, HTML, JS, CSS, etc
  • OkHttp is fully Http compliant, and we are free to extend Http caching policies based on this
  • Decoupled the client and front-end code, by the client to act as the role of the Server, for the front-end is completely unaware, with a relatively low cost to achieve both ends of the cache sharing
  • The maximum cache space allowed by WebView’s own cache mechanism is relatively small. This scheme is equivalent to breaking through the maximum cache capacity limit of WebView
  • If the mobile terminal has preset offline package, then it can judge whether the offline package contains the target file through this scheme. If there is any, it can be used directly. Otherwise, it can be networked to request, refer to the abovegetAssetsImagemethods

It is important to note that the above code is only a sample and cannot be used directly in a production environment. Readers need to expand the code according to their specific business. There is also an open source library on Github that implements WebView cache reuse using this solution, and you can take a look at this idea: CacheWebView

DNS optimization

DNS, also known as domain name resolution, refers to the process of converting a domain name into a specific IP address. The DNS is cached at the system level. If a domain name has been resolved, the system can directly access the known IP address in the next use, without initiating the DNS and then accessing the IP address

If the primary domain name accessed by the WebView is inconsistent with that of the client, the WebView needs to complete domain name resolution before requesting resources for the first time when accessing online resources. This process takes tens of milliseconds longer. Therefore, it is best to keep the client’s overall API address, resource file address, and main domain name of WebView online address consistent

7. CDN acceleration

The full name of CDN is Content Delivery Network. CDN is an intelligent virtual network built on the basis of the existing network. It relies on the edge servers deployed in various places, through the central platform of load balancing, content distribution, scheduling and other functional modules, users can get the content nearby, reduce network congestion, and improve user access response speed and hit ratio

By hosting static type files such as JS, CSS, pictures and videos to CDN, users can receive these files from the server closest to them when loading web pages, which solves the network delay caused by remote access and line access of different network bandwidths

Eight, white screen detection

Under normal circumstances, after completing the above optimization measures, the user can basically open the H5 page in seconds. However, there are always exceptions. The user’s network environment and system environment are very different, and even the WebView can have internal crash. When there is a problem, the user may see a blank screen directly, so the further optimization means is to detect whether there is a blank screen and the corresponding countermeasures

The most intuitive solution to detect a white screen is to take a screenshot of WebView and traverse the color value of pixels in the screenshot. If the color of a non-white screen exceeds a certain threshold, it is considered that the screen is not white. Bytedance’s technical team did this: Use view.getdrawingCache () to get the Bitmap object containing WebView, and then reduce the screenshot to 1/6 of the original image, traversing the pixels of the detected image. When the non-white pixels are greater than 5%, it can be considered as non-white screen. It can be relatively efficient and accurate to determine whether there is a white screen

When a blank screen is detected, if it cannot be successfully tried again, it can only be degraded. Give up the above optimization measures and directly load the details page on the line to ensure user experience first

Ix. Reference materials

Part of the content of this article is directly quoted from the following articles. Specific performance bottlenecks, optimization methods and optimization effects can only be summarized after a large number of users. The scheme introduced in the following articles is very convincing, and it is recommended for readers to read it

  • Baidu APP-Android H5 first screen optimization practice
  • Toutiao quality optimization – Graphic details page second practice
  • Alibaba Tao Technology – front-end performance optimization

Finally, of course, there is the RobustWebView sample code for this article