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
)将部分状态存储在localStorage
或sessionStorage
,避免重复请求。
- 使用插件(如
- 删除无用的订阅:尽量减少监听过多状态变化的组件。
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 的状态变化并高效地更新界面。
核心思想:
- 使用 JavaScript 对象(虚拟节点)表示 DOM 结构。
- 通过比较新旧虚拟 DOM(diff 算法)找到最小的变化集。
- 最小化对真实 DOM 的操作以提升性能。
二、Vue2 的虚拟 DOM 与 diff 算法
1. 虚拟 DOM 的基本工作原理
- 初次渲染时:
- 将模板编译成渲染函数,渲染函数生成虚拟 DOM。
- 虚拟 DOM 被渲染成真实 DOM。
- 数据更新时:
- 渲染函数重新生成新虚拟 DOM。
- 对比新旧虚拟 DOM,生成需要更新的差异(patch)。
- 最后根据差异最小化地操作真实 DOM。
2. Vue2 的 diff 算法
Vue2 的 diff 算法基于以下特性设计:
- 只比较同级节点:不同层级的节点直接删掉重建。
- 递归比较子节点:只处理具体需要更新的节点,跳过未变更的部分。
过程:
- 根节点比较:
- 如果两个节点类型不同(例如
div
和span
),直接替换。 - 如果节点类型相同,继续比较属性和子节点。
- 如果两个节点类型不同(例如
- 属性比较:
- 遍历新旧节点的属性,添加、更新、或删除不一致的属性。
- 子节点比较:
- 对子节点递归执行 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 的核心优化
- 静态提升:
- Vue3 会分析哪些节点是静态的,将它们在编译时提升到渲染函数之外,避免每次重新渲染。
- 例如,静态文本节点只会被创建一次。
- 缓存事件处理函数:
- 默认缓存事件处理函数,避免因更新父组件而重新绑定事件。
- Block Tree 优化:
- Vue3 会在模板中生成“Block Tree”,每个 Block 只关注动态节点,跳过静态节点的对比。
- 模板编译优化:
- 编译阶段将模板转化为更加高效的渲染函数,减少运行时开销。
3. Vue3 的 diff 算法
Vue3 的 diff 算法相较 Vue2 进行了显著优化:
- 动态节点追踪:
- Vue3 的编译器会标记动态节点,只对动态节点执行 diff,静态节点被直接跳过。
- 稳定的列表更新:
- 对于
v-for
列表,Vue3 会尽可能复用 DOM 节点,避免删除重建。
- 对于
过程:
- 头尾双指针:
- 对新旧子节点使用双指针算法,分别从头尾开始比较,尽可能减少移动节点的次数。
- 快速定位节点:
- 如果发现新列表中间部分的节点顺序发生变化,会构建索引表以快速找到新节点的位置。
- 减少 DOM 操作:
- 尽量合并多次修改,使用批量 DOM 更新策略。
四、Vue2 和 Vue3 的对比
特性 | Vue2 | Vue3 |
---|---|---|
静态节点处理 | 无优化,所有节点都认为可能动态 | 静态节点提升,跳过对比 |
动态节点追踪 | 每次对所有节点执行 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 通常通过以下两种方式实现代码分割:
- 动态导入(Dynamic Import):按需加载组件或资源。
- 懒加载(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);
});
}
五、总结
- 代码分割:
- 通过动态导入 (
import()
) 和懒加载 (component: () => import(...)
) 将代码按需加载,减少初始体积。
- 通过动态导入 (
meta
字段的作用:- 可以用于权限验证、动态加载资源和设置页面标题等场景。
- 导航守卫:
- 配合
meta
字段,在路由切换时动态执行逻辑(如加载资源、验证权限)。
- 配合
- 最佳实践:
- 配置合理的路由分块。
- 使用
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 的对比
特性 | Webpack | Vite |
---|---|---|
配置复杂度 | 配置较复杂,需要手动优化 | 配置简单,默认即可满足大多数需求 |
动态导入支持 | import() 实现,支持 chunk 命名 | import() 实现,无需额外配置 |
第三方库优化 | 需要手动配置 SplitChunksPlugin | 默认优化 node_modules ,可手动调整 |
CSS 分割 | 使用 MiniCssExtractPlugin | 默认支持 CSS 分割,配置简单 |
编译性能 | 较慢,依赖构建缓存 | 快速,基于 ES 模块和预编译 |
四、最佳实践
-
按需加载组件和模块:
- 在路由和组件中使用动态导入。
- 将第三方库和大型模块分割到独立
chunk
。
-
分离第三方依赖:
- Webpack 中使用
SplitChunksPlugin
,Vite 中使用manualChunks
。
- Webpack 中使用
-
优化输出文件:
- 配置合理的
minSize
和maxSize
。 - 通过
chunkFilename
或manualChunks
自定义文件名,便于调试。
- 配置合理的
-
监控与调试:
- 使用
webpack-bundle-analyzer
或 Vite 的插件分析打包后的文件大小和依赖关系,优化冗余模块。
- 使用
通过合理配置代码分割,可以显著提升 Web 应用的性能,减少加载时间,并确保用户体验流畅!