Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

41. 秒开 hybrid H5 优化记 #41

Open
funfish opened this issue Aug 22, 2019 · 0 comments
Open

41. 秒开 hybrid H5 优化记 #41

funfish opened this issue Aug 22, 2019 · 0 comments

Comments

@funfish
Copy link
Owner

funfish commented Aug 22, 2019

记得刚做前端,接手移动端 H5 的时候,特别想要将应用优化到极致,想要达到秒开,流畅接近原生的效果,只是业务需求下一直没有时间去做这样或者那样的优化。这次自己接手一个 hybrid H5 项目,做完业务之后,一直想要优化,刚好又是我一个人负责前端,于是将平时的想法收集起来,周六加班做了个深入的优化(可惜才过了四天,就被通知项目要移交到其他团队)。避免涉密,后面的数据,都做了稍微修改。

初始问题

这个项目是基于百度地图做了一个应用,开始存在两个问题,一个是首屏白屏问题;从点击进入到开始到有内容的阶段,有个明显白屏的时间,这个时间是包含 webview 初始化,以及首屏渲染的时间。该首屏渲染的时间,FP 的时间在 Fast 3G 下,大概为 9000+ ms。原本的系统,其实已经做了路由的懒加载了。另外一个百度地图渲染漂移问题。

对于上面的问题,一共做了七层优化,尤其是首屏加载问题。

百度地图

用 vue-baidu-map 来作为 Vue 项目的百度地图组件。只是在移动端存在严重的问题,其覆盖物在移动端渲染性能差,稍微用手拖动一下百度地图,其上面的文字或者自定义的图形都会出现颤抖,而在 pc 端是没有这样的问题,官网的示例也是如此,只是采用覆盖物-点的方式,却能很好的避免颤抖的情况。

若是直接采用百度地图的方式,而不是用 vue-baidu-map,其效果会好很多,不会有颤抖问题。只是想要试试新的 vue-baidu-map,而不是一直用老的方式。由于百度地图的代码没有开源出来,查看 vue-baidu-map 中的实现方式,也无特别收获, vue-baidu-map 只是做了一个 Vue 和百度地图的数据驱动的绑定而已,这给调试代码带来了很大的阻挠。后面为了方便调试, 采用 chrome 的 rendering 来调试代码,发现自定义覆盖物在拖动地图的时候,会反复变深绿色,而使用点覆盖物,只会时不时变深绿色。点覆盖物性能确实要比自定义以及其他覆盖物要很多。后面改为点覆盖物,效果真的提高了不少。

另外经过多次调试后发现高德地图真的要比百度地图好,有三维模式,而且 webview 支持好一些,只是定位没有百度地图准。

首屏优化

由于项目需要适配多语言,而之前的语言包,加起来有 1M+ 的大小,只是里面的冗余数据比较多,需要用到信息并没有那么多,于是采用 nodejs,对每个语言文件进行解析,输出对应的简化版本的 json 文件。nodejs 采用 walkdir 模块遍历所有语言包,并输出为简化版本文件。可以将包的体积减少到 3/5 的水平,并采用按需加载。

lottie 优化

为了更好的还原动画采用的是 lottie + json 数据的方式,实现动效。只是设计最后给出来的一个动效 html 都要接近 300kb,这个是无法接受的,而且其实动画尺寸非常小,就是个简单 icon,采用 30 帧的序列帧体积也要 170kb,很大,而且设计为了统一管理,统一规范,推荐的还是采用 lottie 方案。后面在一篇腾讯 alloyteam 的文章里面有介绍到 lottie-web 仓库的 lottie_light.min.js 只用 140+kb,完整版的要 240+ kb,虽然只支持 svg,但是已经很够用了。

再加上异步组件和懒加载 lottie_light.min.js 和对应的 json 数据,可以大大的减少首屏渲染压力

