Vue3知识弥补漏洞——性能优化篇

Vue2/Vue3 性能优化指南

1. 计算属性与方法的选择
  • 计算属性 :计算属性会基于其依赖进行缓存,只有依赖发生变化时才会重新计算。
    • 使用场景:当需要基于现有数据派生出新数据,并且该派生逻辑较复杂或频繁调用时,优先使用计算属性。
  • 方法 :方法每次调用都会重新执行计算逻辑。
    • 使用场景:不需要缓存的场景,比如简单的事件处理函数或无需多次重复计算的逻辑。
  • 注意:滥用方法代替计算属性可能导致性能问题,因为方法在模板中多次引用时会反复执行。

2. 虚拟 DOM 和 diff 算法
  • Vue 使用虚拟 DOM 进行高效的 DOM 操作。
  • diff 算法优化 :
    • 提供唯一的 key,避免重新渲染整个列表。
    • 避免频繁增删 DOM 节点,尽量批量更新。
  • 静态节点提升 (Vue 3 特性):
    • Vue 3 自动对静态节点进行标记并提升到渲染函数外部,从而避免重复创建。

3. 懒加载与代码分割
  • 懒加载 :

    • 将路由组件按需加载,减少初始加载体积。
    javascript 复制代码
    const About = () => import('@/components/About.vue');
  • 代码分割 :

    • 使用 Webpack 或 Vite 的动态导入功能,将代码拆分成多个块,按需加载。
    javascript 复制代码
    import(/* webpackChunkName: "group-foo" */ './module.js');
  • 配合路由的 meta 字段和导航守卫动态加载需要的资源。


4. Vuex 性能优化
  • 模块化管理:按需拆分模块,避免单一 store 过于庞大。
  • 避免频繁触发 mutations:将多次 mutation 合并为一次操作。
  • 使用 getters 缓存派生状态:减少组件中直接计算派生状态的复杂性。
  • 持久化存储 :
    • 使用插件(如 vuex-persistedstate)将部分状态存储在 localStorage 或 sessionStorage,避免重复请求。
  • 删除无用的订阅:尽量减少监听过多状态变化的组件。

5. 减少不必要的渲染
  • v-if vs v-show :
    • v-if 会真正销毁和重建 DOM,适合条件变化频率较低的场景。
    • v-show 只是切换 CSS 的 display 属性,适合频繁切换显示的场景。
  • 列表优化 :
    • 使用 v-for 时提供唯一且稳定的 key。
    • 避免嵌套过深的 v-for 循环。
  • 事件绑定优化 :
    • 避免在模板中使用内联事件,提取为方法可以重用逻辑。
  • 减少响应式数据的体积 :
    • 仅对必要的数据使用响应式,减少组件的依赖追踪开销。

6. 使用异步组件
  • 异步组件可以延迟加载,直到需要时再加载。
javascript 复制代码
const AsyncComponent = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
);
  • 使用场景 :

    • 大型组件。
    • 不常用的对话框、模态框等功能组件。
  • 配合加载状态和超时:

    javascript 复制代码
    const AsyncComponent = defineAsyncComponent({
        loader: () => import('./components/MyComponent.vue'),
        loadingComponent: LoadingComponent,
        errorComponent: ErrorComponent,
        delay: 200,
        timeout: 3000,
    });

通过这些优化策略,可以显著提升 Vue 项目的性能和用户体验。

Vue2 和 Vue3 中虚拟 DOM 与 diff 算法详解


一、什么是虚拟 DOM?

虚拟 DOM 是一种以 JavaScript 对象形式描述真实 DOM 的抽象表示。Vue 使用虚拟 DOM 来追踪 UI 的状态变化并高效地更新界面。

核心思想:

  1. 使用 JavaScript 对象(虚拟节点)表示 DOM 结构。
  2. 通过比较新旧虚拟 DOM(diff 算法)找到最小的变化集。
  3. 最小化对真实 DOM 的操作以提升性能。

