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

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 项目性能优化:从指标、定位到代码实践

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/51-vue项目你一定会用到的性能优化!
本文目录14 个章节
  1. 一、性能优化先看什么
  2. 二、LCP、FCP、CLS 和 INP 怎么优化
  3. 三、通用网络与资源优化
  4. 四、Vue 项目中的具体优化
  5. 五、图片懒加载与媒体资源
  6. 六、长列表与虚拟滚动
  7. 七、函数式组件的版本边界
  8. 八、分批渲染和空闲任务
  9. 九、排查工具与方法
  10. 十、Nuxt 4 和 SSR/SSG 项目
  11. 十一、性能优化清单
  12. 十二、总结
  13. 官方参考
  14. 原文出处

Vue 项目性能优化:从指标、定位到代码实践

性能优化不能靠背诵“Vue 八股”。要先知道用户到底慢在哪里,再用网络、浏览器、框架和业务数据共同验证。原文中的 Lighthouse、FCP/LCP、代码分割、图片懒加载、虚拟滚动等主线仍然有效,但指标体系和部分 Vue API 已发生变化。

一、性能优化先看什么

1.1 Lighthouse 与真实用户监控

Lighthouse 是一种实验室性能审计工具,可以在控制条件下分析页面。Chrome DevTools、PageSpeed Insights 和 CI 中都可以使用它。

原文 Lighthouse 示意图(本地化,历史截图;界面以当前版本为准)

但 Lighthouse 分数不能代表所有真实用户:网络、设备、地区、缓存和交互路径都不同。生产环境还应使用 RUM(Real User Monitoring)采集真实用户的 PerformanceEntry、导航、资源和错误数据。

实验室数据(Lighthouse)
  ├─ 可重复,适合回归和定位
  └─ 设备、网络、脚本通常是模拟的

现场数据(RUM/CrUX)
  ├─ 反映真实用户体验
  └─ 受地区、设备、缓存和业务路径影响

原文中“本地 70 分,线上才可能及格”只是个人经验,不能作为通用标准。性能目标应该按业务、设备分层,并关注 p75 等分位数,而不是追求一个固定 Lighthouse 分数。

1.2 当前 Core Web Vitals

目前最重要的 Core Web Vitals 是:

指标含义以 p75 为目标的“良好”阈值
LCP最大内容绘制,反映主要内容出现速度≤ 2.5s
INPInteraction to Next Paint,反映交互响应≤ 200ms
CLS累积布局偏移,反映视觉稳定性≤ 0.1

FCP、TTFB、TBT 和 Speed Index 仍然是有用的诊断指标,但不是 Core Web Vitals 的全部。Lighthouse 主要提供实验室数据,不能直接代表真实用户的 INP;TBT 只能作为实验室响应性代理,INP 需要通过 RUM/CrUX 等真实用户数据评估:

指标用途
FCP首次内容绘制,观察页面何时出现第一个内容
TTFB首字节时间,定位服务器、网络和缓存问题
TBT实验室中长任务超过 50ms 的阻塞部分之和,常作为实验室响应性的代理指标
Speed Index观察可视区域填充速度
TTI历史 Lighthouse 指标,已不再是当前主要目标,应优先关注 INP/TBT

FID 已经由 INP 取代成为主要的交互 Core Web Vital。不要继续把原文的“六大指标”表述成当前唯一标准。

Lighthouse 性能报告历史截图(本地化,指标名称可能已更新)

二、LCP、FCP、CLS 和 INP 怎么优化

2.1 FCP:让第一个内容更快出现

FCP 受关键渲染路径影响,常见瓶颈包括:

  • HTML 到达慢;
  • 阻塞渲染的 CSS 过大;
  • 首屏必须执行的 JavaScript 过多;
  • 字体下载和字体交换阻塞文本;
  • SSR/预渲染输出为空或等待过多数据。

建议:

  1. 减少首屏必须下载和执行的 JS;
  2. 抽离非关键 CSS,避免把整个组件库样式放进首屏;
  3. 对关键字体设置合适的 font-display,必要时预加载,但不要盲目预加载大量字体;
  4. 在 SSR/SSG 中直接输出可见内容,在 CSR 中提供合理的骨架屏;
  5. 通过 Network、Coverage 和 Performance 面板确认真正的阻塞资源。

