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

性能优化

文档版本:v1.1
最后更新:2025-01-18


目录

  1. 代码分割
  2. 虚拟列表
  3. 组件优化
  4. 图片优化
  5. Debounce 和 Throttle

1. 代码分割

1.1 路由级代码分割

// 路由级代码分割
const EditorPage = lazy(() => import('@/pages/EditorPage'));

// 组件级代码分割
const HeavyComponent = lazy(() => import('@components/features/HeavyComponent'));

// 使用时包裹 Suspense
<Suspense fallback={<LoadingSpinner />}>
  <HeavyComponent />
</Suspense>

1.2 动态导入

// 按需加载大型库
const handleExport = async () => {
  const { exportToVideo } = await import("@/utils/videoExport");
  await exportToVideo(data);
};

2. 虚拟列表

使用 @tanstack/react-virtual 处理长列表:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // 预估每项高度
    overscan: 5, // 预渲染数量
  });

  return (
    <div ref={parentRef} className="h-full overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            <ItemComponent item={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

3. 组件优化

3.1 使用 memo

// 使用 memo 避免不必要渲染
const StoryboardItem = memo(function StoryboardItem({
  storyboard,
  isSelected,
  onSelect
}: StoryboardItemProps) {
  return (
    <div
      className={cn(
        'p-3 rounded-md cursor-pointer',
        isSelected && 'bg-primary/10 border-l-2 border-primary'
      )}
      onClick={() => onSelect(storyboard.id)}
    >
      {/* ... */}
    </div>
  );
});

3.2 使用 useMemo

// 使用 useMemo 缓存计算结果
const sortedStoryboards = useMemo(
  () => storyboards.sort((a, b) => a.order - b.order),
  [storyboards],
);

3.3 使用 useCallback

// 使用 useCallback 缓存回调
const handleSelect = useCallback(
  (id: string) => {
    selectStoryboard(id);
  },
  [selectStoryboard],
);

4. 图片优化

4.1 懒加载图片

// 图片懒加载组件
function LazyImage({ src, alt, className }: LazyImageProps) {
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && imgRef.current) {
          imgRef.current.src = src;
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, [src]);

  return (
    <div className={cn('relative', className)}>
      {!loaded && <div className="absolute inset-0 bg-muted animate-pulse" />}
      <img
        ref={imgRef}
        alt={alt}
        className={cn(
          'w-full h-full object-cover transition-opacity',
          loaded ? 'opacity-100' : 'opacity-0'
        )}
        onLoad={() => setLoaded(true)}
      />
    </div>
  );
}

4.2 响应式图片

<picture>
  <source
    srcSet={`${image}-small.webp`}
    media="(max-width: 640px)"
    type="image/webp"
  />
  <source
    srcSet={`${image}-medium.webp`}
    media="(max-width: 1024px)"
    type="image/webp"
  />
  <img src={`${image}-large.webp`} alt={alt} />
</picture>

5. Debounce 和 Throttle

5.1 useDebounce Hook

// src/hooks/useDebounce.ts
import { useState, useEffect } from "react";

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

5.2 使用示例

function SearchInput() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);

  useEffect(() => {
    if (debouncedSearch) {
      // 执行搜索
      performSearch(debouncedSearch);
    }
  }, [debouncedSearch]);

  return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}

5.3 useThrottle Hook

// src/hooks/useThrottle.ts
import { useRef, useCallback } from "react";

export function useThrottle<T extends (...args: any[]) => any>(
  callback: T,
  delay: number,
): T {
  const lastRun = useRef(Date.now());

  return useCallback(
    (...args: Parameters<T>) => {
      const now = Date.now();
      if (now - lastRun.current >= delay) {
        callback(...args);
        lastRun.current = now;
      }
    },
    [callback, delay],
  ) as T;
}

6. 性能监控

6.1 React DevTools Profiler

import { Profiler } from "react";

function onRenderCallback(
  id: string,
  phase: "mount" | "update",
  actualDuration: number,
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

<Profiler id="StoryboardList" onRender={onRenderCallback}>
  <StoryboardList />
</Profiler>;

6.2 Web Vitals

// src/lib/vitals.ts
import { onCLS, onFID, onFCP, onLCP, onTTFB } from "web-vitals";

export function reportWebVitals() {
  onCLS(console.log);
  onFID(console.log);
  onFCP(console.log);
  onLCP(console.log);
  onTTFB(console.log);
}

相关文档


最后更新:2025-01-18 | 版本:v1.1