二、Vue2 的虚拟 DOM 与 diff 算法
1. 虚拟 DOM 的基本工作原理
  1. 初次渲染时:
    • 将模板编译成渲染函数,渲染函数生成虚拟 DOM。
    • 虚拟 DOM 被渲染成真实 DOM。
  2. 数据更新时:
    • 渲染函数重新生成新虚拟 DOM。
    • 对比新旧虚拟 DOM,生成需要更新的差异(patch)。
    • 最后根据差异最小化地操作真实 DOM。
2. Vue2 的 diff 算法

Vue2 的 diff 算法基于以下特性设计:

  • 只比较同级节点:不同层级的节点直接删掉重建。
  • 递归比较子节点:只处理具体需要更新的节点,跳过未变更的部分。

过程:

  1. 根节点比较 :
    • 如果两个节点类型不同(例如 div 和 span),直接替换。
    • 如果节点类型相同,继续比较属性和子节点。
  2. 属性比较 :
    • 遍历新旧节点的属性,添加、更新、或删除不一致的属性。
  3. 子节点比较 :
    • 对子节点递归执行 diff。
    • 优化场景 :
      • 当子节点是简单的列表时,通过 key 来加速节点的更新。
3. Vue2 的 diff 细节优化
  • key 的作用 :
    • key 用于唯一标识列表中的节点,有助于 Vue 高效地对比和复用 DOM 节点。
    • 如果没有 key,Vue2 使用"就地复用"策略,可能会导致 DOM 错乱。

示例:

html 复制代码
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
  • 静态节点不优化 :
    • Vue2 对模板中的所有节点都认为是可能变化的,因此会进行全面的对比,即使是从不变化的静态节点。

三、Vue3 的虚拟 DOM 与 diff 算法
1. Vue3 的改进目标
  • 更快:对虚拟 DOM 和 diff 算法进行了多项优化。
  • 更小:减少运行时代码体积。
  • 更易维护:基于现代代码架构重新设计。
2. Vue3 的核心优化
  1. 静态提升 :
    • Vue3 会分析哪些节点是静态的,将它们在编译时提升到渲染函数之外,避免每次重新渲染。
    • 例如,静态文本节点只会被创建一次。
  2. 缓存事件处理函数 :
    • 默认缓存事件处理函数,避免因更新父组件而重新绑定事件。
  3. Block Tree 优化 :
    • Vue3 会在模板中生成"Block Tree",每个 Block 只关注动态节点,跳过静态节点的对比。
  4. 模板编译优化 :
    • 编译阶段将模板转化为更加高效的渲染函数,减少运行时开销。
3. Vue3 的 diff 算法

Vue3 的 diff 算法相较 Vue2 进行了显著优化:

  • 动态节点追踪 :
    • Vue3 的编译器会标记动态节点,只对动态节点执行 diff,静态节点被直接跳过。
  • 稳定的列表更新 :
    • 对于 v-for 列表,Vue3 会尽可能复用 DOM 节点,避免删除重建。

过程:

  1. 头尾双指针 :
    • 对新旧子节点使用双指针算法,分别从头尾开始比较,尽可能减少移动节点的次数。
  2. 快速定位节点 :
    • 如果发现新列表中间部分的节点顺序发生变化,会构建索引表以快速找到新节点的位置。
  3. 减少 DOM 操作 :
    • 尽量合并多次修改,使用批量 DOM 更新策略。

四、Vue2 和 Vue3 的对比
特性 Vue2 Vue3
静态节点处理 无优化,所有节点都认为可能动态 静态节点提升,跳过对比
动态节点追踪 每次对所有节点执行 diff 编译时标记动态节点,运行时仅对动态节点 diff
列表 diff 算法 基于 key 的就地复用,效率较低 使用双指针和索引表优化
渲染函数体积 较大 更小、更高效
事件缓存 默认不缓存 默认缓存事件处理函数

五、通俗易懂的示例

假设有一段列表:

html 复制代码
<ul>
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>

更新场景:

  • Vue2:
    • 如果列表变化(如顺序调整),Vue2 会对每个节点逐一对比并操作 DOM,效率较低。
  • Vue3:
    • Vue3 使用双指针优化,仅对变化部分执行最小 DOM 操作,跳过静态部分,性能更高。

