页面加载速度优化

影响页面加载速度的因素有非常多,我们在对 WebView 加载一个网页的过程进行调试发现,每次加载的过程中都会有较多的网络请求,除了 web 页面自身的 URL 请求,还会有 web 页面外部引用的JS、CSS、字体、图片等等都是个独立的 http 请求。这些请求都是串行的,这些请求加上浏览器的解析、渲染时间就会导致 WebView 整体加载时间变长,消耗的流量也对应的真多。接下来我们就来说说几种优化方案来是怎么解决这个问题的。

选择合适的 WebView 缓存

WebView 缓存看似就是开启几个开关的问题,但是要弄懂这几种缓存机制还是很有深度。下图是腾讯某工程师总结六种 H5 常用的缓存机制的优势及适用场景。

图片来自Bugly
浏览器缓存机制:

主要前端负责,Android 端不需要进行特别的配置。

Dom Storage(Web Storage)存储机制:

配合前端使用,使用时需要打开 DomStorage 开关。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled(true);
Web SQL Database 存储机制:

虽然已经不推荐使用了,但是为了兼容性,还是提供下 Android 端使用的方法

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDatabaseEnabled(true);
final String dbPath = getApplicationContext().getDir("db",Context.MODE_PRIVATE).getPath();
webSettings.setDatabasePath(dbPath)
Application Cache 存储机制

Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。但 AppCache 是对浏览器缓存机制的补充,不是替代。

不过根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持 AppCache的,以后就不太确定了。同样给出 Android 端启用 AppCache 的代码。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setAppCacheEnabled(true);
final String cachePath = getApplicationContext().getDir("cache",Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(5*1024*1024);
Indexed Database 存储机制

IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。

Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
File System API

File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。很可惜到目前,Android 系统的 WebView 还不支持 File System API。

简单的介绍完了上面六种 H5 常用的缓存模式,想必大家能对 Android WebView 所支持的缓存模式有个粗略的了解。如果想和前端更好的配合使用 Android WebView 所支持的缓存,建议看下这篇文章《H5 缓存机制浅析 移动端 Web 加载性能优化》



常用资源预加载

上面介绍的缓存技术,能优化二次启动 WebView 的加载速度,那首次加载 H5 页面的速度该怎么优化呢?上面分析了一次加载过程会有许多外部依赖的 JS、CSS、图片等资源需要下载,那我们能不能提前将这些资源下载好,等H5 加载时直接替换呢?

好在从 API 11(Android 3.0)开始,WebView 引入了 shouldInterceptRequest 函数,这个函数有两种重载。

  • public WebResourceResponse shouldInterceptRequest(WebView webView, String url) 从 API 11 引入,API 21 废弃
  • public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request) 从 API 21 开始引入

考虑到目前大多数 App 还要支持 API 14,所以还是使用 shouldInterceptRequest (WebView view, String url) 为例。

WebView mWebView = (WebView) findViewById(R.id.webview);
mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView webView, final String url) {
                WebResourceResponse response = null;
                // 检查该资源是否已经提前下载完成。我采用的策略是在应用启动时,用户在 wifi 的网络环境下                // 提前下载 H5 页面需要的资源。
                boolean resDown = JSHelper.isURLDownValid(url);
                if (resDown) {
                    jsStr = JsjjJSHelper.getResInputStream(url);
                    if (url.endsWith(".png")) {
                        response = getWebResourceResponse(url, "image/png", ".png");
                    } else if (url.endsWith(".gif")) {
                        response = getWebResourceResponse(url, "image/gif", ".gif");
                    } else if (url.endsWith(".jpg")) {
                        response = getWebResourceResponse(url, "image/jepg", ".jpg");
                    } else if (url.endsWith(".jepg")) {
                        response = getWebResourceResponse(url, "image/jepg", ".jepg");
                    } else if (url.endsWith(".js") && jsStr != null) {
                        response = getWebResourceResponse("text/javascript", "UTF-8", ".js");
                    } else if (url.endsWith(".css") && jsStr != null) {
                        response = getWebResourceResponse("text/css", "UTF-8", ".css");
                    } else if (url.endsWith(".html") && jsStr != null) {
                        response = getWebResourceResponse("text/html", "UTF-8", ".html");
                    }
                }
                // 若 response 返回为 null , WebView 会自行请求网络加载资源。 
                return response;
            }
        });

private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
        WebResourceResponse response = null;
        try {
            response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(getJSPath() + TPMD5.md5String(url) + style)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return response;
    }

public String getJsjjJSPath() {
        String splashTargetPath = JarEnv.sApplicationContext.getFilesDir().getPath() + "/JS";
        if (!TPFileSysUtil.isDirFileExist(splashTargetPath)) {
            TPFileSysUtil.createDir(splashTargetPath);
        }
        return splashTargetPath + "/";
    }
常用 JS 本地化及延迟加载

比预加载更粗暴的优化方法是直接将常用的 JS 脚本本地化,直接打包放入 apk 中。比如 H5 页面获取用户信息,设置标题等通用方法,就可以直接写入一个 JS 文件,放入 asserts 文件夹,在 WebView 调用了onPageFinished() 方法后进行加载。需要注意的是,在该 JS 文件中需要写入一个 JS 文件载入完毕的事件,这样前端才能接受都爱 JS 文件已经种植完毕,可以调用 JS 中的方法了。 附上一段本地化的 JS 代码。

javascript: ;
(function() {
  try{
    window.JSBridge = {
      'invoke': function(name) {
        var args = [].slice.call(arguments, 1),
          callback = args.pop(),
          params, obj = this[name];
        if (typeof callback !== 'function') {
          params = callback;
          callback = function() {}
        } else {
          params = args[0]
        } if (typeof obj !== 'object' || typeof obj.func !== 'function') {
          callback({
            'err_msg': 'system:function_not_exist'
          });
          return
        }
        obj.callback = callback;
        obj.params = params;
        obj.func(params)
      },
      'on': function(event, callback) {
        var obj = this['on' + event];
        if (typeof obj !== 'object') {
          callback({
            'err_msg': 'system:function_not_exist'
          });
          retrun
        }
        if (typeof callback !== 'undefined') obj.callback = callback
      },
      'login': {
        'func': function(params) {
          prompt("login", JSON.stringify(params))
        },
        'params': {},
        'callback': function(res) {}
      },
      'settitle': {
        'func': function(params) {
          prompt("settitle",JSON.stringify(params))
        },
        'params': {},
        'callback': function(res) {}
      },
    }catch(e){
    alert('demo.js error:'+e);
  }
  var readyEvent = document.createEvent('Events');
  readyEvent.initEvent('JSBridgeReady', true, true);
  document.dispatchEvent(readyEvent)
})();
Logo

前往低代码交流专区

更多推荐