// 异步组件方式
components: {
  LottieComponent: () => import("./LottieComponent");
}
// 懒加载Lottie文件
const [lottie, lottieAnimationData] = await Promise.all([
  import(/* webpackChunkName: "lottieLightMin" */ "./lottie_light.min"),
  import(/* webpackChunkName: "lottieComponent" */ "./lottieComponentData")
]);

使得 lottie + json 数据文件大小在接近 200kb 的水平,并达到了按需加载的目的。

最后还想采用将 lottie 文件内置到客户端里面,请求的时候,拦截返回 lottie 文件给到前端就可以了,可惜客户端做的是小白。。。。

图形压缩

之前介绍过图像优化,该项目使用的图像有不是很多,通过有效压缩,可以减少 30% 的体积,只是需要注意的是有些首页的图像,在低于 10 kb 的时候,会被 Vue-cli3 打包进首页的 js 文件里面,导致文件臃肿,于是需要观察图片大小,以及配置对应的 vue.config 来达到最优解,本项目刚好是 10 kb, 附近有几个图像被打包进去了,修改 loader 对应的配置值就可以了。

prefetch 的问题

通过 chrome 的 performance 调试的时候发现,首页加载的时候会同时记载其他文件,包括所有的语言包都加载进来了。只是不是做了按需加载的处理了吗?其实,vue-cli 3 对项目的默认处理是将需要加载的文件都加载上,另外按需加载的文件,会用 link 链接的方式,并设置为 prefetch 来获得,初衷是好的,prefetch 的资源优先级最低,不会和当前需要的 js 文件抢优先级。只是这样有个严重问题,由于浏览器在 http 1.1 下允许同时发送 6 ~ 8 个网络请求,于是当首页的 js 文件下载的同时,存在空闲连接,其他的 prefetch 请求也会被发送出去。导致了和首页 js 文件抢夺有限带宽的情况。

根据这个情况需要修改 vue.config.js 中的配置,就可以了。

config.plugins.delete("prefetch");

经过上面几步下来,首屏渲染时间 FP 时间已经缩短到 4000+ms 了。

百度地图优化

通过 performance 再次分析发现,在首页初始化的时候,会把百度地图也加载上去,只是初始过程并不会直接渲染百度地图,而是有个和服务端交互的过程,这个过程会消耗几秒钟,之后才会显示出百度地图,这样的话,其实百度地图是不用打包进入首页的 chunk 文件的,可以异步加载。只是按照官网的介绍,vue-baidu-map 需要作为插件在 Vue 里面使用。如下方式:

import Vue from "vue";
import BaiduMap from "vue-baidu-map";

Vue.use(BaiduMap, {
  ak: "YOUR_APP_KEY"
});

这种方式 100%会将 vue-baidu-map 打包进首页的包里面。那如何避免呢?这个就要分析 Vue.use 里面的源码了。