六、总结
  • Vue2 的虚拟 DOM 和 diff 算法已经非常高效,但由于没有静态提升和动态节点标记等优化,对复杂场景性能表现稍逊。
  • Vue3 通过 Block Tree、静态节点提升等技术大幅提升了 diff 性能,特别是在模板复杂或组件嵌套深的场景下,能显著减少不必要的计算和 DOM 操作。

通过理解虚拟 DOM 和 diff 算法的工作原理与优化点,可以更好地编写高效的 Vue 应用!

代码分割与路由的动态加载资源:实现详解

在 Vue 应用中,通过代码分割和路由动态加载资源,可以显著减少初始加载时间,提高页面性能。以下是具体的实现方法及其详细解析。


一、代码分割的基本原理

代码分割通过将应用的代码拆分为多个独立模块(chunks),按需加载这些模块,减少首次加载的体积。Vue 通常通过以下两种方式实现代码分割:

  1. 动态导入(Dynamic Import):按需加载组件或资源。
  2. 懒加载(Lazy Loading):结合路由系统加载组件。

工具支持:

  • Webpack:默认支持代码分割。
  • Vite:支持类似的按需加载。

二、在 Vue 项目中实现代码分割

1. 动态导入组件

将组件的导入变为异步加载的形式,只有在需要时才加载:

javascript 复制代码
// 普通导入
import MyComponent from '@/components/MyComponent.vue';

// 动态导入
const MyComponent = () => import('@/components/MyComponent.vue');

使用示例:

vue 复制代码
<template>
  <div>
    <button @click="loadComponent">加载组件</button>
    <component :is="dynamicComponent" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      dynamicComponent: null,
    };
  },
  methods: {
    async loadComponent() {
      const module = await import('@/components/MyComponent.vue');
      this.dynamicComponent = module.default;
    },
  },
};
</script>

2. 路由懒加载

Vue Router 提供与动态导入结合的懒加载功能。

配置路由
javascript 复制代码
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

const routes = [
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue'), // 懒加载
  },
];

export default new Router({
  routes,
});
效果
  • 初始加载时,About.vue 不会被打包到主包中。
  • 当用户访问 /about 时,Vue Router 会动态加载对应的 chunk。

三、配合路由的 meta 字段动态加载资源

1. meta 字段简介

Vue Router 的每个路由对象都支持一个 meta 属性,可以用来存储自定义数据。例如:

javascript 复制代码
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: {
      requiresAuth: true, // 自定义字段:需要权限
      preload: ['chart-lib'], // 需要预加载的资源
    },
  },
];

meta 字段的常用场景:

  • 权限控制。
  • 动态加载资源。
  • 动态设置页面标题。

2. 导航守卫中使用 meta 字段

通过路由的全局或局部导航守卫,动态加载特定的资源或执行逻辑。

示例:动态加载资源
javascript 复制代码
import { loadExternalResource } from './utils'; // 自定义函数,用于动态加载外部资源

const router = new Router({
  routes: [
    {
      path: '/dashboard',
      component: () => import('@/views/Dashboard.vue'),
      meta: {
        preload: ['https://cdn.example.com/chart-lib.js'], // 需要预加载的资源
      },
    },
  ],
});

router.beforeEach(async (to, from, next) => {
  // 检查路由是否有需要预加载的资源
  if (to.meta.preload) {
    await Promise.all(to.meta.preload.map((url) => loadExternalResource(url)));
  }

  next();
});

// 工具函数:加载外部资源
function loadExternalResource(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

3. 权限控制

通过 meta.requiresAuth 来检查是否需要权限:

javascript 复制代码
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isUserLoggedIn()) {
    next('/login'); // 重定向到登录页面
  } else {
    next(); // 放行
  }
});

function isUserLoggedIn() {
  // 自定义登录状态检查逻辑
  return !!localStorage.getItem('token');
}

4. 动态设置页面标题

在导航守卫中利用 meta.title 动态设置页面标题:

javascript 复制代码
router.afterEach((to) => {
  if (to.meta.title) {
    document.title = to.meta.title;
  }
});

路由配置:

javascript 复制代码
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home.vue'),
    meta: {
      title: '首页',
    },
  },
];

四、完整实现案例:动态加载资源与懒加载

路由配置
javascript 复制代码
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'), // 懒加载组件
    meta: {
      requiresAuth: true,
      preload: ['https://cdn.example.com/chart-lib.js'], // 动态加载资源
      title: '仪表盘',
    },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: {
      title: '登录',
    },
  },
];
导航守卫
javascript 复制代码
router.beforeEach(async (to, from, next) => {
  // 权限控制
  if (to.meta.requiresAuth && !isUserLoggedIn()) {
    return next('/login');
  }

  // 动态加载资源
  if (to.meta.preload) {
    await Promise.all(to.meta.preload.map((url) => loadExternalResource(url)));
  }

  next();
});

router.afterEach((to) => {
  // 动态设置页面标题
  if (to.meta.title) {
    document.title = to.meta.title;
  }
});

function isUserLoggedIn() {
  return !!localStorage.getItem('token');
}

function loadExternalResource(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

五、总结

  1. 代码分割 :
    • 通过动态导入 (import()) 和懒加载 (component: () => import(...)) 将代码按需加载,减少初始体积。
  2. meta 字段的作用 :
    • 可以用于权限验证、动态加载资源和设置页面标题等场景。
  3. 导航守卫 :
    • 配合 meta 字段,在路由切换时动态执行逻辑(如加载资源、验证权限)。
  4. 最佳实践 :
    • 配置合理的路由分块。
    • 使用 meta 字段结合导航守卫动态管理资源和逻辑,增强灵活性和性能。

通过这些方法,可以显著提高 Vue 应用的加载性能和用户体验。

Webpack 和 Vite 的代码分割配置详解

代码分割是优化 Web 应用性能的重要手段,可以减少初始加载时间并提高运行效率。以下是 Webpack 和 Vite 的代码分割配置方法:


一、Webpack 中代码分割

Webpack 提供了多个方式进行代码分割,主要包括:

  • 动态导入 :通过 import() 实现按需加载。
  • 手动分包 :使用 SplitChunksPlugin 分割代码。

1. 动态导入

Webpack 支持通过 import() 函数实现按需加载模块。Webpack 在打包时会将动态导入的模块分割为单独的 chunk,在需要时加载。

配置示例
javascript 复制代码
// 动态导入组件
const MyComponent = () => import('./MyComponent.vue');

生成的 chunk 文件名可以通过 Webpack 配置修改:

javascript 复制代码
module.exports = {
  output: {
    chunkFilename: 'js/[name].[contenthash].js', // 输出的 chunk 文件名格式
  },
};

2. 使用 SplitChunksPlugin 配置代码分割

Webpack 内置的 SplitChunksPlugin 用于自动提取共享模块。常见场景包括分离第三方库和共享代码。

配置示例

在 webpack.config.js 中进行配置:

javascript 复制代码
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // 适用于同步和异步模块
      minSize: 30 * 1024, // 文件最小大小,默认30KB
      maxSize: 500 * 1024, // 文件最大大小,默认无上限
      minChunks: 1, // 模块被引用的次数,至少引用1次才会被分割
      maxAsyncRequests: 30, // 按需加载时并行请求的最大数量
      maxInitialRequests: 30, // 入口点的最大并行请求数
      automaticNameDelimiter: '~', // 文件名分隔符
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors', // 生成的 chunk 名称
          priority: -10, // 优先级
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, // 复用已存在的模块
        },
      },
    },
  },
};
示例解释
  • vendors :将来自 node_modules 的模块提取到 vendors 文件中。
  • default:对于被多个入口引用的模块,提取到默认的共享模块。

3. 实现懒加载

通过 webpackChunkName 自定义 chunk 名称。

javascript 复制代码
const About = () => import(/* webpackChunkName: "about" */ './About.vue');

生成的文件名称为 about.js。


二、Vite 中代码分割

Vite 使用 ES 模块和 Rollup 作为打包工具,其代码分割依赖 Rollup 的配置。


1. 动态导入

和 Webpack 类似,Vite 也支持使用 import() 实现动态加载。无需额外配置,Vite 会自动将动态导入的模块分割为单独的 chunk。

示例
javascript 复制代码
const MyComponent = () => import('./MyComponent.vue');

生成的 chunk 文件会以 hash 命名,如 MyComponent.abcd1234.js。


2. 手动配置代码分割

Vite 的代码分割可以通过 Rollup 的 output.manualChunks 选项自定义分包策略。

配置示例

在 vite.config.js 中配置:

javascript 复制代码
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor'; // 所有第三方库打包到 vendor.js
          }
          if (id.includes('src/components')) {
            return 'components'; // 把组件单独分包
          }
        },
      },
    },
  },
});
示例解释
  • node_modules:将所有第三方依赖打包到 vendor.js。
  • src/components:将组件打包到 components.js。

3. CSS 代码分割

Vite 会自动将 CSS 提取到单独的文件中。如果需要更多控制,可以使用 build.cssCodeSplit。

配置示例
javascript 复制代码
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    cssCodeSplit: true, // 启用 CSS 代码分割
  },
});

4. 按需加载库

对于大型第三方库,可以通过插件实现按需加载,如 lodash-es。

示例

安装 babel-plugin-lodash:

bash 复制代码
npm install lodash-es babel-plugin-lodash --save-dev

在 vite.config.js 中按需引入:

javascript 复制代码
import { defineConfig } from 'vite';
import lodash from 'lodash-es';

export default defineConfig({
  optimizeDeps: {
    include: ['lodash-es'],
  },
});

三、Webpack 与 Vite 的对比

特性 Webpack Vite
配置复杂度 配置较复杂,需要手动优化 配置简单,默认即可满足大多数需求
动态导入支持 import() 实现,支持 chunk 命名 import() 实现,无需额外配置
第三方库优化 需要手动配置 SplitChunksPlugin 默认优化 node_modules,可手动调整
CSS 分割 使用 MiniCssExtractPlugin 默认支持 CSS 分割,配置简单
编译性能 较慢,依赖构建缓存 快速,基于 ES 模块和预编译

四、最佳实践

  1. 按需加载组件和模块:

    • 在路由和组件中使用动态导入。
    • 将第三方库和大型模块分割到独立 chunk。
  2. 分离第三方依赖:

    • Webpack 中使用 SplitChunksPlugin,Vite 中使用 manualChunks。
  3. 优化输出文件:

    • 配置合理的 minSize 和 maxSize。
    • 通过 chunkFilename 或 manualChunks 自定义文件名,便于调试。
  4. 监控与调试:

    • 使用 webpack-bundle-analyzer 或 Vite 的插件分析打包后的文件大小和依赖关系,优化冗余模块。

通过合理配置代码分割,可以显著提升 Web 应用的性能,减少加载时间,并确保用户体验流畅!

相关推荐
excel7 小时前
研究 Vue 3 源码的收获
前端·vue.js
lerhxx9 小时前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
mantou13210 小时前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端
默_笙10 小时前
🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
前端·javascript
晚安日记wanna10 小时前
MySQL 回表为什么这么慢?5 种优化手段逐个拆解
数据库·mysql·性能优化
晚安日记wanna10 小时前
索引建了却不走?7 个失效原因逐层排查
数据库·mysql·性能优化
工作10年+,存储芯片行业10 小时前
Linux NVMe 中断排查与性能优化:CPU 亲和性
linux·运维·服务器·windows·性能优化·ssd·pcie
CappuccinoRose11 小时前
FormData数据处理
开发语言·前端·javascript·表单数据
飘尘12 小时前
SVG和Canvas,前端里的两支“画笔”,用的时候怎么选择?
前端·javascript·面试
Wang's Blog13 小时前
Java框架 SpringCloud 快速入门: Feign 的性能优化
java·spring cloud·性能优化