背景
微信考虑到小程序的体验和性能问题限制主包不能超过2M。哈啰微信小程序也随着业务线在主包中由简到复杂,体积越来越大,前期业务野蛮增长阶段npm库缺乏统一管理,第三方组件库本身工程复杂等问题导致包体积长期处于2M临界卡点,目前存在以下痛点:
阻塞各业务正常微信小程序端需求排期。
迭代需求需要人肉搜索包体积的增长点,推动增长业务线去优化对应的包体积,治标不治本。
缺乏微信端包体积统一管理平台来限制各业务包体积增长。
微信包体积太大导致加载时间长、体验差。
所以主要从包体积优化和长期控制包体积增长两个方面让微信包体积达到平衡状态,长期运行。
包体积优化
微信包体积优化是个老生常谈的话题,只要是公司业务体积达到一定的量级都会不可避免的碰到主包体积超出和体验问题,关于怎么解决官方和网上也给出了比较多的解决方案。知其然知其所以然,那我们就从小程序的原理层面去看解决方案。主要也分为常规的优化方案和结合业务优化技术方案。
常规优化方案
按照微信小程序官网介绍,我们把小程序的性能优化分为启动性能优化和运行时性能优化:
启动性能 :小程序的启动过程以「用户打开小程序」为起点,到小程序「首页渲染完成」为止。小程序「首页渲染完成」的标志是首个页面 Page.onReady 事件触发。
运行时性能:小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。
1. 启动性能优化
在进行启动性能优化之前,先介绍下小程序启动流程,小程序的启动流程主要包括以下几个环节:
1.1 资源准备
a. 小程序相关信息准备:微信客户端需要从微信后台获取小程序的头像、昵称、版本、配置、权限等基本信息,这些信息会在本地缓存,并通过一定的机制进行更新。
b. 环境预加载(受到场景、设备资源和操作系统调度的影响,并不能保证每次启动一定命中)
为了尽可能的降低运行环境准备对启动耗时的影响,微信客户端会根据用户的使用场景和设备资源的使用情况,依照一定策略在小程序启动前对运行环境进行部分地预加载,以降低启动耗时。
c. 代码包准备
从微信后台获取代码包的地址,从CDN下载小程序代码包,并对代码包进行校验。
为了提高下载耗时,微信本身就做了一些优化:
代码包压缩
增量更新
更高效的网络协议:下载代码包优先使用 QUIC 和 HTTP/2
预先建立连接:在下载发生前,提前和 CDN 建立连接,降低下载过程中 DNS 请求和连接建立的耗时。
代码包复用:对每个代码包都会计算 MD5 签名。即使发生了版本更新,如果代码包的 MD5 没有发生变化,则不需要重新进行下载。
1.2 小程序代码注入
小程序启动时需要从代码包内读取小程序的配置和代码,并注入到 JavaScript 引擎中,同时WXSS 和 WXML 会编译成 JavaScript 代码注入到视图层,视图层和逻辑层的小程序代码注入是并行进行的。
微信客户端会使用 V8 引擎的 Code Caching 技术对代码编译结果进行缓存,降低非首次注入时的编译耗时(Code Caching:V8会把编译和解析的结果缓存下来,等到下次遇到相同的文件时,直接使用缓存数据)
1.3 首屏渲染
视图层和逻辑层都是从start并行进行初始化操作,视图层初始化完毕后会发送notify给逻辑层,自身进入等待状态,逻辑层收到信号后会结合自身初始化状态(第一种没初始化完,继续初始化。第二种初始化完进入等待状态)发送初始数据Data到视图层,结合初始数据和视图层得到的页面结构和样式信息,小程序框架会进行小程序首页的渲染,展示小程序首屏,并触发首页的 Page.onReady 事件。
1.4 优化方案
a. 控制包体积:降低代码包大小是最直接的手段,代码包大小直接影响了下载耗时,影响用户启动小程序时的体验。
分包:使用 分包加载 是优化小程序启动耗时效果最明显的手段。
及时清理无用代码和资源。
独立分包。
分包预下载:在使用「分包加载」后,虽然能够显著提升小程序的启动速度,但是当用户在使用小程序过程中跳转到分包内页面时,需要等待分包下载完成后才能进入页面,造成页面切换的延迟,影响小程序的使用体验
分包异步化:「分包异步化」将小程序的分包从页面粒度细化到组件甚至文件粒度。
b. 代码注入优化:
按需引入:在小程序启动时,启动页面依赖的所有代码包(主包、分包、插件包、扩展库等)的所有 JS 代码会全部合并注入,包括其他未访问的页面以及未用到自定义组件,同时所有页面和自定义组件的 JS 代码会被立刻执行。这造成很多没有使用的代码在小程序运行环境中注入执行,影响注入耗时和内存占用。
用时注入:在开启「按需注入」特性的前提下,「用时注入」可以指定一部分自定义组件不在小程序启动时注入,而是在真正渲染的时候才进行注入。
c. 首屏渲染优化:
启用【初始渲染缓存】:启用初始渲染缓存,可以使视图层不需要等待逻辑层初始化完毕,而直接提前将页面初始 data 的渲染结果展示给用户,这可以使得页面对用户可见的时间大大提前。
数据预拉取:预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度 。
周期性更新:周期性更新能够在用户未打开小程序的情况下,也能从服务器提前拉取数据,当用户打开小程序时可以更快地渲染页面,减少用户等待时间,增强在弱网条件下的可用性。
骨架屏:如果首页内容是通过接口异步获取的,用户不一定立即看到完整的界面,需要等待接口返回后调用setData进行页面更新,才能看到真实内容,避免过长时间白屏可以选择骨架屏来提高用户体验。
2. 运行时性能优化
2.1 优化方案:
a. 合理使用setData:小程序的逻辑层和视图层是两个独立的运行环境,通讯通过Native层实现。具体的实现原理和bridge实现一致,ios利用WKWebView提供的messageHandlers,安卓是往webview的window对象注入一个原生方法,所以数据传输的耗时和数据量的大小成正比。
b. 页面切换优化:页面切换的性能影响用户操作的连贯性和流畅度,是小程序运行时性能的一个重要组成部分。
请求前置:小程序不同于H5,在跳转本身就需要消耗比较多的时间,特别是在安卓机上,所以我们可以在页面跳转的同时进行数据并行请求。
c. 控制预加载下个页面的时机(仅安卓):
小程序页面加载完成后,会预加载下一个页面。默认情况下,小程序框架会在当前页面 onReady 触发 200ms 后触发预加载。
在安卓上,小程序渲染层所有页面的 WebView 共享同一个线程。很多情况下,小程序的初始数据只包括了页面的大致框架,并不是完整的内容。页面主体部分需要依靠 setData 进行更新。因此,预加载下一个页面可能会阻塞当前页面的渲染,造成 setData 和用户交互出现延迟,影响用户看到页面完整内容的时机。
我们本次拉齐两轮、数科、普惠,分别进行部分页面分包,下掉0流量页面及其依赖的npm包,把仅有单个业务线引用的npm从主小程序移植到分包下从而不占用主包体积,删除无用文件等操作才从2M体积减少到1.88M,这个收益对于反复优化过的主小程序而言已经算是不错的收益,但是很难满足未来各业务线对小程序主包体积的迭代诉求,所以我们还需要更优的解决方案来减少更多的包体积和限制各业务线在现有体积上进行置换而不是无限扩张。
关于这两个问题我们就在结合业务优化方案和长期控制包体积机制中探讨。
结合业务优化方案
1. 第三方组件库异步分包
微信小程序为考虑体验问题主包被限制到了2M,但随着小程序业务线接入越来越多,npm库缺乏统一管理,第三方组件库本身工程比较复杂等问题导致主包超过1M+都被npm库所占用掉,留给业务的空间不足1M,所以可以从vendor.js中进行部分拆分优化,在不占用主包体积下主包也能够使用这些第三方库。
这样操作的意义在于可以把部分第三方npm库抽离到分包中,主包内只剩核心业务和不能拆的npm库。
实现原理:小程序的分包异步化就是来实现这种功能的,按照微信官方文档提供可以使用require来异步引入包含第三方npm的分包。
但是我们的小程序是使用taro,通过webpack进行编译的,静态产物不支持CommonJS模块的写法,所以require在编译的时候会进行报错,解决方法有两种:
自定义webpack插件,将require关键字替换为customRequireKey(自定义key值,在解析的时候替换成require就可以)。
webpack提供的__non_webpack_require__代替require,不会被webpack解析。
注意点1:如果把第三方npm库改成异步引用后,对于之前通过import同步引用的代码需要进行改造,不然可能会出现在包引入前提前调用包内部方法的问题,对于这个问题可以创建缓存队列解决。
注意点2:分包因为网络波动等原因会加载失败,但是概率极低,可以使用重试机制解决。
2. 封面方案
封面方案相比于第三方组件异步分包方案更好理解,就是把业务全部抽离到分包中,主包中只保留各业务线所依赖的基础库和公共文件,在小程序启动的时候做个启动界面,页面一旦加载就立即跳转到真正承载业务的页面中, 而这个页面被放在分包中。
这么做的好处在于主包中的2M体积只用来放基础库和公共文件,包体积始终控制在1M左右,对小程序性能优化和体验上都有很大的提升。而其他业务都放在业务的主分包中进行管理。
长期控制包体积机制
主包体积优化后如果缺乏标准的控制方法,在未来还是会随着各业务迭代增加不停的增加直到超出2M。所以一套标准的管理机制也是至关重要的。
小程序包体积治理主要从两个方面:
业务线管理机制后台
发布系统管理机制
业务线管理机制后台
业务线size管理机制后台主要集临时资源申请和图标展示于一体,以解决业务线临时size压力。可以通过后台系统进行临时size申请,提出申请后说明申请原因、资源需要时长、size大小,到达审批人时可酌情考虑,审批通过\不通过后都会钉钉通知申请。在管理平台也能看到当前业务线的永久size、临时size、临时size到期时间、申请理由和各业务每迭代包体积大小等信息。
a. 申请临时资源流程:用户根据自己的诉求进入后台选择对应业务线点击新增按钮去申请临时资源、申请临时资源时需在申请弹窗中明确以下几点内容:
申请资源大小:最大申请资源为当前包体积剩余的最大值
使用时间:最多为2个迭代就要把临时资源退回、否则限制发布流程
申请理由:在理由中需要明确填写申请资源后带来的业务价值、由平台的产品侧和研发侧共同衡量价值。
prd地址:链接地址。
b. 申请临时资源最长路径:最多为2个迭代就要把申请的临时资源进行退回、否则在发布时限制发布。
c. 临时申请最大包体积:申请最大资源为当前包体积剩余的最大值
d. 包体积到期通知:提前一个迭代时间钉钉通知对应的申请人和leader包体积到期时间进行优化,申请资源到期后后台系统会自动把申请的资源状态改为已到期,并减少对应申请的资源大小,如果未归还对应体积大小,在发布流程阶段会做体积大小卡口,限制发布。
发布系统管理机制
发布系统管理机制主要流程是developer在AppHelloBikeWXSS项目上每次通过feature分支merge到release分支的时候都会触发gitlab的钩子函数,然后触发jenkins的job进行编译、计算现在各业务线在主包中所占的体积,在通过包体积管理后台申请的体积进行比对,如果超出会钉钉通知到开发者并且在发布系统限制发布,如果没超出正常发布。