export function initUse(Vue: GlobalAPI) {
  Vue.use = function(plugin: Function | Object) {
    const installedPlugins =
      this._installedPlugins || (this._installedPlugins = []);
    if (installedPlugins.indexOf(plugin) > -1) {
      return this;
    }

    // additional parameters
    const args = toArray(arguments, 1);
    args.unshift(this);
    if (typeof plugin.install === "function") {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === "function") {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this;
  };
}

不难发现 Vue.use 最后执行的是 vue-baidu-map 的 install 方法,并传入 Vue 以及后面的参数对象。于是回头看 install 方法

install (Vue, options) {
  const {ak} = options
  Vue.prototype._BMap = () => ({ak})

  Vue.component('baidu-map', BaiduMap)
  Vue.component('bm-view', BmView)
  // 省略其他的组件注释
}

// _BMap 使用方法
const ak = this.ak || this._BMap().ak;

install 里面的主要功能一个是给 Vue 构造函数的原型传入 _BMap 方法,_BMap 会在 Map 组件初始化的时候使用。于是摆在面前的就有两个问题

  1. 能不能在组件里面使用 Vue.use 方法动态注册组件
  2. isntall 里面 Vue 构造函数问题:包括了 _BMap 方法挂载,以及组件注册问题问题

如果在组件里面引用 Vue.use 会发现此 Vue 非彼 Vue,即是引入的 Vue 和初始实例化的 Vue 的作用是有差别的,若使用同一个 Vue 函数,控制台又会提示其他问题。所以直接使用 Vue.use 在组件里面注册插件是不行的。那如果换成组件本身,用 this.use 呢,很可惜没有这个方法。这个时候可以看看 Vue 关于组件的源码:

// _createElement 函数里面
if (typeof tag === "string") {
  // 省略部分代码
  if (
    (!data || !data.pre) &&
    isDef((Ctor = resolveAsset(context.$options, "components", tag)))
  ) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag);
  }
}
// createComponent 里面
function createComponent(Ctor, data, context, children, tag) {
  // 省略部分代码
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }
}
// extend 方法
Vue.extend = function(extendOptions) {
  // 省略部分代码
  const Super = this;
  const Sub = function VueComponent(options) {
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend;
  Sub.mixin = Super.mixin;
  Sub.use = Super.use;
};

可以发现 组件的创建其实也是 Vue 的继承关系 ,所以如果想在组件里面用 use 方法的话,就是 this.constructor.use 了,只是方法是可以用了,但是传入 vue-baidu-map 的不是 Vue 构造函数本身,而是组件的构造函数,这个构
造函数可以满足 install 方法里面的组件注册,调试一下,发现可以用~~只是其构造函数的 prototype._BMap 并不是 Vue 构造函数,完全不搭边,好在可以 vue-baidu-map 可以通过传递 this.ak 来解决问题;
最后的动态插件实现如下:

created() {
  import('vue-baidu-map').then(BaiduMap => {
    BaiduMap.install = BaiduMap.default.install;
    this.constructor.use(BaiduMap);
    this.baiduLoading = false;
  })
}

上面的方式就可以将百度地图三方库从首页中拆分来,最后首页必须加载文件大小总减少 108 kb 。将首屏 FP 时间压缩到 3000+ms,首屏文件 js 加载大小为 200+ kb 的样子。基本上在 4G 网络或者 wife 的情况下,就可以 1s 内刷新出来了。

做到这一步差不多也就可以了,只是精益求精,还是想要提前 FP 时间。

包体积优化再分析

再回头看剩余的包体积,主要包含了 vue 的运行时,vue-router 、vue-i18n 和 core.js。这几个模块都是无法分离出来的,整体大小已经有 150kb 了。还有另外一个大的模块,主要包含业务代码、 axios 相关的模块和被转成 base64 的图片。这些模块也是分离不开来的呀。后面要如何优化好?

暴力的 index.html

想起之前看到的一篇文章,Vue 项目骨架屏注入实践 想要学着同样处理一下,但是并没有相应的数据可以用到。

这个时候开始分析手头上的业务,初始记载后,会有一个请求服务器的过程,而服务需要多次轮询之后才有结果(服务端的性能太差了),等获取结果之后才会进入百度地图的页面,这就给了之前分离出百度地图包的契机了,反正百度地图不用立马加载。

那要如何解决中间的白屏问题呢?4G 下也要接近 1s。白屏时间为 webview 初始化,首页资源下载,vue 实例化,后面两者能不能都干掉呢? 仔细盯着首页初始化的过程,从白屏到初始化,到服务端多次轮询拿到结果,这个过程中,前端页面是没有什么大的变化的,只有一个 loading 的图案。webview 初始化后,加载的是打包生成的 index.html,然后再去加载其他 js 资源后,再运行 vue,index.html 只是一个充满连接的 html。于是一个想法就起来了,在 index.html 里面直接渲染出 loading 的界面,等 vue 实例化结束了之后,再隐藏掉,不就可以完美过度了吗?压根就不用等待其他 js 资源下载和 Vue 实例化。

于是很简单的,在根目录下,创建 public/index.html,并简单的用原生代码显示出 loading 界面,再调试一下,就完美了。

堪称完美,只要加载一个 index.html 就够了,怕 2G 下也是秒开吧~~~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant