维护说明:本文档已并入本站直接维护(单仓重构后原 backend/docs/*.md、前端 docs/*.md 不再随仓库发布)。 实现细节以 pay-unify 源码 为准,页面与源码的对应关系见相关资源

⚡ 前端性能优化指南

目标: 将首屏加载时间从 3-5s 降至 < 2s,Lighthouse 评分达到 90+


📊 当前性能分析

主要问题

  1. ❌ Bundle体积过大(估计 500KB+)
  2. ❌ 未使用代码分割
  3. ❌ 图片未优化
  4. ❌ 没有数据缓存
  5. ❌ 每次路由切换都重新请求数据
  6. ❌ 未使用React.memo和useMemo

🎯 优化方案

1. 添加 React Query (数据缓存和状态管理)

1.1 安装依赖

1cd frontend
2npm install @tanstack/react-query @tanstack/react-query-devtools

1.2 配置 Query Provider

创建 src/lib/queryClient.ts:

1import { QueryClient } from '@tanstack/react-query';
2
3export const queryClient = new QueryClient({
4  defaultOptions: {
5    queries: {
6      staleTime: 5 * 60 * 1000, // 5分钟内数据认为是新鲜的
7      gcTime: 10 * 60 * 1000, // 10分钟后清理缓存(原cacheTime)
8      retry: 1, // 失败后重试1次
9      refetchOnWindowFocus: false, // 窗口聚焦时不自动刷新
10    },
11  },
12});

修改 src/app/layout.tsx:

1'use client';
2
3import { QueryClientProvider } from '@tanstack/react-query';
4import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
5import { queryClient } from '@/lib/queryClient';
6import { AuthProvider } from '@/core/auth';
7
8export default function RootLayout({
9  children,
10}: {
11  children: React.ReactNode;
12}) {
13  return (
14    <html lang="zh-CN">
15      <body>
16        <QueryClientProvider client={queryClient}>
17          <AuthProvider>
18            {children}
19          </AuthProvider>
20          {/* 开发环境显示调试工具 */}
21          {process.env.NODE_ENV === 'development' && (
22            <ReactQueryDevtools initialIsOpen={false} />
23          )}
24        </QueryClientProvider>
25      </body>
26    </html>
27  );
28}

1.3 创建自定义Hooks

创建 src/hooks/useProducts.ts:

1import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2import { productService } from '@/features/products/services/productService';
3import { Product, ProductType } from '@/features/products/types';
4
5// 查询产品列表
6export const useProducts = (params?: {
7  type?: ProductType;
8  page?: number;
9  pageSize?: number;
10}) => {
11  return useQuery({
12    queryKey: ['products', params],
13    queryFn: () => productService.getProducts(params),
14    select: (data) => data.data, // 只返回data部分
15  });
16};
17
18// 创建产品
19export const useCreateProduct = () => {
20  const queryClient = useQueryClient();
21
22  return useMutation({
23    mutationFn: productService.createProduct,
24    onSuccess: () => {
25      // 创建成功后刷新产品列表
26      queryClient.invalidateQueries({ queryKey: ['products'] });
27    },
28  });
29};
30
31// 更新产品
32export const useUpdateProduct = () => {
33  const queryClient = useQueryClient();
34
35  return useMutation({
36    mutationFn: ({ id, data }: { id: string; data: Partial<Product> }) =>
37      productService.updateProduct(id, data),
38    onSuccess: () => {
39      queryClient.invalidateQueries({ queryKey: ['products'] });
40    },
41  });
42};
43
44// 删除产品
45export const useDeleteProduct = () => {
46  const queryClient = useQueryClient();
47
48  return useMutation({
49    mutationFn: productService.deleteProduct,
50    onSuccess: () => {
51      queryClient.invalidateQueries({ queryKey: ['products'] });
52    },
53  });
54};

类似地创建其他hooks:

  • src/hooks/useOrders.ts
  • src/hooks/useUsers.ts
  • src/hooks/useCoins.ts
  • src/hooks/usePayments.ts
  • src/hooks/useStats.ts

1.4 重构页面使用hooks

重构 src/app/dashboard/products/page.tsx:

