微前端架构中的资产加载与性能优化
1. 资产引用策略
1.1 内联(Inlining)
确保同步的最简单方法是将标签嵌入到片段本身的标记中。例如,结账团队(Team Checkout)在服务器端生成购买按钮标记时,可将链接和样式标签直接包含在响应按钮标记的请求中,示例如下:
<link href="/checkout/static/fragment.a98749.css" rel="stylesheet" />
<button>buy now</button>
<script src="/checkout/static/fragment.a62c71.js" async></script>
内联虽然可行,但存在一些问题:
- 冗余的链接/脚本标签:如果页面中多次包含购买按钮,会出现多个相同的链接和脚本标签。不过,如果资源可缓存,浏览器会智能地只下载一次文件。
- 更多的 JavaScript 执行:即使浏览器只下载一次 JavaScript 文件,但每个脚本标签都会执行一次,可能会引入意外问题并增加 CPU 负载。
- 仅适用于服务器端集成:由于样式和脚本引用是服务器生成标记的一部分,此解决方案不适用于客户端或通用渲染的微前端。
1.2 集成解决方案(Tailor、Podium 等)
1.2.1 Tailor 的资产处理
Zalando 的 Tailor 通过 HTTP 头传输资产引用。团队可以通过 Link 条目为服务器生成的标记指定关联的资产。响应示例如下:
$ curl -v http://.../checkout/fragment/buy-button
HTTP/1.1 200 OK
Link: </checkout/static/fragment.a98749.css>; rel="stylesheet",
</checkout/static/fragment.a62c71.js>; rel="fragment-script"
Content-Type: text/html
Connection: keep-alive
<button>buy now</button>
由于引用和标记在同一请求中,不存在同步问题。Tailor 服务会组装页面并跟踪所有引用,在最终标记中,为所有唯一的 CSS 文件创建链接标签,并通过 require.js 模块加载器加载 JavaScript。
1.2.2 Podium 的资产处理
使用 Podium 时,团队在 manifest.json 文件中定义其资产引用,该文件还包含版本号。结账团队的购买按钮清单示例如下:
$ curl http://.../checkout/fragment/buy-button/manifest.json
{
"name": "buy-button",
"version": "4",
"content": "/checkout/fragment/buy-button",
"css": [
{ "value": "/checkout/static/fragment.a98749.css" }
],
"js": [
{ "value": "/checkout/static/fragment.a62c71.js" }
]
}
决策团队(Team Decide)使用 Podium 的布局库,并为其提供产品页面所需的所有微前端的 manifest.json URL。启动时,Podium 会下载所有清单文件以确定内容的端点,这些端点返回纯 HTML:
$ curl -v http://.../checkout/fragment/buy-button/
HTTP/1.1 200 OK
Content-Type: text/html
Connection: keep-alive
podlet-version: 4
<button>buy now</button>
响应还包含一个 podlet-version 头,它表示软件的部署版本,通常是构建号或提交哈希。每次 Podium 获取 HTML 内容时,会将 podlet-version 头与缓存的 manifest.json 文件中的版本号进行比较。如果版本匹配,可使用当前清单中指定的资产文件;版本号不同则表示片段所有者部署了新的软件版本,Podium 会重新下载 manifest.json 以获取更新资产的链接。
1.3 快速总结
以下是不同资产加载方法的特点总结:
| 方法 | 独立部署 | 缓存和性能 | 保证同步 |
| ---- | ---- | ---- | ---- |
| 直接 | 否 | 差 | 否 |
| 重定向(客户端) | 是 | 一般 | 否 |
| 包含(服务器) | 是 | 好 | 否 |
| 内联 | 是 | 差 | 是 |
| 集成(Tailor、Podium 等) | 是 | 好 | 是 |
不同团队之间加载所需资产的技术合同如下:
| 方法 | 团队间合同 | 示例 |
| ---- | ---- | ---- |
| 直接 | 资产文件 URL | /checkout/fragment.js
/checkout/fragment.css |
| 重定向(客户端) | 资产文件 URL | /checkout/fragment.js
/checkout/fragment.css |
| 包含(服务器) | 带有注册标记的端点 | /checkout/register_scripts
/checkout/register_styles |
| 内联 | 无(仅适用于服务器标记) | |
| Tailor | HTTP 头 | Link:
;
|
| Podium | manifest.json | /checkout/manifest.json |
2. 捆绑粒度
2.1 HTTP/2 的影响
过去,为了减少网络请求数量,人们会尽量加载最少的资源,将所有内容捆绑到一个文件中,并将多个图像合并为一个(精灵图)。后来,像 Google PageSpeed 这样的工具鼓励将视口的 CSS 内联到 HTML 中,以便在几个 TCP 数据包中完成首次渲染所需的所有内容。
随着 HTTP/2 的引入,这些最佳实践不再适用。该协议降低了从同一域加载多个资源的开销,其内置的多路复用和服务器推送功能消除了手动将资产内联到页面的需求,降低了应用程序的复杂性,也有利于缓存。这些特性在构建微前端风格的应用程序时非常有用。
2.2 一体化捆绑(All-in-one bundle)
曾经有人讨论构建一个全面的资产捆绑过程,将所有团队的脚本和样式收集到一个文件中进行交付。但这种中央资产捆绑器会引入大量的耦合和摩擦,需要有人构建和维护该服务,并且资产服务和应用程序的部署需要同步,以确保标记与交付的资产匹配。如今,对于大多数用例来说,交付一体化捆绑包是一种反模式:
- 传输大量未使用代码的成本超过了减少请求的收益。
- 缓存失效的可能性很高,即使只有一小部分发生变化,也需要重新下载整个捆绑包。
不过,中央捆绑器也有一个有价值的功能,即消除冗余代码。当两个团队使用相同的 JavaScript 库或按钮样式时,中央服务可以删除其中一个实例,使捆绑包更小。
2.3 团队捆绑(Team bundles)
在示例应用程序中,每个团队都有一个页面和片段捆绑包。对于产品页面,决策团队加载他们的页面捆绑包。如果要包含结账团队的购买按钮微前端,则还需要添加结账团队的片段捆绑包。
由于 HTTP/2 使额外请求的成本变得很低但并非免费,因此团队内部仍应使用捆绑,而不是让浏览器处理原始组件和依赖树。在实际项目中,按团队捆绑的方法在捆绑大小、过度获取和跨页面可重用性之间取得了合理的平衡。但具体情况还需根据用例来决定,如果一个团队提供的片段需要大量 CSS 代码,而只有一个小众页面使用它,那么为其创建一个单独的捆绑包可能更有意义。
2.4 页面和片段捆绑(Page and fragment bundles)
每个微前端一个捆绑包的方法将粒度进一步细化,每个片段或页面都有自己的脚本和样式捆绑包。可以将其看作在使用实际组件之前,在文件顶部添加导入语句。
这种更细粒度的捆绑方式确保只下载页面上客户需要的代码,但根据页面结构和包含的片段数量,可能需要加载大量资产。
不同资产捆绑粒度的示意图如下:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(All-in-one bundle):::process --> B(Asset bundler):::process
C(Team bundles):::process --> D(Team Inspire):::process
C --> E(Team Decide):::process
F(Page and fragment bundles):::process --> G(Page):::process
F --> H(Fragment):::process
3. 按需加载
3.1 代理微前端
团队可以采用代码分割等技术来进一步改善加载行为,减少初始下载大小,并在用户需要时获取部分代码。例如,结账团队的购买按钮打开一个需要大量 JavaScript 的花哨层时,团队可以将该层代码从初始片段捆绑包中取出,在用户悬停在按钮上时进行获取。
如果资产文件包含五个不同微前端的代码,而这些微前端很少在一个页面上一起使用,可以设置代理组件,在首次需要时获取实际代码。使用自定义元素的示例代码如下:
class CheckoutBuyProxy extends HTMLElement {
constructor() {
import("./real-buy-button.js").then(...);
}
}
window.customElements.define("checkout-buy", CheckoutBuyProxy);
需要注意的是,代理自定义元素比这个示例更复杂,目前仅通过注册新类无法更新自定义元素定义,并且生命周期方法是同步的。
3.2 懒加载 CSS
如果使用纯 CSS,懒加载并不容易,因为浏览器没有原生支持动态分割和加载 CSS 文件。但许多 CSS-in-JS 解决方案、CSS 模块和大多数打包工具都提供了无需大量手动工作即可实现 CSS 懒加载的机制。这些都是在单体前端中也会使用的标准性能优化技术,在微前端架构中,每个团队都可以为自己的系统部分采用这些技术。
4. 性能优化的重要性
4.1 对微前端性能的初始担忧
早期接触微前端概念时,很多人会担心多个团队各自开发前端会带来大量开销,导致效率低下和速度缓慢。但在实际的微前端项目中,这些担忧往往会逐渐消失。虽然自主性必然会带来接受冗余的成本,但我们应该关注对用户有实际影响的瓶颈,而不是一味地反对代码重复。
4.2 微前端项目的性能优势
实际构建的微前端项目往往比它们所取代的单体应用表现更好,能够实现更快的响应、减少发送到浏览器的代码量,并改善整体加载时间。这些项目的一个共同点是,从一开始就将性能优化作为首要任务,而不是事后才考虑。
4.3 性能优化的策略
4.3.1 定义性能准则
在项目早期,团队可以通过参考竞争对手的网站,为不同页面定义性能准则。例如,决定页面的总重量不应超过 1MB 数据,在良好条件下页面视口必须在一秒内渲染,在 3G 网络条件下必须在三秒内渲染。
4.3.2 不同部分的不同性能要求
不同部分的网站有不同的性能要求:
- 用户首次打开主页时,主要关心的是无需等待即可看到内容。
- 在产品页面上,主图像(英雄图像)最为重要,应该是首批加载的项目之一。
5. 总结
5.1 资产加载与管理
- 团队必须能够在不与其他团队协调的情况下更新其资产。
- 资产路径必须是团队之间合同的一部分,使用微前端的团队需要添加关联的资产。
- 有多种方式来传达资产 URL,如通过文档、重定向或注册包含、HTTP 头或机器可读的清单文件。
- 如果在服务器上渲染,需要确保 JavaScript 和 CSS 文件与生成的标记版本匹配;对于纯客户端渲染,这个问题相对较小。
- 开发团队必须在其应用程序中实施性能优化技术,如按需加载,尽量避免采用像共享资产捆绑服务这样的全面优化,因为它们会引入额外的耦合和复杂性。
5.2 性能优化要点
- 从项目一开始就将性能优化作为首要任务,根据不同部分的特点定义合理的性能准则。
- 关注对用户有实际影响的瓶颈,而不是过度关注代码重复问题。
- 利用 HTTP/2 等技术的优势,选择合适的资产捆绑粒度和加载策略,以实现更好的性能和用户体验。
总之,在微前端架构中,通过合理的资产加载和性能优化策略,可以在保证开发自主性的同时,实现高效、快速的前端应用。
6. 性能测量与问题定位
6.1 测量性能的挑战
当页面包含来自不同团队的多个微前端时,测量性能并根据结果采取行动变得十分棘手。因为不同团队的代码可能相互影响,很难准确判断每个团队的代码对整体性能的贡献。
6.2 定位性能问题的策略
- 使用性能监测工具 :借助专业的性能监测工具,如 Google Chrome 的开发者工具、Lighthouse 等,这些工具可以提供详细的性能指标,如加载时间、渲染时间、资源使用情况等。
- 分阶段测试 :将页面的不同部分分开进行测试,逐步排查问题。例如,先单独测试每个微前端的性能,再测试它们组合在一起的性能。
- 日志记录与分析 :在代码中添加日志记录,记录关键操作的时间点和执行情况。通过分析日志,可以找出性能瓶颈所在。
7. 减少 JavaScript 开销
7.1 共享大型供应商库
在微前端架构中,不同团队可能会使用相同的大型供应商库,如 React、Vue 等。为了减少 JavaScript 开销,可以在团队之间共享这些库,避免重复下载。
7.2 实现库共享的方法
7.2.1 CDN 引用
使用内容分发网络(CDN)来引用大型供应商库。CDN 可以提供高速的文件下载服务,并且通常会对文件进行缓存,减少重复下载的可能性。示例代码如下:
<script src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
7.2.2 模块联邦
使用 Webpack 的模块联邦功能,允许不同的微前端之间共享模块。通过配置 Webpack,可以将共享模块暴露给其他微前端使用。示例配置如下:
// 微前端 A 的 webpack 配置
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// 其他配置...
plugins: [
new ModuleFederationPlugin({
name: 'microfrontendA',
exposes: {
'./SharedComponent': './src/SharedComponent',
},
shared: {
react: { singleton: true, eager: true, requiredVersion: '^17.0.2' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^17.0.2' },
},
}),
],
};
// 微前端 B 的 webpack 配置
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// 其他配置...
plugins: [
new ModuleFederationPlugin({
name: 'microfrontendB',
remotes: {
microfrontendA: 'microfrontendA@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true, requiredVersion: '^17.0.2' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^17.0.2' },
},
}),
],
};
7.3 保持团队独立性
在实现库共享的同时,要确保团队的独立性。可以通过定义清晰的接口和合同,让团队在不影响其他团队的情况下使用共享库。例如,规定共享库的版本范围、使用方式等。
8. 不同性能优化策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 一体化捆绑 | 减少请求数量 | 传输大量未使用代码,缓存失效风险高 | 对请求数量敏感,代码复用率高的场景 |
| 团队捆绑 | 平衡捆绑大小和可重用性 | 可能存在一定的过度获取 | 大多数微前端项目 |
| 页面和片段捆绑 | 只下载需要的代码 | 可能需要加载大量资产 | 页面结构复杂,片段使用灵活的场景 |
| 按需加载 | 减少初始下载大小 | 实现复杂度较高 | 包含大量不常用代码的场景 |
| 共享大型供应商库 | 减少 JavaScript 开销 | 需要额外的配置和管理 | 多个团队使用相同大型库的场景 |
9. 性能优化流程
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(定义性能准则):::process --> B(选择资产加载与捆绑策略):::process
B --> C(实施性能优化技术):::process
C --> D(测量性能):::process
D --> E{是否满足准则?}:::process
E -- 是 --> F(上线):::process
E -- 否 --> G(定位性能问题):::process
G --> H(调整策略与技术):::process
H --> C
10. 总结与建议
10.1 核心要点回顾
- 团队在资产加载和管理方面要保证自主性,同时遵循明确的合同和规范。
- 选择合适的资产捆绑粒度和加载策略,根据不同的用例和性能要求进行权衡。
- 实施性能优化技术,如按需加载、共享大型供应商库等,减少不必要的开销。
- 从项目一开始就重视性能优化,通过合理的测量和定位方法解决性能问题。
10.2 建议
- 在项目启动前,组织团队共同讨论并定义性能准则,确保所有团队对性能目标有清晰的认识。
- 定期进行性能测试和评估,及时发现并解决潜在的性能问题。
- 鼓励团队之间的沟通和协作,共同探索和采用新的性能优化技术。
通过以上的资产加载、捆绑和性能优化策略,可以在微前端架构中实现高效、快速的前端应用,为用户提供更好的体验。
超级会员免费看
1534

被折叠的 条评论
为什么被折叠?



