这是一篇搭建业务优化的心路历程,也是写给搭建业务的性能优化指南。
前言
直到今天,淘内的页面大多都迁移到了 SSR,从我们终端平台 - 搭建研发团队的视角看,业务大致可以分为两类 —— 搭建派 和 源码派。
这两者互不冲突,更多是基于业务灵活运营和开发维护成本的考量,大致遵循以下原则:
而且已知搭建页面性能较差,源码页面性能更好的情况下,我们又加入了“性能”这个维度,构成了一个性能优化的“不可能三角”,即:
对于采用源码开发的业务,自然不存在什么运营成本,性能优化起来就比较顺手;
对于采用模块搭建的业务,由于搭建体系天然不是性能最优的架构,性能优化就相对没那么自然。
笔者作为【搭建派】solution 的开发者,从一开始就注定要肩负着媲美源码派性能的重任。
下面来说说搭建派是如何打破这个不可能三角的,请收好你的搭建优化指南!
搭建优化指南
要想解决搭建的性能问题,我们就要先了解搭建慢在哪里?
▐ 模块搭建慢在哪里?
搭建体系依赖 feloader(模块加载器) + seed(依赖、版本声明文件),相比源码,不仅输出的主文档体积会大,而且服务端会有拉取文件、seed依赖计算等耗时。
服务端文件加载优化
经过一系列打点日志分析后发现,虽然我们已经尽可能并行处理运行时任务,但随着并行处理的资源数量增加,存在时间片抢占,导致单个任务 resolve 时间过长。
为了验证这个猜想,于是我们从扩展入手,将多个文件合并成一个单文件,果然渲染耗时显著降低约 60%,这也为我们后续其他优化打下了基础。
Seed缓存
服务端文件加载优化减少了运行时处理任务的数量,但是即使是单个任务也存在 seed 计算的耗时, 于是我们开始了针对 seed 部分的改造处理,重新 review 了代码进行了优化,并且在有了服务端文件加载优化的基础下,开启seed 缓存变为了可能。
无论是服务端文件加载优化还是 seed 缓存,这二者都是针对运行时做的优化,但是模块加载还是存在多个并行任务,相比于扩展的低复杂度以及单仓库,将模块打包成单文件不太现实,保持现状似乎就是最终的方案了?
但是毕竟还是存在一部分开销,而且每次主文档返回我们都要输出一大坨 seed 信息来让 hydrate 正常工作,始终达不到源码的效果。模块维度的运行时优化似乎没有什么改造空间了,那能不能干掉以 feloader + seed 的动态加载逻辑?Why Not!我们接着往下看。
搭建构建源码
搭建业务由于存在运行时动态拉取模块、计算依赖等操作,相比于源码项目存在着天然的性能劣势。
有句话说的好:打不过就加入!
好汉不吃眼前亏,外表上看虽然是搭建,但其实我也可以是源码。
于是为了帮助搭建业务更好的完成性能指标,我们推出了 搭建构建源码 这个方案,也就是 AOT (Ahead Of Time) 构建,简单说就是提前收集页面搭建模块然后生成源码项目进行构建。
▐ 模块规范
说起搭建就离不开 feloader + seed 体系,这里就不再过多介绍,ATA 也有很多相关文章介绍,感兴趣可以自行搜素阅读。
社区开发中,主流的模块规范是 commonjs(CJS) 和 esmodule(ESM),也有诸如 AMD、CMD 等规范,feloader 是基于 CMD 规范的模块加载器, web 开发中我们以 ESM 为主,去掉 feloader 可以约等于从 CMD 迁移到 ESM,它们之前的映射关系大致如下:
步骤 | CMD | ESM |
定义模块 | 使用 define() 函数定义模块 | 使用 export 导出模块 |
加载模块 | 使用 require() 函数加载模块 | 使用 import 导入模块 |
模块解析 | 使用模块加载器解析依赖关系并加载模块 | 使用 JavaScript 引擎内置的模块解析机制 |
依赖管理 | 通过定义依赖列表(Seed)来管理模块依赖 | 通过模块引入语句来管理模块依赖 |
模块执行顺序 | 异步加载模块,按照依赖顺序执行 | 同步加载模块,按照调用顺序执行 |
导出模块 | 使用返回值或导出对象来导出模块 | 使用 export 关键字导出模块 |
导入模块 | 使用回调函数参数或全局变量来导入模块 | 使用 import 关键字导入模块 |
浏览器支持情况 | 需要使用 CMD 模块加载器支持 | 部分现代浏览器原生支持 ES 模块 |
搞清楚各个环节做了什么事情之后,我们的适配工作就逐渐清晰。
▐ 模块适配
回归到源码的方案中,我们只需要按照标准的 npm 依赖管理进行适配即可。
搭建平台可以很容易从页面获取到所有模块的 npm 包名 和 版本号,有了这两个信息,在脚手架直接生成到package.json即可,另外模块的版本号根据搭建页面是正式还是测试页进行了区分,当在测试页调试的时候,会通过 semver 取到 beta 版本,确保依赖正确。
以下述依赖为例:
{
"@ali/foo":">4.1.0-beta <4.1.0"
"@ali/bar":">4.1.0-beta <4.1.0"
}
脚手架会将其编译为如下文件,也就是 solution 内部消费的模块的数据结构,在最小改动的前提下保证功能的正常运行:
import * as Mod0Instance from '@ali/foo';
const { default: defaultExport0, ...nameExports0 } = Mod0Instance;
import * as Mod1Instance from '@ali/bar';
const { default: defaultExport1, ...nameExports1 } = Mod1Instance;
export default {
'@ali/foo': {
defaultExport: defaultExport0,
nameExports: nameExports0,
},
'@ali/bar': {
defaultExport: defaultExport1,
nameExports: nameExports1,
},
};
当然适配工作远没有这么简单,其他的就不在此过多展开。
技术层面的适配基本完成后,然后就是产品上的功能的实现,整个流程图如下:
▐ 调试
由于我们将搭建页改造成了“源码页”,模块代理规则不再有效,也就是本地的单模块是无法用于代理调试的即 模块调试转变成了源码项目调试。
但真实的源码项目又在云端生成,所以调试问题就转化成了——如何保存源码项目,并支持开发者下载获取指定版本。
设计
一开始想到借助 OSS,通过在构建器中压缩源代码然后调接口上传,后来想到项目上传不就是 git 托管平台所做的事情吗,所以问题又变成了如何把代码上传到 git 平台。
由于是云构建,机器不固定,好在我们的代码托管平台提供了通过域账号和 privateToken登录的方式,这样只需绑定一个公共账号即可将代码推送上去。
同时我们将时间戳(YYYY-MM-DD-mm)作为对应的分支名,现在开启构建后,开发者通过构建日志即可快速 clone 对应分值仓库,然后通过我们预设好的代理规则即可进行快速调试!
结语
通过 AOT Build + 模版的方式来实时生成源码项目,避免运行时动态获取模块的耗时,有效提升了服务端渲染的性能。当然也存在trade-off,如
每次发布前都要等待构建完成,以确保最终预览的是最新的代码内容
相信经过后期的不断使用,会将整个链路打磨的更加贴合用户体验!
团队介绍
我们是淘天集团—终端平台团队,专注打造业界领先的终端技术以及丝滑流畅的体验,通过持续的创新突破,赋能前台业务、连接数亿用户,提供极致高可靠的服务、极致高性能的架构和极致高效率的产品。
¤ 拓展阅读 ¤
3DXR技术 | 终端技术 | 音视频技术
服务端技术 | 技术质量 | 数据算法