设计系统性能优化完整指南

深入解析设计系统的性能优化策略,涵盖组件懒加载、样式优化、Tree Shaking、打包策略等核心技术

设计系统性能优化完整指南

设计系统作为应用的基础设施,其性能直接影响整体用户体验。本文将系统讲解设计系统的性能优化策略,帮助团队构建高性能的组件库。

设计系统性能挑战

常见的设计系统性能问题:

问题类型具体表现影响范围
包体积过大首次加载缓慢首屏性能、带宽消耗
组件渲染慢交互卡顿用户体验、响应性
样式冗余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 代码分割减少样式体积
组件懒加载优化首屏速度
虚拟列表大幅提升列表性能
依赖外部化减少打包体积
运行时优化提升交互响应
性能监控持续优化基础

总结

设计系统性能优化的核心策略:

  1. 包体积优化:通过 Tree Shaking、按需导入减少最终打包体积
  2. 加载优化:懒加载非关键组件,分割代码按需加载
  3. 样式优化:模块化样式、提取关键 CSS、减少运行时计算
  4. 运行时优化:虚拟化、缓存、事件委托减少计算开销
  5. 持续监控:建立性能基准,持续追踪优化效果

通过系统化的性能优化,可以让设计系统在提供丰富功能的同时保持卓越的性能表现。