背景
随着业务的发展,小程序的代码量也在飞速膨胀,古茗最大的 B 端小程序页面已经超过 260+,dev 模式下 dist
目录近 35M,性能稍差的设备从 『代码改动 - Taro 热更新 - 小程序IDE build - 页面reload』这个过程超过 13s;而这个过程在日常需求开发时每天可能重复上百次,这会极大的降低开发效率。
构建现状分析
先聊下业务场景,我们的 B 端小程序都跑在钉钉小程序内,小程序的运行时为 Taro
+ React
,debug 工具是支付宝小程序开发者工具(以下简称IDE),先标记一下它方便后面对它开大招。
用过小程序多端框架的小伙伴应该都清楚,通常都是由框架层进行一次构建,将运行时代码编译成对应平台的 DSL
并且输出 dist
包,在由对应平台对 dist
包在进行一次全量构建,最终输出离线包。
如果是 watch 模式 IDE
还会对 dist
文件进行全量的监听,文件每一次的改动都会重新的走一遍这道流程。
目前我们的技术栈,就是先由 Taro
构建,对应下图左侧的日志;然后呢 IDE
再次构建,下图模拟器下面的 编译中 就是 IDE
构建的标志;IDE
构建完成后模拟器就会自动 reload 加载最新的代码:
以上就是一次小程序热更新完整流程,看似挺快没问题对吧,来我们把剂量加大。
Taro 构建没毛病
刚刚只是 hello world,我们上真实的项目(页面做了脱敏),测试设备是一台 M1 Pro,仅改动一行代码,连续跑三次取最快的一次,你们看跑了几秒,整个流程要 10s+ 。还有些同学在用公司配的 windows 设备,还会更慢。动图中的 IDE版本号
先忽略后面会有大用处。
从两张对比图可以直观的看出,热更新的时间主要花费后半段 IDE
的构建上。从数据上看也确实如此,Taro
的构建时间从 0.3s+ 上升到 1.6s+,IDE
的构建速度从 2s 左右居然升到了恐怖的 9s 多,可怕...
Taro 3.5
之前的版本 webpack
还停留在老版本,各种缓存设置也没有优化,热更新还是有一些慢的。碰巧前段时间刚刚完成了 Taro
的升级,从 3.4.0
升级到 3.6.19
,在升级后首次构建还有热更新速度都得到了很大的提升,默认配置下 200+ 页面的小程序热更新时间控制在 2s 内 ,这对我们来说足够!所以 Taro
构建这部分可以直接过。
至于 Taro 3.5+
添加哪些黑科技,可以移步Taro v3.5 正式发布:开发体验提升。听说后续 4.0 版本还支持了 vite
构建(好奇是如何支持原生 esm
的),这里也推荐大家 Taro
的版本至少在3.5+,除了构建速度提升外,阿里系小程序的包体积也会降低非常多,注意是阿里系 ,因为模板支持了 import
语法每个模板都会减少10k,页面越多效果越明显这里不展开说了。
但是请注意升级有风险 ,Breaking change 还是有一些的。也巧了,我们有一份升级踩坑记录,仅供参考Taro 3.6+升级踩坑记录。
IDE 构建成为瓶颈
那 IDE
构建部分为什么会慢这么多呢?
代码量增加
最直观的因素就代码量增多了 ,几乎所有的构建工具的构建速度都和代码量成正相关。前面聊到 IDE
会再次全量构建 Taro
输出的 dist
文件夹:
- demo 项目
dist
体积为 2.8M - 真实项目在 dev 模式下
Taro
输出的dist
近 35M 的
下图是目前真实项目在 dev 模式下的 dist
目录,在经过几轮构建配置优化后停留在 35M 左右:
前后代码体积膨胀了十几倍,所以 IDE
相比 demo 项目构建速度慢是必然的。即使不经过 Taro
的构建直接在 IDE
中修改代码再触发热更新,最后的结果也是一样的慢。
估计有小伙伴会说了"这不公平",你这是 Taro
应用,七转八转全是模版代码,如果纯原生小程序肯定不会这样!!!嗯~,我猜可能会好一些吧我也没验证过(没有这么大原生小程序项目),但是我觉得结论应该也是大差不差。如果有小伙伴能验证原生支付宝小程序构建速度不会随着代码增加而变慢,那我评论区直接送 10 杯古茗好吧😄😄😄
IDE 版本彩蛋
继上篇友好的探讨的文章后,我们再来致敬下支付宝小程序开发者工具(IDE)
重点来了,前方高能!!!能看到的都是有缘人,请大家将支付宝小程序开发者工具(IDE) 降级到3.4.3之前, 我保你构建速度提升30%以上,降级后都说好,Mac Windows真的都管用,没有效果我再送5杯古茗,管用的话记得回来赏个一键三连。
无图无证据,先上图。降级后,相同的代码,相同的改动,IDE
相同的配置 只要 4s ,惊了!!!这个文章我是不是不用再写的,哈哈,直接完结。这也解释了为什么我要在动图上面加 IDE版本号
。
至于为什么我不清楚,我是人肉试出来的,记得那是某天的下午偶然间的一个降级发现了新大陆。有官方大大看到希望解决我的疑惑,你们到底做了啥 "优化" ?
3.4.3 之后官方又发了好多新版本,甚至有比赛专用版本😄(表扬下 changelog 写的真好真详细,比钉钉小程序文档强多了),新版本增加了很多的 feature 修复了大量的缺陷,甭管其他在碉堡的功能,就优化构建速度过快这个一点,反正我是不会升级。
可行方案
那现状如此,来分析下看看有哪些可行的方案。
方案一:小程序一拆多
单一小程序的代码量太大,那就按业务域进行拆分,从源头来降低代码量。比如数据大盘、商品管理这种关联性不高,逻辑又很复杂的业务就完全可以拆成独立的两个小程项目。这样代码量就会被打散,构建速度自然而然就解决了。这个方案目前还有一些阻力,但确实是我们后续b端小程序的迭代的一个大方向。
从开发的角度看这个方案是非常合理的,小程序就是要 "小" , "快" ,"轻";小程序越小,启动体验、内存占用都会更优,而且小程序的架构层也对发布、预览等环节对包体积做了相应的限制。但是从业务的视角看尤其是操作交互已经固化的前提下再去改造,这会大大提升用户的学习成本。
如果进行快速拆分,前提是应用底层的基础设施要非常健壮,基础组件、工具库、uuc 等通用能力不能再每个小程序都冗余一份。除此之外,应用间的通信、业务拆解梳理、版本管理、以及后期的维护成本这都是需要考虑的问题。
方案二:编译成 h5
既然 IDE
热更新慢,那就不能跳出它吗?浏览器不香吗?
对,确实可以,不要忘了古茗所有的小程序都是跑在 Taro
里面的,Taro
多端的特性天然的就可以把代码转换成web应用的哦。这样确实可以跑的通,单纯只是简单的预览是没问题的,但是要真正的应用到开发中还有有些小问题。
API 的差异
浏览器和 IDE
上对于一些 api
的预览模拟差异比较大,并且因为一些兼容问题浏览器能支持的api比较少,如果开发过程中需要调试还是需要切换到 IDE
中。
设计还原
在对设计稿进行还原的时候,浏览器始终和 IDE
是有差距的,不能精准的做到像素级还原。因为本身构建的产物就是不同的,包括一些其他的兼容问题。
流程问题
想象一下,前端在开发的时候以浏览器为准,在提测和最终上线的时候又以小程序容器为准,鬼知道这里面会有多少坑,开发都是好好的,到 IDE
中就各种问题,要知道小程序容器可是各种定制的黑盒。就目前我们只用小程序环境都会出现 IDE
到真机的各种兼容问题,更不要说浏览器到真机小程序了,所以我觉得从流程上和维护的成本上考虑还是要以小程序环境为准。
现在 Taro
转 web 的场景我们也在使用,但还是仅限于文档中的 demo 预览,基础组件的开发预览。
方案三:动态构建
那能不能 "渐进式" 构建呢?比如初始化的时候只构建一部分,访问到哪个页面在动态分析这个页面的依赖然后再进行构建,永远只构建需要的。
IDE
肯定是做不到了,它是接收 Taro
输出的 dist
,输出多少就构建多少,要想实现只能在 Taro
这边想办法。
那就减少 Taro
侧构建的代码,这样不仅能降低 IDE
的构建速度,Taro
的构建速度也会随之降低。
实际在这之前我们就是这样做的,只不过是手动挡😄。在这之前先来简单的了解下 Taro
的构建流程:
app.config.js
就是小程序的配置文件,这里面包括路由信息,Taro
会遍历这份路由配置,并且逐一推到 webpack
的监听队列里面,被监听的文件后每次改动代码 Taro
就会重新构建生成新的 dist
。并且这份配置文件(app.config.js
)也会被推送到 Taro
的构建监听队列中,也就是说动态修改这份配置文件(app.config.js
),Taro
会重新走一遍整个流程。
那么之前的手动挡操作就是手动去这份配置表(app.config.js
)里注释掉一些当前不需要的路由,只保留本次开发需要debug的路由,这样Taro
就只是构建放开的路由,生成的 dist
文件就小很多,IDE
的构建压力也会相对小很多。但是手动除了操作繁琐还会产生很多问题,比如注释错了,或者把注释提交上去了,又或者需要跳转到被注释的路由时就需要重新构建等。
所以这条路肯定是可行的,核心的问题就是如何变成自动挡,动态按需构建。
联想到单页应用(SPA)
我们从小程序的视角跳出来先,如果是一个传统的web单页应用想要提升热更新的速度可以做哪些事情呢?
- 少监听,上各种懒加载...
- 少编译,上各种缓存还有 esm、umd、cdn...
- 其他
- 升级硬件
- 多线程
- 使用 native 语言?swc、esbuild...
在回到小程序 IDE
的场景上,如果不考虑构建相关的优化(构建相关已经被 IDE
收敛了),也只能从 少监听 这一点入手。
最终实现
来看下最终方案自动挡的整个流程:
核心的关键步骤以下几点:
劫持 app.config.js
在 Taro
构建是生成临时的配置文件 temp.app.config.js
,仅保留最基本的路由信息(tabbar、登录和落地页),其他配置保持与 app.config.js
一致。
并且替换 Taro
的 app.config.js
的引用指向换成 temp.app.config.js
,这样就能保证项目初始化构建为最小体积。
启动本地服务管理临时配置
在 Taro
完成后启动本地 node
服务,专门用于对临时配置(temp.app.config.js
)进行 CRUD
。
并对外暴露一个http的接口 http://localhost:xxxx/update?target=/pages/xxx/a
,这个接口会做如下几件事情
- 接口接收一个
path
参数 - 用
path
去原配置(app.config.js
)查找到对应的完整路由信息,因为路由可能为分包路由 - 将匹配到的结果更新到临时配置文件(
temp.app.config.js
)
校验细节
因为可能会存在分包的场景,我们开发的的场景大部分会在一个分包内完成,所以在路由比对命中后会直接构建一个完整的分包。
比如一个分包中 有 /package/a
,/package/b
,/package/c
三个页面,当访问 /package/a
时,会直接将这个三个页面一次性构建,不然 package/a
跳转 package/b
再跳转 package/c
就会构建三次。当然这也是一个默认配置,可以关闭。
拦截api
- 拦截跳转api
Taro.navigateTo
:我们把触发动态构建的时机标定在跳转时,在跳转时会校验当前的路由是否已经被构建过,检验的逻辑就是用当前跳转路由去临时配置(temp.app.config.js
)查找是否存在,不存在时发起http://localhost:xxxx/update?target=/pages/xxx/a
请求;如果存在的话直接走Taro.navigateTo
原逻辑就好。 - 拦截
Taro DidMount
:当http://localhost:xxxx/update?target=/pages/xxx/a
更新完临时配置文件(temp.app.config.js
)后,Taro
会监听到临时配置文件(temp.app.config.js
)的改动,并触发重新构建。小程序reload后进入到DidMount
,DidMount
在重定向到目标跳转页面
统一封装
因为零零散散的逻辑很多,所以基于 Taro
的插件机制 将上述的所有逻辑包装成了 Taro Plugin
。开启同其他的 Taro Plugin
是一样的方式,只要在编译配置中添加此插件就 ok 了
js
/**
* 伪代码,Taro的构建配置文件 https://docs.taro.zone/docs/config-detail
* config/index.js
*/
module.exports = {
...
plugins: ['taro-plugin-xxx'],
...
};
基于此后续也会在这个插件中添加其他的能力,比如收敛其他插件、收敛 patch-package、内置通用页面等。
可视化拓展
方案内测的时候发现了一个问题,在开发时一旦需要 debug 的页面会跨多个分包时,比如一连跳转 3 个分包那么就会重新构建三次,所以有必要配合这套方案开发一个可视化工具来精细化管理路由,直接勾勾勾就能完成下一次的构建。
所以 vscode
插件来了,可以精细化控制单个路由,上述的场景就可以通过搜索和勾选提前将三个分包勾选好,然后提交,这样一次构建就好了。并且会和临时配置文件(temp.app.config.js
)数据共享。
禁用的选项为默认最小路由集合。
问题
还是留了一些坑的:
- 因为是本地
node
服务,在真机预览时访问没有被构建的路由不能触发动态构建,需要提前构建后真机才能正常访问 - 相对黑盒,劫持
app.config.js
引用以及复写Taro
相关的api都需要修改源码才能得以实现
总结
这套方案目前团队内页面过百的小程序都已经接入了,在配合 IDE
降级,这一套 "组合拳" 下来热更新速度大约提升 3 倍,我自己日常开发整个热更新的流程基本控制在 3s 左右。
为了实现这套方案前期大量的 Taro
的源码,发现了很多有意思的黑科技,也算是意料之外的收获,也参与了共建,很庆幸能参与到这样的开源框架,Taro
这么多年还在一直坚持迭代维护也是真的很不容易,致敬!
最后
📚 小茗文章推荐:
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~