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

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 项目性能优化——实践指南(历史方案与现代补充)

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/19-Vue 项目性能优化 — 实践指南(网上最全 详细)
本文目录7 个章节
  1. 一、代码层面的优化
  2. 二、Webpack 层面的优化
  3. 三、基础的 Web 技术优化
  4. 当前 Web Vitals 目标
  5. 性能优化检查清单
  6. 官方参考
  7. 原文出处

Vue 项目性能优化——实践指南(历史方案与现代补充)

本文保留原文按 Vue 代码、Webpack、基础 Web 技术三部分整理的主线。原文主要对应 Vue 2、Vue CLI 2/3 和 Webpack 3/4;其中 CommonsChunkPluginurl-loaderbabel-plugin-component、旧 source map 名称等已经过时。当前优化应以真实测量、Vue 3/Vite 或 Webpack 5 的构建结果以及 Web Vitals 为依据。

本文内容分为:

  • Vue 代码层面的优化;
  • Webpack 历史配置与现代替代方案;
  • 缓存、压缩、CDN 和性能分析等基础 Web 技术。

一、代码层面的优化

1.1、区分使用 v-ifv-show

v-if 是真正的条件渲染。切换时,条件块中的 DOM、事件监听器和子组件会被创建或卸载;初始条件为假时具有惰性。

v-show 不会反复创建和卸载元素,而是通过 CSS 的 display 控制显隐。

因此:

  • 条件很少改变,或内容不需要初始渲染时,适合 v-if
  • 需要频繁切换,且节点初始化成本可以接受时,适合 v-show

这不是绝对的性能规则,实际还要看子树大小、切换频率和是否需要保留组件状态。

1.2、区分 computedwatch

computed 用于从响应式状态派生值,并缓存计算结果:

const activeUsers = computed(() => {
  return users.value.filter(user => user.isActive)
})

watch 用于在指定来源变化后执行副作用,例如请求接口、写入 storage 或同步第三方库:

watch(searchText, async (value, oldValue, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())

  const response = await fetch(`/api/search?q=${encodeURIComponent(value)}`, {
    signal: controller.signal
  })
  results.value = await response.json()
})

不要在 computed getter 中发请求或修改其他状态。watch 默认惰性执行,深层监听、immediateflush 应按实际需要配置;Vue 3.5+ 还提供了 watcher 清理 API。

1.3、列表使用稳定的 key,不要把 v-ifv-for 混在一起

列表项应使用稳定、唯一的业务 key:

<ul>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</ul>

列表过滤应提前放在 computed 或普通函数中:

const activeUsers = computed(() => {
  return users.value.filter(user => user.isActive)
})

原文说“v-forv-if 优先级高”,只适用于 Vue 2。Vue 3 已改为 v-if 优先,而且在同一元素上混用仍然不利于阅读和性能。Vue 3 中也可以使用 <template v-for> 把条件放到合适的位置:

<template v-for="user in users" :key="user.id">
  <li v-if="user.isActive">{{ user.name }}</li>
</template>

1.4、长列表和不可变数据

原文使用 Object.freeze() 避免 Vue 2 对只读数据进行深度观察:

// Vue 2 历史方案
export default {
  data: () => ({
    users: []
  }),
  async created() {
    const { data } = await axios.get('/api/users')
    this.users = Object.freeze(data)
  }
}

这个方案有两个边界:Object.freeze() 只冻结当前对象层级,且冻结后不能直接修改;示例中的 axios 响应通常还需要取 response.data。它是 Vue 2 时代的优化技巧,不要在没有测量时盲目使用。

Vue 3 对大型不可变数据更推荐使用 shallowRef(),通过替换根引用触发更新:

import { shallowRef } from 'vue'

const users = shallowRef([])

async function loadUsers() {
  const response = await fetch('/api/users')
  users.value = await response.json()
}

如果页面真正的问题是 DOM 节点数量,应使用列表虚拟化,只渲染视口附近的项;也可以根据场景评估 v-memo、分页和增量渲染。

1.5、正确清理事件和副作用

Vue 会清理模板指令和组件内部的事件,但通过 windowdocument、WebSocket、定时器或第三方库手动注册的副作用,需要自行清理:

// Vue 2
export default {
  mounted() {
    window.addEventListener('click', this.handleClick)
  },
  beforeDestroy() {
    window.removeEventListener('click', this.handleClick)
  }
}

Vue 3 使用 onMounted / onUnmounted

<script setup>
import { onMounted, onUnmounted } from 'vue'

