当前位置: 首页 > article >正文

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

Vue2/Vue3 性能优化指南

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

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

3. 懒加载与代码分割
  • 懒加载
    • 将路由组件按需加载,减少初始加载体积。
    const About = () => import('@/components/About.vue');
    
  • 代码分割
    • 使用 Webpack 或 Vite 的动态导入功能,将代码拆分成多个块,按需加载。
    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. 使用异步组件
  • 异步组件可以延迟加载,直到需要时再加载。
const AsyncComponent = defineAsyncComponent(() =>
    import('./components/MyComponent.vue')
);
  • 使用场景
    • 大型组件。
    • 不常用的对话框、模态框等功能组件。
  • 配合加载状态和超时:
    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 错乱。

示例:

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

五、通俗易懂的示例

假设有一段列表:

<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. 动态导入组件

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

// 普通导入
import MyComponent from '@/components/MyComponent.vue';

// 动态导入
const MyComponent = () => import('@/components/MyComponent.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 提供与动态导入结合的懒加载功能。

配置路由
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 属性,可以用来存储自定义数据。例如:

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

meta 字段的常用场景:

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

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

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

示例:动态加载资源
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 来检查是否需要权限:

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

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

4. 动态设置页面标题

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

router.afterEach((to) => {
  if (to.meta.title) {
    document.title = to.meta.title;
  }
});

路由配置:

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

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

路由配置
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: '登录',
    },
  },
];
导航守卫
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,在需要时加载。

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

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

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

2. 使用 SplitChunksPlugin 配置代码分割

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

配置示例

webpack.config.js 中进行配置:

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 名称。

const About = () => import(/* webpackChunkName: "about" */ './About.vue');

生成的文件名称为 about.js


二、Vite 中代码分割

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


1. 动态导入

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

示例
const MyComponent = () => import('./MyComponent.vue');

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


2. 手动配置代码分割

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

配置示例

vite.config.js 中配置:

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

配置示例
import { defineConfig } from 'vite';

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

4. 按需加载库

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

示例

安装 babel-plugin-lodash

npm install lodash-es babel-plugin-lodash --save-dev

vite.config.js 中按需引入:

import { defineConfig } from 'vite';
import lodash from 'lodash-es';

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

三、Webpack 与 Vite 的对比

特性WebpackVite
配置复杂度配置较复杂,需要手动优化配置简单,默认即可满足大多数需求
动态导入支持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 应用的性能,减少加载时间,并确保用户体验流畅!


http://www.kler.cn/a/452776.html

相关文章:

  • 5.近实时数仓数据更新和ID 管理上的优化方案
  • 【星海随笔】删除ceph
  • 频繁拿下定点,华玉高性能中间件迈入商业化新阶段
  • linux RCU调优
  • 纯Dart Flutter库适配HarmonyOS
  • Centos下的OpenSSH服务器和客户端
  • SSH无法启动问题:OpenSSL version mismatch. Built against 30000070, you have 30200020
  • np.triu:NumPy中提取上三角矩阵的利器
  • APP投放的归因框架设计
  • 枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
  • python闭包详解
  • 2024年第十一期 | CCF ODC《开源战略动态月报》
  • 使用Python开发高级游戏:实现一个3D射击游戏
  • UE5仿漫威争锋灵蝶冲刺技能
  • ElasticSearch 的工作原理
  • Springboot + vue3 实现大文件上传方案:秒传、断点续传、分片上传、前端异步上传
  • 医药垃圾分类管理系统|Java|SSM|JSP|
  • Intent--组件通信
  • 华为认证考试模拟题测试题库(含答案解析)
  • STM32-笔记10-手写延时函数(SysTick)
  • nacos-服务发现注册
  • 【Linux】shell脚本:查找可执行文件和批量创建多个账户
  • LabVIEW实现NB-IoT通信
  • Pillow库
  • arXiv-2024 | STMR:语义拓扑度量表示引导的大模型推理无人机视觉语言导航
  • Vuex 的使用和原理详解