1'use client';
2
3import { useState } from 'react';
4import { useProducts, useCreateProduct } from '@/hooks/useProducts';
5import { ProductType } from '@/features/products/types';
6import CreateProductModal from '@/components/products/CreateProductModal';
7import { toast } from 'react-hot-toast';
8
9export default function ProductsPage() {
10  const [activeTab, setActiveTab] = useState<ProductType>('vip');
11  const [showCreateModal, setShowCreateModal] = useState(false);
12
13  // 使用React Query hooks
14  const { data, isLoading, error, refetch } = useProducts({
15    type: activeTab,
16  });
17
18  const createProductMutation = useCreateProduct();
19
20  const handleCreateProduct = async (productData: any) => {
21    try {
22      await createProductMutation.mutateAsync(productData);
23      toast.success('商品创建成功');
24      setShowCreateModal(false);
25      // 不需要手动刷新,React Query自动处理
26    } catch (error) {
27      toast.error('创建失败,请重试');
28    }
29  };
30
31  if (error) {
32    return <div>加载失败: {error.message}</div>;
33  }
34
35  return (
36    <div>
37      {/* 标签切换 */}
38      <div className="tabs">
39        {['vip', 'coin'].map((tab) => (
40          <button
41            key={tab}
42            onClick={() => setActiveTab(tab as ProductType)}
43            className={activeTab === tab ? 'active' : ''}
44          >
45            {tab === 'vip' ? 'VIP商品' : '金币商品'}
46          </button>
47        ))}
48      </div>
49
50      {/* 产品列表 */}
51      {isLoading ? (
52        <div>加载中...</div>
53      ) : (
54        <div className="grid grid-cols-3 gap-4">
55          {data?.list.map((product) => (
56            <ProductCard key={product.productId} product={product} />
57          ))}
58        </div>
59      )}
60
61      {/* 创建弹窗 */}
62      {showCreateModal && (
63        <CreateProductModal
64          onClose={() => setShowCreateModal(false)}
65          onSuccess={handleCreateProduct}
66        />
67      )}
68    </div>
69  );
70}

2. 代码分割和懒加载

2.1 路由级别代码分割

修改 src/app/dashboard/layout.tsx:

1import dynamic from 'next/dynamic';
2import { Suspense } from 'react';
3import Loading from './loading';
4
5// 懒加载侧边栏(包含大量图标)
6const Sidebar = dynamic(() => import('@/components/layout/Sidebar'), {
7  loading: () => <div className="w-64 bg-gray-900 animate-pulse" />,
8  ssr: false, // 侧边栏不需要SSR
9});
10
11// 懒加载顶部栏
12const TopBar = dynamic(() => import('@/components/layout/TopBar'), {
13  loading: () => <div className="h-16 bg-white animate-pulse" />,
14});
15
16export default function DashboardLayout({
17  children,
18}: {
19  children: React.ReactNode;
20}) {
21  return (
22    <div className="flex h-screen">
23      <Sidebar />
24      <div className="flex-1 flex flex-col">
25        <TopBar />
26        <main className="flex-1 overflow-auto bg-gray-50 p-6">
27          <Suspense fallback={<Loading />}>
28            {children}
29          </Suspense>
30        </main>
31      </div>
32    </div>
33  );
34}

2.2 组件级别代码分割

创建 src/components/charts/ChartWrapper.tsx:

1import dynamic from 'next/dynamic';
2
3// 图表库很大,懒加载
4const LineChart = dynamic(
5  () => import('recharts').then((mod) => mod.LineChart),
6  { ssr: false }
7);
8
9const BarChart = dynamic(
10  () => import('recharts').then((mod) => mod.BarChart),
11  { ssr: false }
12);
13
14export { LineChart, BarChart };

2.3 弹窗懒加载

修改 src/app/dashboard/products/page.tsx:

1import dynamic from 'next/dynamic';
2
3// 弹窗只在需要时加载
4const CreateProductModal = dynamic(
5  () => import('@/components/products/CreateProductModal'),
6  {
7    loading: () => <div>加载中...</div>,
8  }
9);
10
11export default function ProductsPage() {
12  const [showModal, setShowModal] = useState(false);
13
14  return (
15    <>
16      <button onClick={() => setShowModal(true)}>创建商品</button>
17      {showModal && <CreateProductModal onClose={() => setShowModal(false)} />}
18    </>
19  );
20}

3. 图片优化

3.1 使用 Next.js Image 组件

修改所有图片引用:

