我们从Vue到Alpine.js的旅程
在 2019 年底,我们为一位客户重发布了电子商务网站。这次重新发布的变动很大,不仅影响了整体设计和模板,还涉及了前端的架构。唯一没什么改动的就是后端。
客户的主要需求是:
优化 PageSpeed 指标
提高可用性,从而提高转化率
在数月的实施之后,客户和我们都对最终成果感到满意。我们在 Lighthouse 的全部四个类别中都达到了绿色评级,转化率也有了显著的提升。直到谷歌在 Lighthouse 6.0 更新中更改了性能评分的计算模式,让我们的评分从绿色降级为红色。
顺带一提,Lighthouse 在新标准中将重点转移到了前端内容上,在首字节时间(TTFB)以及如文件大小、CSS 优化、网页字体等会对总体网页性能有影响的内容之外,还囊括了“可交互时间(TTI)”以及“最大内容绘制(LCP)”指标。随着网页可交互性越来越强,其对性能的感知也越发重要。理论上来说,我们是支持谷歌将这些新指标纳入评分标准的,尽管谷歌在展示“优秀范例”时用的是几乎没有任何交互性的博客站点,这完全是在拿苹果和橘子在作比较。
在与客户的一次会议后,我们延后了针对 Lighthouse 新指标的优化工作。在分析了网站访客的常用设备后,我们很难再说服自己将大量时间花费在我们和所有竞争对手都要面临的问题上。
然而,随着在 2020 年底、2021 年初谷歌公布部分新指标将对搜索结果排名有影响后(是时候将页面体验引入谷歌搜索了),显然我们并不能再继续将这个问题推延了。在与客户的又一次商讨后,我们确定了我们所能提供显著竞争优势,并让最终用户感受到速度的提升。
我们需要更多的数据。坦白来说,在这之前我们从来没怎么重视过更深层次的性能指标,而现在我们要开始赶进度了。我们通过谷歌 Chrome 浏览器和其内置的 Lighthouse 应用,外加开发者工具中的性能标签,三管齐下分析网站性能。
在重发布后,我们将前端架构完全推翻重写,用 Vue 2 作为 JavaScript 框架,TailwindCSS 为 CSS 框架。所有内容都由 Symfony Encore(Webpack)进行打包。
我们的站点没有用 SPA,而是将根实例捆绑到一个 div 元素 #app 上。借助无渲染组件(Vue.js 中的无渲染组件)让我们可以使用服务器端变量或是用 Twig 轻松编写大部分模板,而不需要编写任何 API。
<notepad-star
:product-id="{{ product_id }}"
:initial-star="{{ is_stared(product_id) ? 'true' : 'false' }}"
>
<div>
<button @click.prevent="toggle">Toggle</button>
</div>
</notepad-star>
product_id是服务器端变量,is_stared(product_id)是Twig函数,二者都是作为props传入Vue组件的。
目前我们的流程大致是这样子的:
在 chrome 里生成性能报告
研究报告结果
改点东西
重新生成新报告以确定或者推翻我们假设
性能报告中最有用的部分是“评估脚本”,似乎浏览器在评估我们 JavaScript 包的时候要做不少事。
生产环境
我们的第一步是注释掉脚本标签,看看对指标会有什么影响。结果发现,效果相当显著。
注:这份报告是我们在开发环境中生成的,与实际生产环境大约有 10%-15% 的差异。
我们确定了以下几点亟需关注:
优化关键资源的预加载
最大限度地缩减阻断时间
优化交互时间
最大限度地减少主线程工作
为追求简单快速简单的部署,我们没有对谷歌标签管理和我们的 CCM 进行完善的性能测试,这也导致了一些渲染阻塞。我们测试了预加载和预连接的各种不同组合,并最终得出了以下结果:
预加载关键资源,如 CCM 脚本
预连接 GTM
预加载我们自己的关键资源,如网页字体或我们自己的主要 css、js 文件
这些是我们用到的工具:
Lighthouse:直观展示哪些资源应该被预加载
Firefox:通过开发者工具可以找到被预加载,但在最初几秒内未被使用的字体
在优化预加载后,剩下可能对我们关键指标有影响的就只有我们 JavaScript 包中自己的资源了,其余指标也都或多或少跟这些资源挂钩。
在开始优化之前,我们需要先从更深层次分析问题。如前文所述,我们对所有的 Vue 组件都应用了无渲染组件,并用 Vue 实例打包了整个网站。这种方式让我们可以很方便地进行全局状态管理,我们还可以通过添加额外的混合器来为网站增加交互性,比如:
export const searchOverlay = {
data() {
return {
showSearchOverlay: false,
}
},
}
全局状态示例 / 混合器提供功能性
Vue 有两个不同的版本:运行时构建,以及包含模板编译器的版本。运行时构建的文件大小相比来说要小很多,但只能用于单一文件的组件,因为这类组件会被包含在捆绑包中,因此不需要模板编译器。另一方面,模板编译器让我们可以从模板引擎(Twig)中生成模板,并插入到无渲染组件的默认槽中。
另外,由于我们需要将网站整体打包,Vue 需要对所有可见的 DOM 节点进行评估,而光是在主页上就有大约 4500 个节点。这也是为什么我们的脚本评估时间会是如此的长。
既然对根因有了更好的理解,我们可以开始着手评估问题缓解的方法了。
很可惜我们最终并没有找到能显著提升当前架构性能的方法,我们的模板架构和后端结构并不允许我们优雅地切换到运行时构建。
下一步,我们开始整理当前网页上所提供的组件和交互功能,以从我们全新的解决方案中获得新的视角。
这些是我们目前已有组件的例子:
实时搜索
动态侧边栏导航
弹出框菜单
模态框
我们还有一些之前由混合器提供的小型函数。这些函数因为没有状态且可以简单直接地在任何地方触发,主要用于不需要单独组件即可实现的功能,如:
动态更新产品类别
打开发货模式
展示或隐藏全局信息轮播图
这些功能都有一个共同点:需要组件间的交流。这些组件都不算复杂,主要用于提供互动性或防止网页重新加载。
我们希望且需要从新框架中获得的有:
反应性,在数据发生变化后模板会重新渲染
事件系统以方便组件间交流
占用空间小
我们曾在其他项目中用 Alpine.js 来提供交互性,最终效果也很好。既然我们已经在项目中使用 TailwindCSS 了,Alpine.js 所声称的“类似 JavaScript 中的 TailwindCSS”说法很得我们心。我们并不确定 Alpine.js 是否能胜任如此大型的电子商务站点,因此我们需要建立一个概念验证,以测试它是否最难处理的部分。我们重新构建了如滑动导航、动态购物车以及主菜单等包含前文所提到需求的重要组件,如果我们能重新整合这些组件,那我们可以肯定地认为其他组件都没问题。在经过了大约一天左右的工作,我们收获了满意的成果。虽然重构过程并不是一帆风顺,但既然我们的大部分逻辑都是用 JavaScript 写的,从 Vue 到 Alpine.js 的转换都是很直接的。我们最终确定了以下的架构形式:
js/
├── components/
│ ├── cart.js
│ ├── mobileMenu.js
│ └── ...
├── enums/
│ ├── events.js
│ └── ...
├── helper/
│ └── customEvent.js
├── providers/
│ ├── cart.js
│ ├── googleTagManager.js
│ └── ...
└── stores/
├── cart.js
├── global.js
└── ...
组件
组件是以窗口范围的函数所定义的,可以返回用于在 Alpines 的 x-data 属性中用于初始化组件的对象。
下面是一个简化的模态组件示例,请注意我们是怎么使用 customEvent 函数和“枚举(enums )”的。
import customEvent from '@/helper/customEvent'
import { MODAL_OPEN, MODAL_OPENED, MODAL_CLOSE } from '@/enums/events'
window.modal = () => ({
open: false,
init() {
if (this.instantDisplay !== undefined) {
this.open = true
}
},
close() {
this.open = false
customEvent(MODAL_CLOSE, this.name)
},
wrapper: {
async [`@${MODAL_OPEN}.window`](e) {
if (modalToOpen !== e.payload.name) {
return
}
customEvent(MODAL_OPENED, this.name)
this.open = true
},
},
})
enum
并不是指实际意义上的枚举,只是个用来保存常量的辅助文件,方便我们在整体代码库中使用这些常量,而不用担心事件在重命名时会连锁搞崩掉别的东西。
const MODAL_OPEN = 'modal-open'
const MODAL_OPENED = 'modal-opened'
const MODAL_CLOSE = 'modal-close'
helper
我们可以在任何地方导入 helper 函数且不会保留任何状态。这个是我们的 customEvent helper 函数:
export default function (name, payload = null, originalEvent = null) {
// 入参对象应包含以下:
// name: 'string',
// payload: 'object'
// originalEvent: 'this',或者其他你需要点击的目标
const customEvent = new CustomEvent(name, {
detail: {
payload: payload,
originalEvent: originalEvent,
},
})
window.dispatchEvent(customEvent)
}
这个简单的 helper 给我们带来极大的灵活性,让我们摆脱了定义无数个 Alpine 组件的烦恼,在包括 HTML 中等任何地方直接调用。其本质也不过是标准 CustomEvent API 的一部分,改造成可在窗口范围内使用的函数且能接收 onclick 属性入参。
<button type="button" onclick="customEvent('name', 'payload')"></button>
内容提供器
内容提供器通过可复用功能提供数据,可以把它看作是客户端的 API 层。和 helper 函数一样,这些函数不应包含任何状态,且可被组件消耗的。
下面是实时搜索的内容提供器大致代码:
import customEvent from '@/helper/customEvent'
import { SEARCH_GET } from '@/enums/events'
async function getResultFor(searchTerm) {
let result = undefined
await fetch(`/search?q=${searchTerm}`)
.then((response) => response.json())
.then((data) => {
result = data
})
customEvent(SEARCH_GET, result)
return result
}
export { getResultFor }
store
既然我们 JavaScript 框架选择依赖 Alpine.js 2.8,那么选择 Spruce 做全局状态管理也很合理。网站的每个部分都有一个 store,以下几行代码是我们用于管理大型菜单状态的:
Spruce.store('megamenu', {
activeId: null,
toggle(id) {
if (id === this.activeId) {
this.activeId = null
return
}
this.activeId = id
},
})
在确定架构并顺利实施最复杂的组件后,我们很自信我们前进的道路一定是正确的。性能标准测试结果也很好,大部分的性能分类都有了 15-20 百分点的提升。
我们迫不及待地想实现所有组件以获得完整的指标结果,每次点击 Lighthouse 标签中“生成报告”按钮,都会让我们的心跳加速。如果不包含脚本的话,预计我们的网站是不可能达到 56 的评分,但这是我们现在的结果:
再次声明,这只是我们的开发环境,因此很多图中的“机会”并不适用于实际生产环境。
这次的结果让我们颇为满意,在最后的几项测试,并对代码进行清理后,我们开始准备下周一的版本发布。
上午 8 点 24 分,我们点下了“合并”按钮。在这之前我们进行了发布前的最后一次 Lighthouse 测试,性能评分当时下降到了 28,具体是什么原因造成的这次 10 分左右的下降我们并不清楚。部署工作顺利进行,网站运行正常,于是我们又进行了一次 Lighthouse 测试。这是测试结果:
在上线之后我们发现了一些小问题,在及时修复后我们成功将评分打上 62 分,真是太刺激了!当然,这并不会是我们旅途的终点,我们仅仅是为后续进一步改善用户界面体验打下了良好的基础。
在这次重新部署中,我们需要一个能对指标进行监控的工具。在研究通过 CI/CD 管道、手动测试或是通过 Lighthouse 节点 CLI 运行脚本时,我们偶然发现了 Debugbear。
Debugbear 的服务可以检测网站的核心状态、运行 Lighthouse 测试并将测试结果与竞争对手或历史结果进行对比,从而提供对两次测试结果的深层次解读。它不仅帮我们更好地了解问题根因,还提升了我们对优化工作的信心。
可以说,Debugbear 的性价比非常高,再加上 Matt 人真的很好,当时我们的信用卡除了问题,他非常慷慨地延长了我们的试用期,让我们安心测试而不用担心最后期限。
以上基本就是我们旅程的第一阶段了。
最终的成果让我们对自己的决策充满了信心。Vue 并不适合我们的项目,老实说,当初选择 Vue 或许是因为它看起来不错,但它从来不是我们最好的选择。错处不在 Vue,Vue 是个很强的框架,我们也还在继续使用它,但现在我们有了另一个比 Vue 更合适的工具。
希望这篇文章能帮上你!如果有任何问题,欢迎在推特(https://twitter.com/timkley)上联系我😊。
原文链接:
https://www.tim-kleyersburg.de/articles/from-vue-to-alpinejs by Tim Kleyersburg
声明:本文为InfoQ翻译,未经许可禁止转载。
微信扫码关注该文公众号作者