设计系统性能优化完整指南
设计系统作为应用的基础设施,其性能直接影响整体用户体验。本文将系统讲解设计系统的性能优化策略,帮助团队构建高性能的组件库。
设计系统性能挑战
常见的设计系统性能问题:
| 问题类型 | 具体表现 | 影响范围 |
|---|---|---|
| 包体积过大 | 首次加载缓慢 | 首屏性能、带宽消耗 |
| 组件渲染慢 | 交互卡顿 | 用户体验、响应性 |
| 样式冗余 | CSS 文件过大 | 解析时间、内存占用 |
| 依赖膨胀 | node_modules 庞大 | 安装时间、构建效率 |
| 运行时开销 | 动态计算过多 | CPU 占用、电池消耗 |
Tree Shaking 优化
Tree Shaking 是减小包体积最有效的方法,确保只打包实际使用的代码。
包结构设计
packages/ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── index.ts
│ │ │ ├── Button.vue
│ │ │ └── Button.css
│ │ ├── Input/
│ │ │ └── ...
│ │ └── index.ts # 统一导出
│ ├── composables/
│ │ ├── useDialog.ts
│ │ └── index.ts
│ └── index.ts # 主入口
├── dist/
│ ├── es/ # ES Module 格式
│ │ ├── components/
│ │ │ ├── Button/
│ │ │ │ └── index.mjs
│ │ │ └── ...
│ │ └── index.mjs
│ ├── lib/ # CommonJS 格式
│ └── types/ # 类型声明
└── package.json
package.json 配置
{
"name": "@company/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/lib/index.cjs",
"module": "./dist/es/index.mjs",
"types": "./dist/types/index.d.ts",
"sideEffects": [
"**/*.css",
"**/*.scss"
],
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/es/index.mjs",
"require": "./dist/lib/index.cjs"
},
"./components/*": {
"types": "./dist/types/components/*/index.d.ts",
"import": "./dist/es/components/*/index.mjs",
"require": "./dist/lib/components/*/index.cjs"
},
"./composables": {
"types": "./dist/types/composables/index.d.ts",
"import": "./dist/es/composables/index.mjs"
},
"./styles/*": "./dist/styles/*"
}
}
组件导出方式
// src/components/index.ts
// 使用具名导出而非默认导出,便于 Tree Shaking
// 推荐:具名导出
export { Button } from './Button'
export { Input } from './Input'
export { Select } from './Select'
export { Modal } from './Modal'
export { Table } from './Table'
// 不推荐:对象导出(会阻止 Tree Shaking)
// export default { Button, Input, Select, Modal, Table }
按需导入方式
// 方式一:直接导入具体组件(最优)
import { Button } from '@company/ui/components/Button'
import { Input } from '@company/ui/components/Input'
// 方式二:从主入口导入(依赖 Tree Shaking)
import { Button, Input } from '@company/ui'
// 方式三:使用自动导入插件(无需手动导入)
// 配置后直接在模板中使用组件
自动导入配置
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { CompanyUIResolver } from '@company/ui/resolver'
export default defineConfig({
plugins: [
Components({
resolvers: [
CompanyUIResolver({
importStyle: 'css' // 自动导入样式
})
]
})
]
})
// packages/ui/resolver.ts
// 自定义解析器实现
export function CompanyUIResolver(options = {}) {
return {
type: 'component',
resolve: (name: string) => {
// 匹配组件名前缀
if (name.startsWith('Cu')) {
const componentName = name.slice(2) // 去掉前缀
return {
name: componentName,
from: `@company/ui/components/${componentName}`,
sideEffects: options.importStyle
? `@company/ui/styles/${componentName}.css`
: undefined
}
}
}
}
}
组件懒加载策略
动态导入组件
// 异步组件定义
import { defineAsyncComponent } from 'vue'
// 基础用法
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal/Modal.vue')
)
// 带加载状态和错误处理
const AsyncChart = defineAsyncComponent({
loader: () => import('./components/Chart/Chart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorFallback,
delay: 200, // 延迟显示 loading
timeout: 10000, // 超时时间
suspensible: true // 支持 Suspense
})
按路由分割组件
// router/index.ts
const routes = [
{
path: '/dashboard',
component: () => import('@/layouts/DashboardLayout.vue'),
children: [
{
path: 'analytics',
// 按路由懒加载页面组件
component: () => import('@/pages/Analytics.vue')
},
{
path: 'reports',
component: () => import('@/pages/Reports.vue')
}
]
}
]
按可见性懒加载
<template>
<div ref="containerRef">
<component
v-if="isVisible"
:is="AsyncComponent"
v-bind="$attrs"
/>
<div v-else class="placeholder" :style="placeholderStyle" />
</div>
</template>
<script setup lang="ts">
import { useIntersectionObserver } from '@vueuse/core'
const props = defineProps<{
loader: () => Promise<any>
rootMargin?: string
threshold?: number
placeholderHeight?: string
}>()
const containerRef = ref<HTMLElement>()
const isVisible = ref(false)
const AsyncComponent = shallowRef(null)
const { stop } = useIntersectionObserver(
containerRef,
([{ isIntersecting }]) => {
if (isIntersecting) {
isVisible.value = true
// 开始加载组件
props.loader().then(module => {
AsyncComponent.value = module.default
})
stop() // 停止观察
}
},
{
rootMargin: props.rootMargin || '100px',
threshold: props.threshold || 0
}
)
const placeholderStyle = computed(() => ({
height: props.placeholderHeight || '200px'
}))
</script>
CSS 性能优化
样式模块化
// 构建配置:按组件分割样式
// vite.config.ts
export default defineConfig({
build: {
cssCodeSplit: true, // 启用 CSS 代码分割
rollupOptions: {
output: {
// 每个组件生成独立的 CSS 文件
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'styles/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
}
}
}
}
})
CSS 变量优化
/* 避免过度使用 CSS 变量计算 */
/* 不推荐:运行时计算 */
.button {
padding: calc(var(--spacing-base) * 2) calc(var(--spacing-base) * 4);
font-size: calc(var(--font-size-base) * 1.125);
}
/* 推荐:预定义具体值 */
.button {
padding: var(--button-padding-y) var(--button-padding-x);
font-size: var(--button-font-size);
}
/* 在根级别定义计算值 */
:root {
--spacing-base: 4px;
--button-padding-y: 8px; /* spacing-base * 2 */
--button-padding-x: 16px; /* spacing-base * 4 */
--button-font-size: 14px;
}
关键 CSS 提取
// 提取首屏关键 CSS
import { createCriticalCSS } from '@company/ui/build'
// 构建时提取关键样式
async function extractCriticalCSS() {
const critical = await createCriticalCSS({
src: 'dist/index.html',
css: ['dist/styles/main.css'],
width: 1300,
height: 900,
// 关键组件列表
criticalComponents: [
'Button',
'Input',
'Layout',
'Header',
'Navigation'
]
})
return critical
}
样式作用域优化
<template>
<button class="btn" :class="variantClass">
<slot />
</button>
</template>
<style scoped>
/* 使用 scoped 避免全局污染,但注意其实现方式 */
.btn {
/* 基础样式 */
}
</style>
<style>
/*
* 性能提示:
* scoped 会为每个元素添加 data 属性
* 对于频繁创建销毁的组件,考虑使用 CSS Modules
*/
</style>
<template>
<button :class="$style.btn">
<slot />
</button>
</template>
<style module>
/* CSS Modules:类名在编译时替换,无运行时开销 */
.btn {
padding: 8px 16px;
border-radius: 4px;
}
</style>
运行时性能优化
组件渲染优化
<template>
<!-- 使用 v-memo 缓存静态内容 -->
<div v-memo="[item.id, item.status]">
<ExpensiveComponent :data="item" />
</div>
</template>
<script setup lang="ts">
import { computed, shallowRef, markRaw } from 'vue'
// 使用 shallowRef 避免深度响应
const tableData = shallowRef<TableRow[]>([])
// 非响应式对象使用 markRaw
const chartConfig = markRaw({
type: 'line',
options: { /* 大量配置 */ }
})
// 避免不必要的计算属性重新计算
const filteredData = computed(() => {
// 缓存计算结果
return expensiveFilter(tableData.value)
})
</script>
虚拟化长列表
<template>
<VirtualList
:items="items"
:item-height="48"
:buffer-size="5"
key-field="id"
>
<template #default="{ item, index }">
<ListItem :data="item" :index="index" />
</template>
</VirtualList>
</template>
<script setup lang="ts">
// 虚拟列表组件实现核心逻辑
function useVirtualList<T>(options: VirtualListOptions<T>) {
const { items, itemHeight, containerHeight, bufferSize = 3 } = options
const scrollTop = ref(0)
// 计算可见范围
const visibleRange = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight)
const visibleCount = Math.ceil(containerHeight.value / itemHeight)
return {
start: Math.max(0, start - bufferSize),
end: Math.min(items.value.length, start + visibleCount + bufferSize)
}
})
// 只渲染可见项
const visibleItems = computed(() => {
const { start, end } = visibleRange.value
return items.value.slice(start, end).map((item, index) => ({
item,
index: start + index
}))
})
// 计算偏移量
const offsetY = computed(() => visibleRange.value.start * itemHeight)
// 计算总高度
const totalHeight = computed(() => items.value.length * itemHeight)
return { visibleItems, offsetY, totalHeight, scrollTop }
}
</script>
防抖与节流
// composables/useDebounce.ts
export function useDebounce<T extends (...args: any[]) => any>(
fn: T,
delay: number = 300
) {
let timeoutId: ReturnType<typeof setTimeout>
const debouncedFn = (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
const cancel = () => clearTimeout(timeoutId)
onUnmounted(cancel)
return { debouncedFn, cancel }
}
// 使用示例
const { debouncedFn: debouncedSearch } = useDebounce(
(query: string) => searchAPI(query),
300
)
事件委托
<template>
<!-- 使用事件委托处理大量子元素的事件 -->
<div class="list" @click="handleClick">
<div
v-for="item in items"
:key="item.id"
:data-id="item.id"
class="list-item"
>
{{ item.name }}
</div>
</div>
</template>
<script setup lang="ts">
function handleClick(event: MouseEvent) {
const target = event.target as HTMLElement
const listItem = target.closest('.list-item')
if (listItem) {
const id = listItem.dataset.id
// 处理点击事件
handleItemClick(id)
}
}
</script>
打包优化策略
Rollup 配置优化
// rollup.config.js
import { defineConfig } from 'rollup'
import vue from '@vitejs/plugin-vue'
import { terser } from 'rollup-plugin-terser'
import analyze from 'rollup-plugin-analyzer'
export default defineConfig({
input: {
index: 'src/index.ts',
// 按组件分割入口
...getComponentEntries('src/components')
},
output: [
{
dir: 'dist/es',
format: 'esm',
preserveModules: true, // 保留模块结构
preserveModulesRoot: 'src',
entryFileNames: '[name].mjs',
exports: 'named'
},
{
dir: 'dist/lib',
format: 'cjs',
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: '[name].cjs',
exports: 'named'
}
],
external: [
'vue',
/^@vue\//,
// 外部化所有生产依赖
...Object.keys(pkg.peerDependencies || {})
],
plugins: [
vue(),
terser({
compress: {
drop_console: true,
drop_debugger: true
}
}),
analyze({ summaryOnly: true })
]
})
// 自动获取组件入口
function getComponentEntries(dir: string) {
const components = fs.readdirSync(dir)
return components.reduce((entries, component) => {
const entryPath = path.join(dir, component, 'index.ts')
if (fs.existsSync(entryPath)) {
entries[`components/${component}/index`] = entryPath
}
return entries
}, {})
}
依赖外部化
// 将大型依赖外部化,由消费者自行引入
const externalDeps = [
'vue',
'vue-router',
'pinia',
'@vueuse/core',
// 图表库等大型依赖
'echarts',
'chart.js'
]
export default defineConfig({
external: (id) => {
return externalDeps.some(dep => id === dep || id.startsWith(`${dep}/`))
}
})
代码分析与优化
# 使用 source-map-explorer 分析包体积
npx source-map-explorer dist/es/index.mjs
# 使用 bundlephobia 查看依赖大小
# https://bundlephobia.com/package/@company/ui
性能监控与测量
组件性能指标
// 性能监控 Hook
export function useComponentPerformance(componentName: string) {
const mountTime = ref(0)
const updateCount = ref(0)
const lastUpdateTime = ref(0)
onMounted(() => {
mountTime.value = performance.now()
// 记录挂载时间
if (import.meta.env.DEV) {
console.log(`[Perf] ${componentName} mounted in ${mountTime.value.toFixed(2)}ms`)
}
})
onUpdated(() => {
updateCount.value++
lastUpdateTime.value = performance.now()
// 检测频繁更新
if (import.meta.env.DEV && updateCount.value > 10) {
console.warn(`[Perf] ${componentName} updated ${updateCount.value} times`)
}
})
return { mountTime, updateCount, lastUpdateTime }
}
性能基准测试
// tests/performance/Button.bench.ts
import { describe, bench } from 'vitest'
import { mount } from '@vue/test-utils'
import { Button } from '@company/ui'
describe('Button Performance', () => {
bench('mount Button', () => {
const wrapper = mount(Button, {
props: { variant: 'primary' }
})
wrapper.unmount()
})
bench('mount 100 Buttons', () => {
const wrappers = Array.from({ length: 100 }, () =>
mount(Button, { props: { variant: 'primary' } })
)
wrappers.forEach(w => w.unmount())
})
bench('update Button props', async () => {
const wrapper = mount(Button, {
props: { variant: 'primary', loading: false }
})
for (let i = 0; i < 100; i++) {
await wrapper.setProps({ loading: i % 2 === 0 })
}
wrapper.unmount()
})
})
性能优化清单
| 优化项 | 优先级 | 预期收益 | 实施难度 |
|---|---|---|---|
| Tree Shaking 配置 | 高 | 减少 30-60% 体积 | 低 |
| 按需导入 | 高 | 减少首屏加载 | 低 |
| CSS 代码分割 | 中 | 减少样式体积 | 低 |
| 组件懒加载 | 中 | 优化首屏速度 | 中 |
| 虚拟列表 | 高 | 大幅提升列表性能 | 中 |
| 依赖外部化 | 中 | 减少打包体积 | 低 |
| 运行时优化 | 中 | 提升交互响应 | 高 |
| 性能监控 | 低 | 持续优化基础 | 中 |
总结
设计系统性能优化的核心策略:
- 包体积优化:通过 Tree Shaking、按需导入减少最终打包体积
- 加载优化:懒加载非关键组件,分割代码按需加载
- 样式优化:模块化样式、提取关键 CSS、减少运行时计算
- 运行时优化:虚拟化、缓存、事件委托减少计算开销
- 持续监控:建立性能基准,持续追踪优化效果
通过系统化的性能优化,可以让设计系统在提供丰富功能的同时保持卓越的性能表现。