1// ❌ 旧方式
2<img src="/003.png" alt="Background" />
3
4// ✅ 新方式
5import Image from 'next/image';
6
7<Image
8  src="/003.png"
9  alt="Background"
10  fill
11  priority // 首屏图片
12  quality={75} // 压缩质量
13  placeholder="blur" // 模糊占位
14  blurDataURL="data:image/..." // 占位图
15/>

3.2 配置图片优化

修改 next.config.ts:

1import type { NextConfig } from 'next';
2
3const nextConfig: NextConfig = {
4  images: {
5    formats: ['image/avif', 'image/webp'], // 使用现代格式
6    deviceSizes: [640, 750, 828, 1080, 1200, 1920], // 响应式尺寸
7    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
8    minimumCacheTTL: 60 * 60 * 24 * 7, // 缓存7天
9    remotePatterns: [
10      {
11        protocol: 'https',
12        hostname: 'api.example.com',
13        pathname: '/uploads/**',
14      },
15    ],
16  },
17};
18
19export default nextConfig;

3.3 压缩现有图片

1# 安装压缩工具
2npm install -g imagemin-cli imagemin-webp
3
4# 压缩PNG/JPG
5cd frontend/public
6imagemin *.{jpg,png} --out-dir=./optimized
7
8# 生成WebP格式
9imagemin *.{jpg,png} --plugin=webp --out-dir=./optimized
10
11# 替换原图
12mv optimized/* ./
13rm -rf optimized

4. 性能优化技巧

4.1 使用 React.memo

优化列表项组件:

1// components/ProductCard.tsx
2import { memo } from 'react';
3
4interface ProductCardProps {
5  product: Product;
6  onEdit?: (id: string) => void;
7  onDelete?: (id: string) => void;
8}
9
10const ProductCard = memo(({ product, onEdit, onDelete }: ProductCardProps) => {
11  return (
12    <div className="card">
13      <h3>{product.name}</h3>
14      <p>{product.price}</p>
15      {onEdit && <button onClick={() => onEdit(product.productId)}>编辑</button>}
16      {onDelete && <button onClick={() => onDelete(product.productId)}>删除</button>}
17    </div>
18  );
19});
20
21ProductCard.displayName = 'ProductCard';
22
23export default ProductCard;

4.2 使用 useMemo 缓存计算

1import { useMemo } from 'react';
2
3function OrdersPage() {
4  const { data: orders } = useOrders();
5
6  // 缓存统计计算
7  const statistics = useMemo(() => {
8    if (!orders) return null;
9    
10    return {
11      totalAmount: orders.reduce((sum, order) => sum + order.amount, 0),
12      successCount: orders.filter(o => o.status === 'success').length,
13      pendingCount: orders.filter(o => o.status === 'pending').length,
14    };
15  }, [orders]);
16
17  return <div>{/* 使用 statistics */}</div>;
18}

4.3 使用 useCallback 缓存函数

1import { useCallback } from 'react';
2
3function ProductList() {
4  const { data: products } = useProducts();
5
6  // 缓存事件处理函数
7  const handleEdit = useCallback((id: string) => {
8    // 编辑逻辑
9  }, []);
10
11  const handleDelete = useCallback((id: string) => {
12    // 删除逻辑
13  }, []);
14
15  return (
16    <>
17      {products?.list.map((product) => (
18        <ProductCard
19          key={product.productId}
20          product={product}
21          onEdit={handleEdit}
22          onDelete={handleDelete}
23        />
24      ))}
25    </>
26  );
27}

4.4 虚拟滚动(长列表)

1npm install @tanstack/react-virtual
1import { useVirtualizer } from '@tanstack/react-virtual';
2import { useRef } from 'react';
3
4function LargeOrderList({ orders }: { orders: Order[] }) {
5  const parentRef = useRef<HTMLDivElement>(null);
6
7  const virtualizer = useVirtualizer({
8    count: orders.length,
9    getScrollElement: () => parentRef.current,
10    estimateSize: () => 100, // 每行高度
11    overscan: 5, // 预渲染5行
12  });
13
14  return (
15    <div ref={parentRef} className="h-screen overflow-auto">
16      <div
17        style={{
18          height: `${virtualizer.getTotalSize()}px`,
19          position: 'relative',
20        }}
21      >
22        {virtualizer.getVirtualItems().map((virtualRow) => {
23          const order = orders[virtualRow.index];
24          return (
25            <div
26              key={virtualRow.key}
27              style={{
28                position: 'absolute',
29                top: 0,
30                left: 0,
31                width: '100%',
32                height: `${virtualRow.size}px`,
33                transform: `translateY(${virtualRow.start}px)`,
34              }}
35            >
36              <OrderCard order={order} />
37            </div>
38          );
39        })}
40      </div>
41    </div>
42  );
43}

