一、代码层面优化

1. 数据层面

1.1 合理使用 v-show 和 v-if

1
2
3
4
5
6
7
8
9
// 频繁切换用 v-show
<template>
<div v-show="isShow">频繁切换的内容</div>
</template>

// 条件渲染用 v-if
<template>
<div v-if="isAdmin">管理员菜单</div>
</template>

1.2 使用 computed 代替 methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
// 不推荐
methods: {
getFullName() {
return this.firstName + ' ' + this.lastName;
}
},

// 推荐
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
}

1.3 避免重复数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
// 不推荐
list: [],
filterList: [], // 通过 list 过滤得到

// 推荐:使用计算属性
list: []
}
},
computed: {
filterList() {
return this.list.filter(item => item.visible);
}
}
}

2. 组件优化

2.1 路由懒加载

1
2
3
4
5
6
7
8
9
// router/index.js
const routes = [
{
path: '/user',
name: 'user',
// 使用动态导入
component: () => import('@/views/user/index.vue')
}
]

2.2 组件按需加载

1
2
3
4
5
6
// 不推荐
import { Button, Select, Table } from 'element-plus'

// 推荐
import Button from 'element-plus/lib/button'
import Select from 'element-plus/lib/select'

2.3 keep-alive 缓存组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<keep-alive :include="['UserList', 'UserInfo']">
<router-view></router-view>
</keep-alive>
</template>

<script>
export default {
name: 'App',
data() {
return {
// 需要缓存的组件名
cacheComponents: ['UserList', 'UserInfo']
}
}
}
</script>

3. 渲染优化

3.1 使用 v-once 处理静态内容

1
2
3
4
5
6
7
<template>
<!-- 静态内容只渲染一次 -->
<div v-once>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>

3.2 使用 v-memo 缓存模板

1
2
3
4
5
6
<template>
<div v-memo="[item.id, item.updated]">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
</div>
</template>

3.3 长列表优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<recycle-scroller
class="scroller"
:items="items"
:item-size="32"
>
<template #default="{ item }">
<div class="user-item">
{{ item.name }}
</div>
</template>
</recycle-scroller>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default {
components: {
RecycleScroller
},
data() {
return {
items: Array.from({ length: 10000 }).map((_, i) => ({
id: i,
name: `User ${i}`
}))
}
}
}
</script>

二、打包优化

1. 代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
}
}

2. 压缩资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vue.config.js
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
configureWebpack: {
plugins: [
new CompressionPlugin({
test: /\.(js|css|html)$/,
threshold: 10240, // 10KB
deleteOriginalAssets: false
})
]
}
}

3. CDN 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vue.config.js
module.exports = {
chainWebpack: config => {
config.externals({
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
})
}
}

// public/index.html
<head>
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.31"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@4.0.14"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex@4.0.2"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.26.1"></script>
</head>

三、运行时优化

1. 事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<!-- 不推荐 -->
<div @scroll="handleScroll">
<!-- 内容 -->
</div>

<!-- 推荐:使用节流 -->
<div @scroll="throttleScroll">
<!-- 内容 -->
</div>
</template>

<script>
import { throttle } from 'lodash-es'

export default {
created() {
this.throttleScroll = throttle(this.handleScroll, 200)
},
methods: {
handleScroll(e) {
// 处理滚动事件
}
},
beforeDestroy() {
// 清除节流函数
this.throttleScroll.cancel()
}
}
</script>

2. 大数据渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="list">
<div v-for="chunk in chunks" :key="chunk[0].id">
<div v-for="item in chunk" :key="item.id">
{{ item.name }}
</div>
</div>
</div>
</template>

<script>
export default {
data() {
return {
list: [],
chunkSize: 100
}
},
computed: {
chunks() {
const chunks = []
for (let i = 0; i < this.list.length; i += this.chunkSize) {
chunks.push(this.list.slice(i, i + this.chunkSize))
}
return chunks
}
},
methods: {
async loadData() {
const data = await fetchLargeData()
// 使用 nextTick 分批渲染
this.$nextTick(() => {
this.list = data
})
}
}
}
</script>

3. 内存优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
export default {
data() {
return {
timer: null,
observers: {
scroll: null,
intersection: null,
resize: null,
mutation: null
},
domRefs: {
listContainer: null,
lazyImages: []
}
}
},
mounted() {
// 1. 初始化 DOM 引用
this.domRefs.listContainer = this.$refs.listContainer
this.domRefs.lazyImages = this.$refs.lazyImages

// 2. 创建交叉观察者 - 用于图片懒加载
this.observers.intersection = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
// 图片加载后取消观察
this.observers.intersection.unobserve(img)
}
})
},
{
root: this.domRefs.listContainer,
rootMargin: '50px',
threshold: 0.1
}
)

// 3. 创建滚动观察者 - 用于无限滚动
this.observers.scroll = new IntersectionObserver(
(entries) => {
const trigger = entries[0]
if (trigger.isIntersecting) {
this.loadMoreData()
}
},
{
root: null,
rootMargin: '100px',
threshold: 0
}
)

// 4. 创建 ResizeObserver - 用于响应容器大小变化
this.observers.resize = new ResizeObserver(
this.throttle((entries) => {
const containerSize = entries[0].contentRect
this.handleContainerResize(containerSize)
}, 200)
)

// 5. 创建 MutationObserver - 监听 DOM 变化
this.observers.mutation = new MutationObserver(
this.debounce((mutations) => {
this.handleDomChanges(mutations)
}, 200)
)

// 启动观察
this.startObserving()
},
methods: {
// 开始所有观察
startObserving() {
// 1. 观察懒加载图片
this.domRefs.lazyImages.forEach(img => {
this.observers.intersection.observe(img)
})

// 2. 观察滚动触发器
const trigger = this.$refs.scrollTrigger
if (trigger) {
this.observers.scroll.observe(trigger)
}

// 3. 观察容器大小
if (this.domRefs.listContainer) {
this.observers.resize.observe(this.domRefs.listContainer)
}

// 4. 观察 DOM 变化
this.observers.mutation.observe(this.domRefs.listContainer, {
childList: true,
subtree: true,
attributes: true,
characterData: true
})
},

// 处理容器大小变化
handleContainerResize(size) {
// 根据新的容器大小调整布局
this.updateLayout(size)
},

// 处理 DOM 变化
handleDomChanges(mutations) {
// 处理 DOM 变化,例如更新虚拟滚动的计算
this.updateVirtualScroll()
},

// 加载更多数据
async loadMoreData() {
try {
const newData = await this.fetchData()
this.list.push(...newData)
} catch (error) {
console.error('Failed to load more data:', error)
}
},

// 节流函数
throttle(fn, delay) {
let timer = null
return function(...args) {
if (timer) return
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
},

// 防抖函数
debounce(fn, delay) {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
},
beforeDestroy() {
// 清除定时器
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}

// 断开所有观察者
Object.values(this.observers).forEach(observer => {
if (observer) {
observer.disconnect()
observer = null
}
})
}
}

四、实践建议

1. 开发阶段

  • 使用开发者工具的 Performance 面板分析性能
  • 使用 Vue DevTools 检查组件渲染情况
  • 编写高质量的代码,遵循最佳实践

2. 构建阶段

  • 合理配置 webpack 优化项
  • 分析并优化打包体积
  • 使用动态导入和按需加载

3. 部署阶段

  • 使用 CDN 加速资源加载
  • 开启 Gzip 压缩
  • 配置合理的缓存策略

参考文献