function handleClick(event) {
  console.log(event)
}

onMounted(() => window.addEventListener('click', handleClick))
onUnmounted(() => window.removeEventListener('click', handleClick))
</script>

监听器必须使用同一个 target、事件类型和函数引用才能移除。涉及 windowdocument 的代码还要避免在 SSR 期间直接执行。

1.6、图片懒加载

对于首屏之外的大量图片,可以优先使用浏览器原生能力:

<img
  src="/images/article-cover.webp"
  loading="lazy"
  width="800"
  height="600"
  alt="文章封面"
>

widthheight 有助于浏览器提前预留空间,减少布局偏移;响应式图片可以搭配 srcsetsizes。首屏主图通常不应盲目设置 lazy,应该根据 LCP 目标和实际布局决定。

原文使用的 vue-lazyload 是 Vue 2 生态的历史插件,且 npm install vue-lazyload --save-dev 不适合运行时依赖。现代项目优先使用 loading="lazy";需要自定义占位、预加载或复杂可见性控制时,可以使用 IntersectionObserver 或维护良好的 Vue 3 组件。

1.7、路由懒加载

单页面应用如果把所有路由组件都同步打入首屏,首屏 JavaScript 会变大。动态 import() 可以让构建工具拆出代码块,在访问路由时加载:

Vue Router 3 历史写法:

const Foo = () => import('./Foo.vue')

const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

Vue Router 4 当前写法:

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/foo',
      component: () => import('./views/FooView.vue')
    }
  ]
})

路由懒加载是代码分割的一种形式。应根据路由访问频率、网络条件和 chunk 数量设置拆分边界,不能只追求 chunk 越多越好。路由组件通常直接使用动态 import(),不需要额外包一层 defineAsyncComponent()

1.8、第三方插件的按需引入

原文使用 babel-plugin-component 和 Element UI:

// Vue 2 / Element UI 历史方式
import Vue from 'vue'
import { Button, Select } from 'element-ui'

Vue.use(Button)
Vue.use(Select)

这套方案依赖 Babel 6/旧 Element UI 的组件安装机制。当前 Vue 3 项目应优先选择提供 ESM 的依赖,让构建工具进行 tree-shaking,并按组件库官方文档使用 Element Plus 等库的自动导入或局部导入方案。不要为了“按需”复制一份已经过时的 Babel 配置;还要确认样式是否被单独引入。

通用原则:

  • 只导入实际使用的 API。
  • 优先 ESM 和 tree-shaking 友好的包。
  • 检查依赖的实际构建体积,而不是只看 npm 包体积。
  • 对很大的库寻找轻量替代品,但要先确认功能和维护状态。

1.9、优化无限列表

长列表的瓶颈经常来自 DOM 节点数量,而不是单纯的 Vue 计算。窗口化(virtualization)只渲染视口附近的内容,可以减少节点创建和更新成本。

可参考:

使用虚拟列表时还要处理动态高度、键盘导航、滚动位置、可访问性和搜索过滤;如果列表并不大,虚拟化本身的复杂度可能得不偿失。

1.10、SSR、SSG 和预渲染

服务端渲染(SSR)是在服务器生成首屏 HTML,再在客户端 hydration;静态站点生成(SSG)则是在构建时生成 HTML 文件。它们可以改善首屏内容到达时间,并让内容更容易被搜索引擎获取,但也引入服务端负载、hydration 一致性和数据预取等问题。

SSR 的优点:

  • 首屏 HTML 可以直接包含内容,通常有利于 LCP 和 SEO。
  • 用户不必等待全部客户端 JavaScript 下载后才看到首屏文本。

SSR 的限制:

  • 服务端不能访问 windowdocument 等浏览器 API。
  • Vue 2 中 beforeMountmounted 不在服务端执行;Vue 3 的 onMounted 等 DOM 生命周期同样不在 SSR 执行。
  • Vue 3 可以使用 onServerPrefetch(),也可以交给 Nuxt 等框架处理服务端数据获取和 hydration。
  • 服务端渲染会增加 CPU、内存和缓存设计成本。

如果主要是博客、营销页等内容型页面,SSG 往往比每次请求都 SSR 简单。旧的 prerender-spa-plugin 可以作为 Vue CLI 时代的历史方案保留,但当前应结合 Vite、Nuxt 或实际部署框架选择 SSG/预渲染方式。不要笼统地说搜索引擎“完全不会等待 Ajax”;正确结论取决于搜索引擎、内容出现时机和站点的渲染策略。