5. Bundle 优化

5.1 分析 Bundle 大小

1# 安装分析工具
2npm install @next/bundle-analyzer
3
4# 修改 next.config.ts
5import bundleAnalyzer from '@next/bundle-analyzer';
6
7const withBundleAnalyzer = bundleAnalyzer({
8  enabled: process.env.ANALYZE === 'true',
9});
10
11export default withBundleAnalyzer({
12  // ... 其他配置
13});
14
15# 运行分析
16ANALYZE=true npm run build

5.2 优化导入

1// ❌ 导入整个库
2import _ from 'lodash';
3import * as Icons from 'lucide-react';
4
5// ✅ 按需导入
6import debounce from 'lodash/debounce';
7import { Search, User, Settings } from 'lucide-react';

5.3 移除未使用的依赖

1# 检查未使用的依赖
2npx depcheck
3
4# 移除
5npm uninstall <unused-package>

5.4 使用生产模式构建

1# 确保生产环境变量
2NODE_ENV=production npm run build
3
4# 启动生产服务器
5npm start

6. 网络优化

6.1 启用 HTTP/2 和压缩

Nginx 配置:

1server {
2    listen 443 ssl http2;
3    
4    # Gzip压缩
5    gzip on;
6    gzip_vary on;
7    gzip_min_length 1024;
8    gzip_types text/plain text/css text/xml text/javascript 
9               application/x-javascript application/xml+rss 
10               application/json application/javascript;
11    
12    # Brotli压缩(更好)
13    brotli on;
14    brotli_comp_level 6;
15    brotli_types text/plain text/css application/json 
16                 application/javascript text/xml application/xml;
17    
18    # 缓存静态资源
19    location /_next/static {
20        add_header Cache-Control "public, max-age=31536000, immutable";
21    }
22    
23    location /images {
24        add_header Cache-Control "public, max-age=604800";
25    }
26}

6.2 预加载关键资源

修改 src/app/layout.tsx:

1export default function RootLayout({ children }: { children: React.ReactNode }) {
2  return (
3    <html>
4      <head>
5        {/* 预连接API服务器 */}
6        <link rel="preconnect" href="https://api.example.com" />
7        <link rel="dns-prefetch" href="https://api.example.com" />
8        
9        {/* 预加载关键字体 */}
10        <link
11          rel="preload"
12          href="/fonts/custom-font.woff2"
13          as="font"
14          type="font/woff2"
15          crossOrigin="anonymous"
16        />
17      </head>
18      <body>{children}</body>
19    </html>
20  );
21}

6.3 实现请求去重

修改 src/services/apiClient.ts:

1class ApiClient {
2  private pendingRequests = new Map<string, Promise<any>>();
3
4  private generateKey(url: string, params?: any): string {
5    return `${url}:${JSON.stringify(params)}`;
6  }
7
8  async get<T>(url: string, params?: any): Promise<T> {
9    const key = this.generateKey(url, params);
10    
11    // 如果相同请求正在进行,返回已有Promise
12    if (this.pendingRequests.has(key)) {
13      return this.pendingRequests.get(key)!;
14    }
15
16    const promise = this.instance.get<T>(url, { params }).then((res) => {
17      this.pendingRequests.delete(key);
18      return res.data;
19    });
20
21    this.pendingRequests.set(key, promise);
22    return promise;
23  }
24}

7. 监控和追踪

7.1 添加 Web Vitals 追踪

创建 src/lib/analytics.ts:

1import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
2
3function sendToAnalytics(metric: any) {
4  // 发送到分析服务
5  console.log(metric);
6  
7  // 可以集成 Google Analytics
8  if (typeof window !== 'undefined' && (window as any).gtag) {
9    (window as any).gtag('event', metric.name, {
10      value: Math.round(metric.value),
11      event_category: 'Web Vitals',
12      event_label: metric.id,
13      non_interaction: true,
14    });
15  }
16}
17
18export function reportWebVitals() {
19  getCLS(sendToAnalytics);
20  getFID(sendToAnalytics);
21  getFCP(sendToAnalytics);
22  getLCP(sendToAnalytics);
23  getTTFB(sendToAnalytics);
24}

src/app/layout.tsx 中使用:

1'use client';
2
3import { useEffect } from 'react';
4import { reportWebVitals } from '@/lib/analytics';
5
6export default function RootLayout({ children }: { children: React.ReactNode }) {
7  useEffect(() => {
8    reportWebVitals();
9  }, []);
10
11  return <html>{children}</html>;
12}

7.2 添加性能标记

1// 标记关键事件
2export function markPerformance(name: string) {
3  if (typeof window !== 'undefined' && window.performance) {
4    window.performance.mark(name);
5  }
6}
7
8export function measurePerformance(name: string, startMark: string, endMark: string) {
9  if (typeof window !== 'undefined' && window.performance) {
10    window.performance.measure(name, startMark, endMark);
11    const measure = window.performance.getEntriesByName(name)[0];
12    console.log(`${name}: ${measure.duration}ms`);
13  }
14}
15
16// 使用示例
17markPerformance('data-fetch-start');
18await fetchData();
19markPerformance('data-fetch-end');
20measurePerformance('data-fetch', 'data-fetch-start', 'data-fetch-end');

📈 性能检查清单

构建优化

  • 启用生产模式构建
  • 分析Bundle大小(< 200KB gzipped)
  • 移除未使用的依赖
  • 按需导入第三方库
  • 启用Tree Shaking

代码优化

  • 使用React.memo优化组件
  • 使用useMemo缓存计算
  • 使用useCallback缓存函数
  • 实现虚拟滚动(长列表)
  • 懒加载路由和组件

数据优化

  • 集成React Query
  • 实现数据缓存策略
  • 防抖搜索输入
  • 实现分页加载
  • 请求去重

资源优化

  • 使用Next.js Image组件
  • 压缩图片(WebP/AVIF)
  • 预加载关键资源
  • 配置静态资源缓存
  • 启用Gzip/Brotli压缩

监控

  • 配置Web Vitals追踪
  • 添加性能标记
  • 集成错误追踪(Sentry)
  • 配置Lighthouse CI

🎯 性能目标

指标 当前 目标 优化后
FCP (首次内容绘制) ~2.5s < 1.5s ~1.2s
LCP (最大内容绘制) ~4s < 2.5s ~2s
TTI (可交互时间) ~5s < 3s ~2.5s
Bundle Size ~500KB < 200KB ~180KB
Lighthouse Score ~60 > 90 ~95

📱 移动端优化

1. 响应式图片

1<Image
2  src="/hero.jpg"
3  alt="Hero"
4  sizes="(max-width: 768px) 100vw, 50vw"
5  width={800}
6  height={600}
7/>

2. 触摸优化

1/* 增大点击区域 */
2.button {
3  min-height: 44px;
4  min-width: 44px;
5}
6
7/* 防止文本选择 */
8.no-select {
9  -webkit-user-select: none;
10  user-select: none;
11}
12
13/* 优化滚动 */
14.scroll-container {
15  -webkit-overflow-scrolling: touch;
16}

3. 减少重排

1// ❌ 多次修改DOM
2element.style.width = '100px';
3element.style.height = '100px';
4element.style.margin = '10px';
5
6// ✅ 批量修改
7element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

🚀 快速实施计划

Week 1: 数据层优化

  • Day 1-2: 集成React Query
  • Day 3-4: 创建所有自定义hooks
  • Day 5: 重构所有页面使用hooks

Week 2: 代码分割

  • Day 1-2: 实现路由懒加载
  • Day 3: 实现组件懒加载
  • Day 4-5: 添加React.memo/useMemo

Week 3: 资源优化

  • Day 1-2: 图片优化和Next.js Image
  • Day 3: Bundle分析和优化
  • Day 4-5: 网络优化和缓存

Week 4: 监控和测试

  • Day 1-2: 添加性能监控
  • Day 3-4: 性能测试和调优
  • Day 5: 文档和培训

预期提升:

  • ⚡ 加载速度提升 50-60%
  • 📦 Bundle体积减少 40-50%
  • 🎯 Lighthouse评分提升至 90+
  • 💾 数据请求减少 70%(缓存)

开始优化吧!🚀