技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTETC

我们从 Vue 到 Alpine.js 的旅程

所属馆藏
Other
文件格式
Markdown
原始路径
Other/19-我们从 Vue 到 Alpine js 的旅程
本文目录12 个章节
  1. 一、问题
  2. 二、分析过程
  3. 三、我们当时的设置
  4. 四、生产环境与问题定位
  5. 五、我们需要优化什么
  6. 六、Vue 版本与运行时构建
  7. 七、为什么考虑 Alpine.js
  8. 八、从 Alpine.js 2 到 Alpine.js 3
  9. 九、性能结果应该怎么读
  10. 十、迁移前的检查清单
  11. 十一、写在最后
  12. 原文及参考资料

我们从 Vue 到 Alpine.js 的旅程

Category(分类): Other Status: 已整理

作者:Tim Kleyersburg

本文保留原文关于一个电商站点从 Vue 迁移到 Alpine.js、分析 JavaScript 主线程开销和使用渐进式增强的实践。原文项目发生在 2019—2021 年,文中的 Vue 2、Alpine.js 2.8、Spruce、TTI 和 Lighthouse 分数都属于当时的技术背景,不能直接当作今天的默认方案。

原文配图已下载到本文同目录的 images/ 文件夹。配图中的 Lighthouse 报告只代表当时的实验环境,不代表当前网站或当前工具的结果。

一、问题

在 2019 年底,我们为一位客户重新发布了一个电子商务网站。这次变动很大,不仅影响整体设计和模板,也涉及前端架构;后端基本没有改变。

客户的主要需求是:

  • 优化 PageSpeed 指标;
  • 提高可用性,从而提高转化率。

经过数月实施,客户和我们都对结果满意。我们在当时的 Lighthouse 四个类别中都达到了绿色评级,转化率也有明显提升。后来 Lighthouse 更新了性能评分计算方式,原来的分数从绿色降到了红色。

这里首先要补充一个今天仍然适用的判断:实验室评分变化不等于真实用户体验突然变差。Lighthouse 的版本、模拟设备、网络、缓存状态、CPU 限制、第三方脚本和运行时机都会影响一次报告。发布决策还应该结合 CrUX、RUM、转化率和错误率,而不是只追逐一个分数。

当时 Google 将 TTFB、资源体积、CSS、网页字体、TTI 和 LCP 等因素纳入性能评估。今天的指标体系已经发生变化:TTI 从未是 Core Web Vital,它是旧版 Lighthouse 的实验室指标,如今也不再是主要推荐指标;INP 已于 2024 年取代 FID 成为交互指标。LCP、INP、CLS 应结合真实用户数据观察。

二、分析过程

我们需要更多数据。此前我们没有认真关注更深层的性能指标,于是开始使用:

  1. Chrome DevTools 的 Lighthouse 面板;
  2. 命令行或 CI 中运行的 Lighthouse;
  3. DevTools 的 Performance 面板,用来观察主线程、长任务、布局和网络;
  4. 后续补充真实用户监控,确认实验室优化是否真的改善用户体验。

原文中的站点示意图

Lighthouse 的“脚本评估”或 Main thread 工作很有价值:它能帮助我们发现 JavaScript 下载、解析、编译、执行以及框架初始化花费了多少时间。但不要把“脚本评估时间”直接等同于用户的 INP;INP 覆盖整个页面生命周期中的真实交互,脚本只是可能的原因之一。

原文中的 Lighthouse 脚本评估报告

三、我们当时的设置

重发布后,我们使用 Vue 2 作为 JavaScript 框架,Tailwind CSS 作为 CSS 框架,Symfony Encore(Webpack)负责打包。

站点不是 SPA。我们把 Vue 根实例挂载到一个 #app 元素上,再利用无渲染组件提供交互。服务器端使用 Twig 生成大部分 HTML 和初始数据,不需要为每个组件额外设计 API:

<notepad-star
  :product-id="{{ product_id }}"
  :initial-star="{{ is_stared(product_id) ? 'true' : 'false' }}"
>
  <div>
    <button type="button" @click.prevent="toggle">Toggle</button>
  </div>
</notepad-star>

product_id 是服务器端变量,is_stared(product_id) 是 Twig 函数,最终值作为 props 传入组件。真实项目必须保证服务器输出经过正确 HTML 转义,并避免把未验证的字符串直接拼进属性或脚本。

今天看这套架构

这种模式本质上是“服务器渲染 HTML + 局部客户端增强”,并不一定需要完整 SPA。今天可以继续使用这种思路,也可以选择:

  • Vue 3 的局部挂载或组件岛;
  • Nuxt、Astro 等框架提供的 SSR/SSG 与 islands;
  • Alpine.js、原生 Web Components 或少量原生 JavaScript 做轻量交互;
  • 对需要复杂状态、路由、表单和数据缓存的部分使用完整客户端框架。

重点不是把所有页面都塞进同一个根实例,而是让交互边界、数据边界和渲染边界尽量清晰。

四、生产环境与问题定位

我们当时的流程大致是:

  1. 在 Chrome 中生成性能报告;
  2. 研究报告和 Performance trace;
  3. 修改一个或一组变量;
  4. 在相同的网络、设备和缓存条件下重新测试;
  5. 用真实用户指标确认是否值得保留这次修改。

第一步是临时去掉脚本标签,观察指标变化:

原文中的生产环境对比报告

结果很明显:整个站点都挂载 Vue 根实例,即使页面只有少量交互,也需要让 Vue 处理大量可见 DOM。主页大约有 4500 个节点,框架初始化、模板编译、响应式建立和组件生命周期都可能增加主线程工作。

这并不意味着“DOM 节点多就一定不能用 Vue”,也不意味着 Alpine 一定更快。真正需要比较的是:

  • 初始 JavaScript 的传输、解析和执行成本;
  • 需要被增强的 DOM 范围;
  • 交互发生时的事件处理和更新范围;
  • 服务器渲染、hydration 或客户端挂载的成本;
  • 设备性能、网络和第三方脚本的影响。

五、我们需要优化什么

原文列出的方向仍然有价值,可以重新表达为:

  1. 让关键资源尽早且只加载一次;
  2. 减少初始 JavaScript 和长任务;
  3. 让交互尽快提供视觉反馈,改善 INP;
  4. 减少首屏无关组件的初始化;
  5. 用真实用户数据确认 LCP、INP、CLS 是否改善。

1. 预加载和预连接要谨慎

我们当时测试了预加载和预连接的不同组合。今天不应把“预加载越多越快”当作结论:

  • preload 只适合浏览器较晚发现、且当前页面一定会使用的关键资源;
  • preconnect 会提前消耗连接资源,只对确实很快要访问的跨源 origin 使用;
  • Google Tag Manager、广告和分析脚本通常不是首屏关键资源,不应为了提高 Lighthouse 分数而盲目 preload
  • 字体预加载要配合正确的 as="font"、格式和 crossorigin,否则可能重复下载;
  • CSS、JavaScript、图片和字体应优先通过响应式、代码分割、合理缓存和资源优先级解决。

可以用 DevTools Network 的 Initiator、Priority 和 Timing 检查资源是否被重复请求;不要只根据 Lighthouse 的一条建议复制标签。

2. 优化主线程

初始 JavaScript 的成本通常来自多个部分:

  • 下载和解压;
  • JavaScript 解析、编译和执行;
  • 框架初始化、响应式代理和组件生命周期;
  • 模板编译或 hydration;
  • 第三方脚本;
  • 大量 DOM 的样式计算、布局和绘制。

常见措施包括:

  • 删除未使用依赖和重复 polyfill;
  • 按路由、组件和功能做代码分割;
  • 使用动态 import() 延迟低频功能;
  • 将非关键第三方脚本延后,明确它们的性能和隐私影响;
  • 拆分长任务,把不阻塞下一帧的工作放到空闲时段或 Worker;
  • 只让需要交互的 DOM 进入客户端增强范围;
  • 给列表、图片和广告预留尺寸,减少 CLS;
  • 优先修复真实用户最常遇到的慢交互,而不是只优化首页加载截图。

六、Vue 版本与运行时构建