deferasync 不是“给所有脚本都加上就一定更快”:

  • defer 脚本在 HTML 解析完成后按顺序执行;
  • async 脚本下载完成后立即执行,顺序不保证,依赖关系复杂时可能出错;
  • type="module" 默认具有 defer 类似的延迟执行语义;
  • 第三方脚本应按是否关键、是否依赖 DOM、是否影响交互来安排,不要只因为“放底部”就认为问题解决。

2.2 LCP:优化最大内容元素

LCP 候选可能是首屏图片、视频 poster、标题或大段文本。浏览器会在加载过程中不断更新候选,最终选出符合条件的最大内容元素。

对于图片 LCP:

<img
  src="/hero.avif"
  width="1280"
  height="720"
  fetchpriority="high"
  decoding="async"
  alt="产品首页"
/>

优化方向:

  • 使用 AVIF/WebP 和合适尺寸;
  • 使用 srcset/sizes,不要让移动设备下载桌面大图;
  • 给图片设置 width/heightaspect-ratio,避免布局跳动;
  • LCP 图片不要无条件使用 loading="lazy",否则会推迟加载;
  • 必要时使用 preload,但只预加载真正的 LCP 资源;
  • 检查 CDN、缓存、压缩、响应时间和图片解码成本;
  • 如果首屏是文本,检查字体请求和 CSS 是否阻塞文本显示。
<template>
  <picture>
    <source
      type="image/avif"
      srcset="/hero-640.avif 640w, /hero-1280.avif 1280w"
      sizes="100vw"
    />
    <img
      src="/hero-1280.webp"
      width="1280"
      height="720"
      fetchpriority="high"
      alt="产品首页"
    />
  </picture>
</template>

2.3 CLS:预留布局空间

CLS 常见来源:

  • 图片没有宽高;
  • 广告、弹窗或异步内容插入顶部;
  • 字体切换导致文字尺寸改变;
  • 首屏请求完成后突然插入提示条;
  • 使用 transform 以外的方式在加载后移动内容。
.avatar {
  width: 48px;
  height: 48px;
  aspect-ratio: 1;
  object-fit: cover;
}

.hero {
  aspect-ratio: 16 / 9;
}

2.4 INP:减少交互后的主线程工作

INP 关注用户点击、键盘输入、触摸等交互之后,到下一次绘制的响应时间。常见问题不是 Vue 的一次 diff,而是:

  • 点击后执行大 JSON 解析;
  • 大列表同步筛选、排序和渲染;
  • 长任务占满主线程;
  • 事件处理器中同步执行大量第三方代码;
  • 不必要的深层响应式更新和大量组件重渲染。

建议:

  • 把昂贵计算拆分或放到 Web Worker;
  • 通过分页、虚拟列表和服务端筛选减少数据量;
  • 对搜索输入防抖,但不要让防抖掩盖服务器慢;
  • 用 Performance 面板找出 Long Task;
  • 只让真正需要变化的组件依赖响应式数据;
  • 非关键工作安排到空闲时段,但要保留不支持 API 的回退。

三、通用网络与资源优化

3.1 代码分割与路由懒加载

Vue Router 路由组件推荐直接使用动态 import:

const routes = [
  {
    path: '/reports',
    component: () => import('./views/ReportsView.vue'),
  },
]

大型非首屏组件也可以异步加载:

import { defineAsyncComponent } from 'vue'

const RichEditor = defineAsyncComponent(() => import('./RichEditor.vue'))
<RichEditor v-if="showEditor" />

代码分割要结合访问频率、chunk 数量和缓存策略。把所有依赖都切成极小文件会增加请求和解析开销;把整个应用打成一个大文件又会拖慢首屏。

3.2 Tree Shaking 与包体积

  • 生产构建关闭开发警告和调试代码;
  • 优先使用支持 ESM 的依赖;
  • 避免从一个巨大入口导入不需要的全部模块;
  • 检查重复依赖和多个版本;
  • 用 bundle analyzer 找到真正占体积的模块;
  • sideEffects 配置错误会破坏 Tree Shaking,不能盲目删除副作用模块。

旧项目中的:

{
  "scripts": {
    "report": "vue-cli-service build --report"
  }
}

仍可用于 Vue CLI 项目,但 Vue CLI 已处于维护模式。Vite 项目可以使用 rollup-plugin-visualizer、Vite 的构建分析能力或对应框架工具;Nuxt 4 应结合 Nitro、Vite 和 Nuxt analyze 工具分析产物。

Lighthouse 审计建议历史截图(本地化,历史资料)

3.3 压缩、缓存和 CDN

  • 优先使用 Brotli,无法使用时使用 gzip;
  • JS/CSS/字体设置长期缓存,文件名包含内容 hash;
  • HTML 和 API 按更新频率配置缓存;
  • CDN 能缩短网络距离,但源站、缓存命中率和回源策略同样重要;
  • HTTP/2/HTTP/3、连接复用和资源优先级会改变“减少请求”的收益;
  • 不要因为旧版“减少 HTTP 请求军规”就把所有文件拼成一个巨大 bundle。

3.4 预加载与资源优先级

<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link
  rel="preload"
  as="image"
  href="/hero.avif"
  type="image/avif"
/>

preloadprefetchpreconnect 只应针对确实需要的资源使用。错误的 preload 会抢占带宽,反而影响 LCP。

四、Vue 项目中的具体优化

4.1 让组件依赖稳定

Vue 3 性能指南强调,优化不是“少写几个组件”,而是减少不必要的更新:

<!-- 不够稳定:activeId 改变时,所有子组件都收到新 prop -->
<ListItem
  v-for="item in items"
  :key="item.id"
  :active-id="activeId"
/>

更稳定的方式是由父级计算每项真正需要的布尔状态:activeId 改变时,通常只有旧的 active 项和新的 active 项需要更新:

<ListItem
  v-for="item in items"
  :key="item.id"
  :active="item.id === activeId"
  :item="item"
/>

实际是否优化要用组件更新分析验证,不要为了形式而增加复杂 computed。

4.2 computed、watch 和方法

  • computed 适合同步派生值,并具有缓存;
  • watch 适合请求、日志、存储和第三方副作用;
  • 不要在模板中反复调用昂贵方法;
  • computed getter 不要修改其他状态;
  • 监听大型 reactive 对象要评估深度遍历成本;
  • 只有真正需要时才使用 deep: true,Vue 3.5+ 可以使用有限深度 deep: number

4.3 v-ifv-show 和 KeepAlive

  • v-if 不满足条件时不创建组件,适合低频切换或延迟加载;
  • v-show 保留组件和 DOM,只切换 CSS,适合高频切换;
  • KeepAlive 缓存组件实例和状态,适合返回后希望保留状态的页面;
  • KeepAlive 不是“初始化性能优化神器”,缓存过多会占内存;
  • v-ifv-for 不要放在同一个元素上,列表过滤使用 computed。
<KeepAlive :max="8">
  <component :is="currentView" />
</KeepAlive>

4.4 v-oncev-memo 和 shallow API

<!-- 确认永远不会改变的静态区域 -->
<StaticLegalNotice v-once />

<!-- 只有依赖变化时才更新该子树,使用前先测量 -->
<div v-memo="[item.id, item.version]">
  {{ item.title }}
</div>

大对象或外部不可变数据可以考虑:

import { shallowRef, triggerRef } from 'vue'

const rows = shallowRef<Row[]>([])

// 替换数组会触发更新
rows.value = nextRows

// 如果确实原地修改了 shallowRef 内部对象,需要手动通知
rows.value.push(newRow)
triggerRef(rows)

shallowRefmarkRaw 会减少响应式转换,但也会减少自动追踪。不要为了“优化”而让普通业务状态失去响应式。

五、图片懒加载与媒体资源

图片懒加载只适合非首屏、低优先级图片:

<img
  src="/avatar.webp"
  width="80"
  height="80"
  loading="lazy"
  decoding="async"
  alt="用户头像"
/>

不要给 LCP 图片统一加 loading="lazy"。对于长列表,懒加载可以和虚拟列表组合,但仍要设置图片尺寸避免 CLS。

旧文章推荐的 vue-lazyload 适合某些 Vue 2 项目,但新项目可以优先使用原生 loading="lazy"、IntersectionObserver 或经过维护的框架方案。引入插件前要检查 Vue 版本、SSR 支持和维护状态。

Lighthouse 审计条目历史截图(本地化,历史资料)Lighthouse 报告历史截图(本地化,历史资料)

六、长列表与虚拟滚动

一次渲染数万条数据会增加:

  • VNode 和 DOM 数量;
  • 响应式依赖和内存;
  • 布局、绘制和事件处理成本;
  • 滚动时的主线程工作。

虚拟列表只渲染视口附近的数据,并用占位高度保持滚动条长度:

总数据 100000 条
       ↓
只渲染视口 + overscan 的几十条
       ↓
上方/下方占位空间维持滚动位置

固定高度列表可以自己实现;动态高度、键盘可访问性和滚动定位复杂时,优先使用经过验证的库。详见 52:虚拟列表

旧文提到的 vue-virtual-scrollervue-virtual-scroll-list 是第三方 Vue 2 生态方案,使用前需要确认 Vue 版本和维护状态;Vue 3 也可以考虑 VueUse useVirtualList 或适配当前项目的虚拟列表库。

Speed Index 渐进渲染历史示意图(本地化,历史资料)

七、函数式组件的版本边界

Vue 2 中函数式组件没有实例和响应式状态,适合纯展示、无状态的轻量组件:

export default {
  functional: true,
  props: ['title'],
  render(h, { props }) {
    return h('span', props.title)
  },
}

Vue 3 中函数式组件就是接收 props/context 并返回 VNode 的函数;普通组件的运行时开销已经降低,不能再把“把所有展示组件改成函数式组件”当作通用性能方案:

import { h } from 'vue'

export default (props: { title: string }) => h('span', props.title)

首先确保组件结构清晰,再通过性能分析判断是否真的需要这种写法。

八、分批渲染和空闲任务

原文使用 requestIdleCallback 分批更新数据,这个方向适合非关键内容,但它不是所有浏览器都稳定支持,也不应拿来延迟关键首屏内容:

<script setup lang="ts">
import { onMounted, ref } from 'vue'

const rows = ref(
  Array.from({ length: 100 }, (_, id) => ({
    id,
    title: `第 ${id + 1} 条内容`,
  })),
)
const displayedRows = ref(rows.value.slice(0, 20))

function runWhenIdle(task: () => void) {
  // SSR 阶段不能访问 window;非关键任务可以等到客户端再安排
  if (typeof window === 'undefined') return

  if (typeof window.requestIdleCallback === 'function') {
    window.requestIdleCallback(() => task())
  } else {
    window.setTimeout(task, 0)
  }
}

onMounted(() => {
  runWhenIdle(() => {
    displayedRows.value = rows.value
  })
})
</script>

模板中遍历 displayedRows。真实项目可以把示例数组替换成已准备好的数据,并在首屏先返回少量内容,再在空闲时追加非关键内容。

适用场景:

  • 统计、预取、非首屏索引;
  • 大量低优先级内容的分批初始化;
  • 不影响用户立即交互的工作。

不适用场景:

  • LCP 内容;
  • 用户点击后必须马上出现的结果;
  • 用来掩盖一次性加载过多数据的问题。

对于真正昂贵的计算,可考虑 Web Worker;对于大列表,优先分页、服务端筛选和虚拟列表。

九、排查工具与方法

9.1 Performance 面板

用 Chrome DevTools Performance 记录一次真实操作,关注:

  • Long Task;
  • Recalculate Style、Layout、Paint;
  • JavaScript 调用栈;
  • 图片请求和解码;
  • 滚动处理器是否频繁触发;
  • Vue 组件更新与业务函数是否重复执行。

9.2 Coverage

