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)将部分状态存储在 localStoragesessionStorage,避免重复请求。
  • 删除无用的订阅:尽量减少监听过多状态变化的组件。

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. 根节点比较
    • 如果两个节点类型不同(例如 divspan),直接替换。
    • 如果节点类型相同,继续比较属性和子节点。
  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. 优化输出文件

    • 配置合理的 minSizemaxSize
    • 通过 chunkFilenamemanualChunks 自定义文件名,便于调试。
  4. 监控与调试

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

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

相关推荐
Amd79424 分钟前
PostgreSQL 数据库的启动与停止管理
postgresql·性能优化·数据库管理·故障处理·日常维护·启动数据库·停止数据库
LCG元1 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
哥谭居民00012 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~2 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
踢足球的,程序猿2 小时前
Android native+html5的混合开发
javascript
前端没钱2 小时前
探索 ES6 基础:开启 JavaScript 新篇章
前端·javascript·es6
PleaSure乐事3 小时前
使用Vue的props进行组件传递校验时出现 Extraneous non-props attributes的解决方案
vue.js
一条不想当淡水鱼的咸鱼4 小时前
taro中实现带有途径点的路径规划
javascript·react.js·taro
土豆炒马铃薯。4 小时前
【Vue】前端使用node.js对数据库直接进行CRUD操作
前端·javascript·vue.js·node.js·html5
温轻舟5 小时前
前端开发 -- 自动回复机器人【附完整源码】
前端·javascript·css·机器人·html·交互·温轻舟