维护说明:本文档已并入本站直接维护(单仓重构后原
backend/docs/*.md、前端docs/*.md不再随仓库发布)。 实现细节以 pay-unify 源码 为准,页面与源码的对应关系见相关资源。
目标: 将首屏加载时间从 3-5s 降至 < 2s,Lighthouse 评分达到 90+
1cd frontend
2npm install @tanstack/react-query @tanstack/react-query-devtools创建 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}创建 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.tssrc/hooks/useUsers.tssrc/hooks/useCoins.tssrc/hooks/usePayments.tssrc/hooks/useStats.ts重构 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}修改 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}创建 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 };修改 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}修改所有图片引用:
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/>修改 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;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优化列表项组件:
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;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}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}1npm install @tanstack/react-virtual1import { 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}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 build1// ❌ 导入整个库
2import _ from 'lodash';
3import * as Icons from 'lucide-react';
4
5// ✅ 按需导入
6import debounce from 'lodash/debounce';
7import { Search, User, Settings } from 'lucide-react';1# 检查未使用的依赖
2npx depcheck
3
4# 移除
5npm uninstall <unused-package>1# 确保生产环境变量
2NODE_ENV=production npm run build
3
4# 启动生产服务器
5npm startNginx 配置:
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}修改 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}修改 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}创建 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}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');| 指标 | 当前 | 目标 | 优化后 |
|---|---|---|---|
| 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<Image
2 src="/hero.jpg"
3 alt="Hero"
4 sizes="(max-width: 768px) 100vw, 50vw"
5 width={800}
6 height={600}
7/>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}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;';预期提升:
开始优化吧!🚀