原文讨论了 Vue 运行时构建和包含模板编译器的构建。这个区分仍然存在,但现代构建工具通常会在构建阶段预编译单文件组件。

  • 运行时构建:包含响应式和渲染能力,不包含运行时模板编译器,适合已经被构建工具编译的模板;
  • 完整构建:额外包含模板编译器,适合运行时才需要把字符串模板编译成渲染函数的场景,但体积和执行成本更高;
  • Vue 3 + Vite/Rollup/Webpack:通常能够在构建时完成模板编译,并通过 tree-shaking 和代码分割减少交付内容。

Vue 2 已于 2023 年 12 月 31 日结束官方维护。新项目应使用 Vue 3;存量 Vue 2 项目应评估迁移、第三方扩展兼容性和安全支持,而不能把“还能从 CDN 下载”理解成仍在维护。

迁移到 Vue 3 也不是自动的性能优化。若仍然把整个站点和大量静态 DOM 挂到一个根实例上,换版本可能只改变部分开销,不能替代渲染边界和加载策略的重新设计。

七、为什么考虑 Alpine.js

我们整理了站点已有的交互组件:

  • 实时搜索;
  • 动态侧边栏;
  • 弹出菜单;
  • 模态框;
  • 购物车和大型菜单;
  • 少量不需要单独组件的全局操作。

这些功能需要一定的响应式、事件通信和状态管理,但并不需要完整 SPA 的路由和客户端数据层。于是我们用 Alpine.js 做概念验证,重建了滑动导航、动态购物车和主菜单等较复杂组件。如果这些组件能满足需求,其他轻量交互也有机会采用同样的方案。

经过约一天的验证,我们得到满意结果。但这只是该项目的结果,不是“Alpine.js 永远比 Vue 快”的证明。Alpine 更适合在已有 HTML 上添加局部行为;复杂表单、编辑器、图表、路由、跨页面缓存和大型团队协作仍可能更适合 Vue、React 或其他完整方案。

八、从 Alpine.js 2 到 Alpine.js 3

原文依赖 Alpine.js 2.8 和 Spruce。今天新代码应优先使用 Alpine.js 3 的 Alpine.store(),不要把 Spruce 当作默认状态管理方案。迁移时还需要逐项确认插件、指令、构建方式和事件语法。

1. 组件

组件可以由返回状态和方法的函数定义,再通过 x-data 初始化:

// components/modal.js
import { dispatch } from '@/helper/customEvent'

const modal = () => ({
  open: false,
  name: null,
  instantDisplay: false,

  init() {
    this.open = this.instantDisplay === true
  },

  openModal(event) {
    if (event.detail?.payload?.name !== this.name) {
      return
    }

    this.open = true
  },

  close() {
    this.open = false
    dispatch('modal-close', { name: this.name })
  }
})

export default modal

构建入口还需要把它注册为 Alpine 组件,例如 Alpine.data('modal', modal),再启动 Alpine。对应的 HTML 可以是:

<div
  x-data="modal"
  @modal-open.window="openModal($event)"
  x-show="open"
  x-cloak
>
  <button type="button" @click="close">关闭</button>
</div>

x-cloak 用于避免 Alpine 初始化前短暂显示内容;模态框还应处理焦点、键盘 Escape、背景滚动、ARIA 属性和关闭后的焦点回收,不能只依赖 x-show

2. 事件常量

原文用 enum 文件保存事件名。这个思路仍然有用,可以避免字符串散落在代码库中:

export const MODAL_OPEN = 'modal-open'
export const MODAL_CLOSE = 'modal-close'
export const SEARCH_RESULT = 'search-result'

3. CustomEvent helper

Alpine 可以监听窗口级事件,标准 CustomEvent 足以完成简单通信:

export function dispatch(name, payload = null, originalEvent = null) {
  window.dispatchEvent(
    new CustomEvent(name, {
      detail: {
        payload,
        originalEvent
      }
    })
  )
}

如果在 HTML 内联属性中调用,应明确把函数暴露到 window,或改用 Alpine 表达式,避免模块函数在全局不存在:

window.dispatch = dispatch
<button type="button" @click="dispatch('modal-open', { name: 'cart' })">
  打开购物车
</button>

事件名、payload 结构和来源应有明确约定。跨页面或复杂数据流不要无限增加全局事件,否则排查依赖关系会比组件通信更困难。

4. 内容提供器

内容提供器可以被看作无状态的客户端 API 层。它负责请求、检查 HTTP 状态、解析 JSON,再把结果交给组件:

import { dispatch } from '@/helper/customEvent'

export async function getResultFor(searchTerm, signal) {
  const url = new URL('/search', window.location.origin)
  url.searchParams.set('q', searchTerm)

  const response = await fetch(url, { signal })

  if (!response.ok) {
    throw new Error(`Search failed: ${response.status}`)
  }

  const result = await response.json()
  dispatch('search-result', result)
  return result
}

实际项目还要处理竞态请求、取消旧请求、加载状态、错误提示、缓存、鉴权和服务端返回的数据校验。把所有逻辑写成全局函数并不会自动解决这些问题。

5. 全局 store

Alpine 3 提供了官方的 Alpine.store()

document.addEventListener('alpine:init', () => {
  Alpine.store('megamenu', {
    activeId: null,

    toggle(id) {
      this.activeId = this.activeId === id ? null : id
    }
  })
})

模板中通过 $store 访问:

<nav x-data>
  <button type="button" @click="$store.megamenu.toggle('products')">
    产品
  </button>
  <div x-show="$store.megamenu.activeId === 'products'">
    产品菜单
  </div>
</nav>

Store 适合少量跨组件状态;不要把所有服务器数据、缓存、表单和路由都塞进一个全局对象。需要复杂数据同步时,应使用明确的数据层或完整框架方案。

九、性能结果应该怎么读

原文在开发环境中看到了大约 15–20 个百分点的提升,后来发布前和发布后又出现明显波动:

原文中的发布前报告

原文中的发布后报告

这种波动并不奇怪。Lighthouse 结果会受到以下因素影响:

  • 测试设备和 CPU 限制;
  • 网络延迟、吞吐和丢包;
  • 首次访问还是缓存访问;
  • 第三方脚本、广告和分析服务是否响应;
  • 测试时间、服务器负载和 CDN 调度;
  • Lighthouse 与浏览器版本的差异。

现在更建议按下面的顺序判断:

  1. 用 Lighthouse、DevTools Performance 和 WebPageTest 等实验室工具定位原因;
  2. web-vitals 收集 LCP、INP、CLS 等真实用户指标;
  3. 按页面、设备、地区、版本观察 p75 和失败率;
  4. 结合业务转化率、表单完成率和错误率评估收益;
  5. 把关键性能预算放入 CI,但避免让单次实验室噪声阻塞所有发布。

Core Web Vitals 的常用目标是 p75:LCP ≤ 2.5 秒、INP ≤ 200 毫秒、CLS ≤ 0.1。它们是用户体验目标,不是某个框架的专属指标。TBT 可以在没有可靠交互脚本的实验室测试中作为 INP 的诊断代理,但不能代替真实 INP。

十、迁移前的检查清单

迁移前不要只比较压缩后 JavaScript 文件的 KB 数,还应逐项确认:

  • 哪些页面真的需要复杂客户端状态;
  • 哪些组件可以保留服务器 HTML,只增强按钮和表单;
  • 是否需要路由、表单校验、国际化、无障碍和 SSR;
  • Vue 插件、指令、mixins 和第三方组件的替代方案;
  • Alpine 3 插件和构建方式是否兼容;
  • 模态框、菜单、焦点管理和键盘操作是否通过无障碍测试;
  • 事件通信是否可追踪,状态是否有明确所有者;
  • 迁移后首屏脚本、长任务、LCP、INP、CLS 和错误率是否真的改善;
  • 是否有可回滚的灰度发布和真实用户监控。

十一、写在最后

Vue 并不是“错误答案”。原文项目发现:对一个由服务器模板生成、交互点分散、没有 SPA 路由的电商站点,把整个页面交给 Vue 根实例会带来不必要的初始化成本;Alpine.js 更贴合它的局部增强需求。

这条经验可以总结为:

先观察页面的渲染边界、交互复杂度和数据流,再选择框架。不要为了追逐某个排行榜分数,机械地把 Vue 换成 Alpine;也不要为了使用熟悉的框架,把一整页静态 HTML 都变成客户端应用。

原文及参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS