You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.3 KiB

ProjectResourcePanel 性能优化

日期: 2026-01-28
类型: 性能优化
影响范围: 前端 - 资源管理面板

概述

基于 React Best Practices 对 ProjectResourcePanel 组件进行全面性能优化,包括 Bundle Size 优化、图片加载优化、Tab 切换优化和 Re-render 优化。

优化内容

1. Bundle Size 优化

问题:使用 barrel imports 导致不必要的代码打包

解决方案

  • 拆分 @/components/ui/dropdown-menu 为直接导入
  • 使用 React.lazy 延迟加载 Dialog 相关组件
  • 仅在需要时加载删除确认弹窗

代码变更

// Before
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

// After
import { DropdownMenu } from '@/components/ui/dropdown-menu';
import { DropdownMenuContent } from '@/components/ui/dropdown-menu';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu';

// Dialog 延迟加载
const Dialog = lazy(() => import('@/components/ui/dialog').then(m => ({ default: m.Dialog })));

收益

  • 减少初始 bundle size
  • 提升首屏加载速度
  • 删除弹窗按需加载

2. 图片加载优化(已修正)

问题

  1. 图片加载无过渡效果,用户体验差
  2. 每次切换 tab,图片都重新显示 loading,即使已加载过

解决方案

  • ResourceGridItem 添加 imageLoading 状态
  • 检查图片是否已在浏览器缓存中
  • 已缓存的图片直接显示,不显示 loading skeleton
  • 使用 loading="lazy"decoding="async" 优化加载
  • 添加淡入动画(transition-opacity duration-300

代码变更

const [imageLoading, setImageLoading] = useState(true);

// 检查图片是否已在浏览器缓存中
const imageUrl = resource.thumbnailUrl || resource.fileUrl;
const isImageCached = useMemo(() => {
  if (!imageUrl) return false;
  const img = new Image();
  img.src = imageUrl;
  return img.complete && img.naturalHeight !== 0;
}, [imageUrl]);

// 如果图片已缓存,直接设置为已加载
useEffect(() => {
  if (isImageCached) {
    setImageLoading(false);
  }
}, [isImageCached]);

{/* Loading Skeleton - 仅在首次加载时显示 */}
{imageLoading && !isImageCached && (
  <div className="absolute inset-0 flex h-full w-full items-center justify-center bg-fill-darker animate-pulse">
    <Icon className="h-1/3 w-1/3 text-text-secondary/30" />
  </div>
)}
<img
  loading="lazy"
  decoding="async"
  className={cn(
    'h-full w-full transition-opacity duration-300',
    imageLoading && !isImageCached ? 'opacity-0' : 'opacity-100'
  )}
  onLoad={() => setImageLoading(false)}
/>

收益

  • 提升用户体验
  • 避免图片突然出现
  • 清晰的加载状态反馈
  • 已缓存图片立即显示,无 loading 闪烁
  • 利用浏览器原生懒加载优化性能

3. Tab 切换优化(已修正)

问题

  1. Tab 切换时频繁显示 loading,影响用户体验
  2. Loading 和空状态同时闪烁

解决方案

  • 添加 loadedTabs Set 记录已加载过的 tab
  • 仅首次加载时显示 loading
  • Loading 遮罩仅覆盖列表区域,不遮挡 tab 选项
  • 优化渲染逻辑:loading → 内容(列表或空状态),避免同时显示

代码变更

const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set([RESOURCE_TYPES.CHARACTER]));

// 标记当前 tab 为已加载
useEffect(() => {
  if (!isLoading && resources && resources.length >= 0 && !loadedTabs.has(filterType)) {
    const timer = setTimeout(() => {
      setLoadedTabs((prev) => new Set(prev).add(filterType));
    }, 0);
    return () => clearTimeout(timer);
  }
}, [isLoading, resources, filterType, loadedTabs]);

// 判断是否需要显示 loading(首次加载且未加载过该 tab)
const shouldShowLoading = isLoading && !loadedTabs.has(filterType);

// 优化渲染逻辑:loading → 内容,避免闪烁
{shouldShowLoading ? (
  <LoadingSpinner />
) : error ? (
  <EmptyState error />
) : (
  <VirtualList ... />
)}

收益

  • 避免频繁的 loading 闪烁
  • Tab 选项始终可见和可交互
  • 更流畅的切换体验
  • 仅首次加载显示 loading
  • 避免 loading 和空状态同时出现

4. Re-render 优化

问题:不必要的 useMemo 和多个 useEffect 导致额外渲染

解决方案

  • 简化不必要的 useMemo(常量数据、简单逻辑)
  • 合并相关的 useEffect
  • 移除空状态组件的 useMemo(JSX 本身已足够轻量)

代码变更

// Before
const tabs: TabItem[] = useMemo(
  () => RESOURCE_CATEGORIES.map(...),
  [selectedProjectId]
);

// After - RESOURCE_CATEGORIES 是常量,无需 memo
const tabs: TabItem[] = RESOURCE_CATEGORIES.map(...);

// Before - 两个独立的 useEffect
useEffect(() => { /* projectId 变化 */ }, [selectedProjectId, ...]);
useEffect(() => { /* 清除选中状态 */ }, [selectedResourceId, ...]);

// After - 合并相关逻辑
useEffect(() => {
  // projectId 变化逻辑
  // 清除选中状态逻辑
}, [selectedProjectId, filterType, searchQuery, selectedResourceId, selectedResources]);

收益

  • 减少不必要的重渲染
  • 简化代码逻辑
  • 提升运行时性能

5. 样式优化

问题:使用了非标准的 Tailwind 类名

解决方案

  • bg-gradient-to-t 修正为 bg-linear-to-t

性能指标

预期提升

  • Bundle Size: 减少 ~5-10KB(Dialog 延迟加载)
  • 首屏加载: 提升 ~100-200ms
  • Tab 切换: 更流畅的 UI 响应
  • 图片加载: 更好的用户体验

测试建议

  1. Bundle Size 测试

    npm run build
    # 对比优化前后的 bundle size
    
  2. 功能测试

    • 切换不同 Tab,验证 loading 状态
    • 上传图片,验证 loading skeleton
    • 删除资源,验证 Dialog 延迟加载
    • 多选资源,验证选中状态
  3. 性能测试

    • 使用 React DevTools Profiler 对比优化前后
    • 验证 re-render 次数减少
    • 验证 Tab 切换响应时间

相关文件

  • client/src/components/features/project/ProjectResourcePanel.tsx

参考