Coverage 可以帮助发现首屏没有执行的 JS/CSS,但“未执行”不等于“可以删除”:它可能在第二个路由、点击后或错误路径才使用。应结合 bundle analyzer 和路由访问数据决定是否拆分。

Bundle analyzer 历史截图(本地化,工具界面以当前版本为准)

9.3 网络与资源

Network
  ├─ HTML/JS/CSS/图片大小
  ├─ TTFB、下载、解码和缓存
  ├─ 优先级与阻塞关系
  └─ 是否错误预加载或重复请求

可以在 Network 中模拟移动设备、慢速网络和禁用缓存,避免只在开发机高速网络下判断性能。

原文 DevTools 网络/覆盖率截图(本地化,历史资料)

十、Nuxt 4 和 SSR/SSG 项目

Nuxt 4 项目可以通过 SSR、预渲染或混合渲染减少首屏空白,但 SSR 不是自动更快:

  • 服务端渲染会增加服务器计算和数据请求;
  • HTML 输出后仍需要客户端 hydration;
  • 过大的 hydration 负担会影响 INP;
  • 缓存、边缘节点、数据请求和组件体积都需要测量;
  • 只在客户端需要的组件可以使用 client-only 或懒加载,但要注意 SEO 和首屏内容。
<script setup lang="ts">
const { data, pending, error } = await useFetch('/api/products')
</script>

<template>
  <ProductList v-if="data" :items="data" />
  <LoadingState v-else-if="pending" />
  <ErrorState v-else-if="error" />
</template>

静态站点、SSR、客户端渲染的目标不同:SEO 重要的内容可以预渲染或 SSR,登录后的后台页面可能更适合 CSR 和路由懒加载。不要用单一方案覆盖所有页面。

十一、性能优化清单

测量

  • 区分 Lighthouse 实验室数据和 RUM 现场数据;
  • 关注 LCP、INP、CLS,并结合 FCP、TTFB、TBT 定位;
  • 用 Performance、Network、Coverage 和 bundle analyzer 验证;
  • 使用 p75 和真实设备,不追求固定分数。

网络和资源

  • 路由和大型功能按需加载;
  • 图片使用现代格式、响应式尺寸和明确宽高;
  • 不给 LCP 内容错误添加 lazy;
  • 使用内容 hash、Brotli/gzip、合理缓存和 CDN;
  • 只预加载确实关键的资源;
  • 清理重复依赖和无效 polyfill。

Vue 代码

  • 过滤列表使用 computed,不把 v-ifv-for 写在同一元素;
  • 合理使用 v-ifv-show 和 KeepAlive;
  • 稳定 props,避免无意义的组件更新;
  • 大列表使用分页、服务端筛选或虚拟列表;
  • 只对需要的状态使用深层响应式;
  • 对异步请求、定时器和事件监听做好取消和清理。

用户体验

  • 骨架屏与实际布局尺寸一致;
  • 首屏内容优先,非关键内容延后;
  • 错误和慢网络状态可见;
  • 键盘、读屏和低端设备体验不能因“优化”被破坏。

十二、总结

  • 性能优化首先是测量,不是背“Lighthouse 多少分”;
  • 当前 Core Web Vitals 重点是 LCP、INP、CLS,FID 已由 INP 取代;
  • FCP、TTFB、TBT、Speed Index 用于诊断不同阶段的瓶颈;
  • LCP 需要优化资源发现、图片尺寸、服务器响应和字体,不能无脑懒加载;
  • CLS 需要预留尺寸,INP 需要减少长任务和交互后的同步工作;
  • Vue 项目重点关注代码分割、组件更新、响应式深度、列表规模和 hydration 成本;
  • 函数式组件、KeepAlive、v-show、CDN 和 PWA 都不是无条件有效的银弹;
  • 通过性能工具定位后,只修改真正影响用户的瓶颈,并用真实设备复测。

官方参考

原文出处

作者:老骥farmer

来源:稀土掘金

本文保留原文 Lighthouse、FCP/LCP/Speed Index、图片优化、代码分析、虚拟滚动、函数式组件和分批渲染内容,并更新 Core Web Vitals 与 Vue 3/Nuxt 4 的实践边界。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS