性能问题

了解如何诊断和解决 @feoe/fs-router 相关的性能问题。

性能诊断

识别性能瓶颈

  1. 使用浏览器开发者工具

    1// 在控制台中测量路由切换时间
    2console.time('route-change')
    3navigate('/new-route')
    4// 在新页面加载完成后
    5console.timeEnd('route-change')
  2. React Profiler

    1import { Profiler } from 'react'
    2
    3function onRenderCallback(id, phase, actualDuration) {
    4  console.log('组件渲染时间:', { id, phase, actualDuration })
    5}
    6
    7<Profiler id="App" onRender={onRenderCallback}>
    8  <RouterProvider router={router} />
    9</Profiler>
  3. 网络面板分析

    • 检查 JavaScript bundle 大小
    • 分析代码分割效果
    • 监控资源加载时间

性能指标监控

1// 自定义性能监控 Hook
2function usePerformanceMonitor() {
3  const location = useLocation()
4  
5  useEffect(() => {
6    const startTime = performance.now()
7    
8    return () => {
9      const endTime = performance.now()
10      const duration = endTime - startTime
11      
12      // 记录页面停留时间
13      console.log(`页面 ${location.pathname} 停留时间: ${duration}ms`)
14      
15      // 发送到分析服务
16      analytics.track('page_duration', {
17        path: location.pathname,
18        duration
19      })
20    }
21  }, [location])
22}

首次加载优化

减少初始 Bundle 大小

  1. 启用代码分割

    1// vite.config.ts
    2export default defineConfig({
    3  plugins: [
    4    FileBasedRouterVite({
    5      typeGenerateOptions: {
    6        routesTypeFile: 'src/routes-type.ts',
    7        generateRouteParams: true,
    8        generateLoaderTypes: true,
    9        routesDirectories: []
    10      }
    11    })
    12  ],
    13  build: {
    14    rollupOptions: {
    15      output: {
    16        manualChunks: {
    17          vendor: ['react', 'react-dom', 'react-router-dom'],
    18          ui: ['@mui/material', 'antd']  // UI 库单独打包
    19        }
    20      }
    21    }
    22  }
    23})
  2. 路由级别的代码分割

    1// 自动生成的懒加载组件
    2const HomePage = lazy(() => import('./routes/page'))
    3const AboutPage = lazy(() => import('./routes/about/page'))
    4const UserPage = lazy(() => import('./routes/user/[id]/page'))
  3. 预加载关键路由

    1// 在应用启动时预加载重要路由
    2const preloadRoutes = [
    3  () => import('./routes/page'),
    4  () => import('./routes/about/page')
    5]
    6
    7// 在空闲时间预加载
    8if ('requestIdleCallback' in window) {
    9  requestIdleCallback(() => {
    10    preloadRoutes.forEach(load => load())
    11  })
    12}

优化资源加载

  1. 使用 Resource Hints

    1<!-- 在 index.html 中添加 -->
    2<link rel="preload" href="/src/routes.tsx" as="script">
    3<link rel="prefetch" href="/src/routes/about/page.tsx" as="script">
  2. 配置 Webpack/Vite 预加载

    1// vite.config.ts
    2export default defineConfig({
    3  build: {
    4    rollupOptions: {
    5      output: {
    6        chunkFileNames: (chunkInfo) => {
    7          // 为路由 chunk 添加预加载提示
    8          if (chunkInfo.name?.includes('routes')) {
    9            return 'routes/[name]-[hash].js'
    10          }
    11          return '[name]-[hash].js'
    12        }
    13      }
    14    }
    15  }
    16})

路由切换优化

减少切换延迟

  1. 智能预加载

    1// 鼠标悬停时预加载
    2function PreloadLink({ to, children, ...props }) {
    3  const [isHovered, setIsHovered] = useState(false)
    4  
    5  useEffect(() => {
    6    if (isHovered) {
    7      // 预加载目标路由
    8      import(`./routes${to}/page`)
    9    }
    10  }, [isHovered, to])
    11  
    12  return (
    13    <Link
    14      to={to}
    15      onMouseEnter={() => setIsHovered(true)}
    16      {...props}
    17    >
    18      {children}
    19    </Link>
    20  )
    21}
  2. 视口预加载

    1// 当链接进入视口时预加载
    2function useIntersectionPreload(ref, importFn) {
    3  useEffect(() => {
    4    const observer = new IntersectionObserver(
    5      ([entry]) => {
    6        if (entry.isIntersecting) {
    7          importFn()
    8          observer.disconnect()
    9        }
    10      },
    11      { threshold: 0.1 }
    12    )
    13    
    14    if (ref.current) {
    15      observer.observe(ref.current)
    16    }
    17    
    18    return () => observer.disconnect()
    19  }, [importFn])
    20}

优化 Suspense 边界

  1. 分层 Suspense

    1function App() {
    2  return (
    3    <Router>
    4      <Suspense fallback={<AppSkeleton />}>
    5        <Layout>
    6          <Suspense fallback={<PageSkeleton />}>
    7            <Routes />
    8          </Suspense>
    9        </Layout>
    10      </Suspense>
    11    </Router>
    12  )
    13}
  2. 智能 Loading 状态

    1function SmartSuspense({ children, fallback }) {
    2  const [showFallback, setShowFallback] = useState(false)
    3  
    4  useEffect(() => {
    5    const timer = setTimeout(() => {
    6      setShowFallback(true)
    7    }, 200) // 200ms 后才显示 loading
    8    
    9    return () => clearTimeout(timer)
    10  }, [])
    11  
    12  return (
    13    <Suspense fallback={showFallback ? fallback : null}>
    14      {children}
    15    </Suspense>
    16  )
    17}

数据加载优化

Loader 性能优化

  1. 并行数据加载

    1export async function loader({ params }) {
    2  // 并行加载多个数据源
    3  const [user, posts, comments] = await Promise.all([
    4    fetchUser(params.id),
    5    fetchUserPosts(params.id),
    6    fetchUserComments(params.id)
    7  ])
    8  
    9  return { user, posts, comments }
    10}
  2. 数据缓存策略

    1// 简单的内存缓存
    2const cache = new Map()
    3const CACHE_TTL = 5 * 60 * 1000 // 5分钟
    4
    5export async function loader({ params }) {
    6  const cacheKey = `user-${params.id}`
    7  const cached = cache.get(cacheKey)
    8  
    9  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    10    return cached.data
    11  }
    12  
    13  const user = await fetchUser(params.id)
    14  cache.set(cacheKey, {
    15    data: { user },
    16    timestamp: Date.now()
    17  })
    18  
    19  return { user }
    20}
  3. 增量数据加载

    1export async function loader({ params, request }) {
    2  const url = new URL(request.url)
    3  const page = parseInt(url.searchParams.get('page') || '1')
    4  const pageSize = 20
    5  
    6  // 只加载当前页数据
    7  const posts = await fetchPosts({
    8    userId: params.id,
    9    page,
    10    pageSize
    11  })
    12  
    13  return { posts, page, hasMore: posts.length === pageSize }
    14}

避免数据重复加载

  1. 使用 React Query 集成
    1import { useQuery } from '@tanstack/react-query'
    2
    3export async function loader({ params }) {
    4  // 在 loader 中预填充查询缓存
    5  const queryClient = getQueryClient()
    6  
    7  await queryClient.prefetchQuery({
    8    queryKey: ['user', params.id],
    9    queryFn: () => fetchUser(params.id)
    10  })
    11  
    12  return null // 数据通过 React Query 管理
    13}
    14
    15export default function UserPage() {
    16  const { id } = useParams()
    17  const { data: user } = useQuery({
    18    queryKey: ['user', id],
    19    queryFn: () => fetchUser(id)
    20  })
    21  
    22  return <div>{user?.name}</div>
    23}

内存优化

避免内存泄漏

  1. 清理事件监听器

    1export default function UserPage() {
    2  useEffect(() => {
    3    const handleResize = () => {
    4      // 处理窗口大小变化
    5    }
    6    
    7    window.addEventListener('resize', handleResize)
    8    
    9    return () => {
    10      window.removeEventListener('resize', handleResize)
    11    }
    12  }, [])
    13}
  2. 清理定时器

    1export default function AutoRefreshPage() {
    2  useEffect(() => {
    3    const interval = setInterval(() => {
    4      // 自动刷新数据
    5    }, 30000)
    6    
    7    return () => clearInterval(interval)
    8  }, [])
    9}
  3. 取消未完成的请求

    1export async function loader({ params, signal }) {
    2  try {
    3    const user = await fetchUser(params.id, { signal })
    4    return { user }
    5  } catch (error) {
    6    if (error.name === 'AbortError') {
    7      // 请求被取消,不需要处理
    8      return null
    9    }
    10    throw error
    11  }
    12}

组件优化

  1. 使用 React.memo

    1const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
    2  // 复杂的渲染逻辑
    3  return <div>{/* 渲染内容 */}</div>
    4})
  2. 优化依赖数组

    1// ❌ 避免
    2useEffect(() => {
    3  fetchData(user)
    4}, [user]) // user 对象每次都不同
    5
    6// ✅ 推荐
    7useEffect(() => {
    8  fetchData(user)
    9}, [user.id]) // 只依赖必要的属性

构建优化

Bundle 分析

  1. 分析 Bundle 大小

    1# 使用 webpack-bundle-analyzer
    2npm install --save-dev webpack-bundle-analyzer
    3
    4# 或使用 Vite 插件
    5npm install --save-dev rollup-plugin-visualizer
  2. 配置分析工具

    1// vite.config.ts
    2import { visualizer } from 'rollup-plugin-visualizer'
    3
    4export default defineConfig({
    5  plugins: [
    6    // ... 其他插件
    7    visualizer({
    8      filename: 'dist/stats.html',
    9      open: true
    10    })
    11  ]
    12})

Tree Shaking 优化

  1. 确保正确的导入方式

    1// ❌ 避免 - 导入整个库
    2import * as _ from 'lodash'
    3
    4// ✅ 推荐 - 只导入需要的函数
    5import { debounce } from 'lodash'
    6
    7// 或使用具体路径
    8import debounce from 'lodash/debounce'
  2. 配置 sideEffects

    1// package.json
    2{
    3  "sideEffects": false
    4}

监控和测量

性能指标收集

1// 性能监控服务
2class PerformanceMonitor {
3  static measureRouteChange(from: string, to: string) {
4    const startTime = performance.now()
5    
6    return () => {
7      const endTime = performance.now()
8      const duration = endTime - startTime
9      
10      // 发送到分析服务
11      this.sendMetric('route_change_duration', {
12        from,
13        to,
14        duration
15      })
16    }
17  }
18  
19  static measureComponentRender(componentName: string) {
20    return (id: string, phase: string, actualDuration: number) => {
21      if (actualDuration > 16) { // 超过一帧的时间
22        this.sendMetric('slow_component_render', {
23          component: componentName,
24          phase,
25          duration: actualDuration
26        })
27      }
28    }
29  }
30  
31  private static sendMetric(name: string, data: any) {
32    // 发送到分析服务
33    if (typeof window !== 'undefined' && window.gtag) {
34      window.gtag('event', name, data)
35    }
36  }
37}
38
39// 在组件中使用
40export default function UserPage() {
41  const location = useLocation()
42  
43  useEffect(() => {
44    const cleanup = PerformanceMonitor.measureRouteChange(
45      document.referrer,
46      location.pathname
47    )
48    
49    return cleanup
50  }, [location])
51  
52  return (
53    <Profiler
54      id="UserPage"
55      onRender={PerformanceMonitor.measureComponentRender('UserPage')}
56    >
57      {/* 页面内容 */}
58    </Profiler>
59  )
60}

Core Web Vitals 监控

1// 监控 Core Web Vitals
2import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
3
4function sendToAnalytics(metric) {
5  // 发送到分析服务
6  gtag('event', metric.name, {
7    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
8    event_category: 'Web Vitals',
9    event_label: metric.id,
10    non_interaction: true
11  })
12}
13
14// 测量所有指标
15getCLS(sendToAnalytics)
16getFID(sendToAnalytics)
17getFCP(sendToAnalytics)
18getLCP(sendToAnalytics)
19getTTFB(sendToAnalytics)

性能最佳实践总结

  1. 启用代码分割和懒加载
  2. 实现智能预加载策略
  3. 优化数据加载和缓存
  4. 使用适当的 Loading 状态
  5. 避免内存泄漏
  6. 监控性能指标
  7. 定期分析 Bundle 大小
  8. 优化关键渲染路径