二、Webpack 层面的优化

2.1、Webpack 处理图片资源

原文的 url-loader 配置属于 Webpack 3/4 时代。Webpack 5 应优先使用 Asset Modules:

// webpack 5
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        },
        generator: {
          filename: 'images/[name].[contenthash][ext]'
        }
      }
    ]
  }
}

小文件可以内联为 data URL,大文件输出为带 hash 的资源。阈值应结合实际网络和缓存测量,不要认为所有图片都适合转 base64。图片压缩可以在构建阶段使用 image-minimizer-webpack-plugin 或在资源流水线中处理;不要重复压缩已经是 WebP/AVIF 的资源。

2.2、减少 Babel 辅助代码

原文提到的 babel-plugin-transform-runtimebabel-runtime 是 Babel 6 时代名称。现代 Babel 7 使用:

{
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs": false
    }]
  ]
}

同时根据构建目标安装匹配版本的 @babel/runtime,它是运行时代码依赖,通常应放在 dependencies 而不是只放在 devDependencies。现代 ESM、目标浏览器配置和构建工具也会影响最终是否需要这些辅助代码,不能只靠一个插件保证体积变小。

2.3、提取公共代码和代码分割

原文的 CommonsChunkPlugin 已在 Webpack 4 中移除。Webpack 5 使用 optimization.splitChunksruntimeChunk

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    runtimeChunk: 'single'
  }
}

Webpack 会根据模块图和配置拆分公共依赖。拆分时应关注:

  • 首屏真正需要的代码是否减少。
  • chunk 是否过多,导致请求和解析成本上升。
  • 依赖更新后 hash 是否稳定,缓存是否能复用。
  • 动态 import 的边界是否符合用户访问路径。

Webpack 3 时代的 vendor.jsmanifest.js 配置可以作为历史资料保留,但不能直接复制到 Webpack 5 或 Vite。

2.4、模板预编译

使用 Vue 单文件组件和构建工具时,模板通常在构建阶段编译为 render 函数,生产环境不需要携带完整模板编译器,可以减小体积并避免运行时编译成本。

vue-template-loader、Browserify/v2 Vueify 等属于旧工具链。Vue 3 使用 @vue/compiler-sfc,Webpack 项目通常配合 Vue Loader 16/17,Vite 项目则由 @vitejs/plugin-vue 处理。模板预编译不是把模板简单交给几条正则表达式替换,而是由编译器生成 AST 和运行时代码。

2.5、提取组件 CSS

开发环境中通过 style 注入方便热更新;生产环境可以提取 CSS,以利于缓存、压缩和并行加载。Webpack 5 常见的生产配置是 mini-css-extract-plugin,而不是依赖旧版 Vue CLI 模板的固定文件名。

CSS 提取并不自动解决所有 FOUC 问题。SSR、关键 CSS、字体加载和样式分割仍应结合页面首屏结构验证。避免把不需要的全局样式和第三方主题全部打入首屏。

2.6、合理配置 Source Map

Source Map 让压缩后的线上代码可以映射回源代码,便于定位错误,但公开 source map 也可能暴露源码。原文中的 cheap-module-eval-source-map 属于旧 Webpack 命名,不能作为 Webpack 5 通用推荐。

常见选择:

  • 开发环境:eval-source-mapeval-cheap-module-source-map,便于调试。
  • 生产环境:根据是否公开源码选择 source-maphidden-source-map 或不生成 source map。
  • 线上错误监控:将 source map 上传到监控平台,但不一定公开给浏览器。

cheapmoduleevalsource-map 分别影响列信息、源码映射方式、生成速度和 map 文件形式,应以 Webpack devtool 文档 为准,而不是只记住一个固定值。

Webpack Source Map 选项的历史示意图(本地化)

2.7、构建结果分析

Bundle Analyzer 仍然有用,但原文的 webpack.prod.conf.jsconfig.build.bundleAnalyzerReportnpm run build --report 是 Vue CLI 2 的项目约定。现代 Webpack 可以直接配置:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static'
    })
  ]
}

Vite、Nuxt 等工具应使用各自的分析插件或构建报告。分析时重点查看重复依赖、首屏 chunk、动态 chunk 和 source map,不要只看压缩前的单个文件大小。

构建产物分析报告的历史示意图(本地化)

2.8、构建耗时优化

如果构建时间过长,可以先使用构建工具的 profile、cache 和分析功能定位瓶颈,再考虑:

  • 升级到受支持的 Webpack/Vite 和 loader。
  • 开启持久化缓存,减少重复编译。
  • 减少不必要的 polyfill、loader 和大型依赖。
  • 将类型检查、Lint、测试等任务合理并行或移出热更新主链路。
  • 不要因为“优化构建”而关闭必要的 source map、校验和错误提示。

三、基础的 Web 技术优化

3.1、压缩传输内容

Express 可以使用 compression 中间件协商压缩:

import express from 'express'
import compression from 'compression'

const app = express()
app.use(compression())

生产环境也可以由 Nginx、CDN 或网关处理 Brotli/gzip。压缩率不是固定的“通常 70%”:文本类型受内容影响较大,图片、视频、压缩包等通常不应重复压缩。响应还需要正确设置 Content-Encoding,并在缓存层考虑 Vary: Accept-Encoding

开启压缩后的响应头历史示意图(本地化)

3.2、浏览器缓存

HTTP 缓存可以减少重复下载,但应根据资源是否带内容 hash 设计策略:

  • 带内容 hash 的 JS、CSS、图片等静态资源可以使用长期缓存,例如:
Cache-Control: public, max-age=31536000, immutable
  • HTML 入口通常需要快速发现新版本,可以使用 Cache-Control: no-cache 配合 ETag/条件请求,而不是无条件长期缓存。
  • 个性化响应不应进入共享缓存,应根据情况使用 privateno-store
  • no-cache 的含义是使用前需要重新验证,不等同于完全不存储;no-store 才是禁止存储。

缓存策略必须配合文件名 hash、CDN 刷新和发布流程,否则用户可能拿到旧入口或旧 chunk。

3.3、CDN 的使用

CDN 可以把静态资源缓存到更靠近用户的节点,降低网络延迟和源站带宽压力。现代 HTTP/2 和 HTTP/3 支持多路复用,因此不能简单依靠“使用不同域名增加并发连接数”来优化;过多域名还会增加 DNS、TLS 和连接建立成本。

使用 CDN 时应考虑:

  • 资源缓存时间和发布失效策略。
  • Cache-ControlETag 和内容 hash。
  • gzip/Brotli 协商、跨域和字体响应头。
  • 源站回源、可用区、隐私和访问控制。

3.4、使用 Chrome Performance 查找性能瓶颈

Chrome DevTools 的 Performance 面板适合实验室分析:

  1. 打开开发者工具的 Performance 面板。
  2. 点击 Record 开始录制。
  3. 刷新页面或执行需要分析的交互。
  4. 点击 Stop,查看主线程任务、网络、布局、绘制和长任务。

Chrome Performance 面板历史截图(本地化)

同时建议结合:

  • PageSpeed Insights 和 Lighthouse:实验室指标和诊断。
  • Chrome User Experience Report(CrUX):真实用户数据。
  • RUM:项目自己的真实用户监控。
  • Vue Devtools 或 app.config.performance:分析 Vue 组件相关标记。

当前 Web Vitals 目标

页面性能不应只看打包文件大小,还应关注真实用户体验。当前常用的核心指标包括:

  • LCP:最大内容绘制,良好目标通常不超过 2.5 秒。
  • INP:交互到下一次绘制,良好目标通常不超过 200 毫秒。
  • CLS:累计布局偏移,良好目标通常不超过 0.1。

这些目标通常按移动端和桌面端的第 75 百分位评估。FID 已由 INP 取代。应先测量,再根据瓶颈选择减少 JavaScript、优化 LCP 图片、减少长任务、预留图片尺寸或虚拟化列表等措施。

性能优化检查清单

  1. 先用 Performance、Lighthouse、RUM 或构建分析确认瓶颈。
  2. 通过 computed 过滤列表,使用稳定 key,避免无意义的深度 watch。
  3. 对非首屏路由和大功能使用动态 import。
  4. 使用原生图片懒加载、响应式图片和合适的图片格式。
  5. 清理窗口事件、定时器、请求和第三方订阅。
  6. Webpack 5 使用 Asset Modules、SplitChunks 和合适的 source map;Vite/Nuxt 使用对应构建工具能力。
  7. 为带 hash 的静态资源设置长期缓存,为 HTML 设置重新验证策略。
  8. 使用 Brotli/gzip、CDN 和 HTTP/2/3,但不要重复压缩或盲目拆域名。
  9. 用 LCP、INP、CLS 和真实用户数据验证优化是否有效。

官方参考

原文出处

本文整理自:

作者:随风而逝_风逝

来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS