Skip to content

懒加载与预加载

课程概述

本章节深入探讨懒加载(Lazy Loading)和预加载(Preloading)技术,学习如何通过合理的资源加载策略优化应用性能。掌握这些技术能够显著提升用户体验和页面加载速度。

学习目标

  • 理解懒加载和预加载的概念
  • 掌握React组件的懒加载
  • 学习图片和资源的懒加载
  • 了解预加载和预获取策略
  • 掌握IntersectionObserver API
  • 学习加载优先级管理

第一部分:懒加载基础

1.1 什么是懒加载

懒加载是一种按需加载策略,只在需要时才加载资源,而不是一次性加载所有内容。

传统加载:

javascript
// 立即加载所有内容
<img src="image1.jpg" />
<img src="image2.jpg" />
<img src="image3.jpg" />
// ... 100 张图片

懒加载:

javascript
// 只加载可见区域的内容
<img data-src="image1.jpg" class="lazy" />
// 当滚动到视口时才加载

1.2 懒加载的好处

javascript
1. 减少初始加载时间
2. 节省带宽
3. 提升页面性能
4. 改善用户体验
5. 降低服务器压力

1.3 懒加载类型

javascript
1. 组件懒加载  - React.lazy
2. 图片懒加载  - Intersection Observer
3. 路由懒加载  - Dynamic Import
4. 脚本懒加载  - Script Loading
5. 数据懒加载  - Infinite Scroll

第二部分:React组件懒加载

2.1 基础组件懒加载

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'

// 懒加载组件
const Dashboard = lazy(() => import('./components/Dashboard'))
const Profile = lazy(() => import('./components/Profile'))
const Settings = lazy(() => import('./components/Settings'))

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading Dashboard...</div>}>
        <Dashboard />
      </Suspense>
      
      <Suspense fallback={<div>Loading Profile...</div>}>
        <Profile />
      </Suspense>
      
      <Suspense fallback={<div>Loading Settings...</div>}>
        <Settings />
      </Suspense>
    </div>
  )
}

2.2 带命名导出的懒加载

typescript
// src/components/Charts.tsx
export function LineChart() {
  return <div>Line Chart</div>
}

export function BarChart() {
  return <div>Bar Chart</div>
}

// src/App.tsx
import { lazy } from 'react'

// 懒加载命名导出
const LineChart = lazy(() => 
  import('./components/Charts').then(module => ({ 
    default: module.LineChart 
  }))
)

const BarChart = lazy(() => 
  import('./components/Charts').then(module => ({ 
    default: module.BarChart 
  }))
)

2.3 条件懒加载

typescript
// src/components/Editor.tsx
import { lazy, Suspense, useState } from 'react'

const RichTextEditor = lazy(() => import('./RichTextEditor'))
const CodeEditor = lazy(() => import('./CodeEditor'))

export function Editor() {
  const [editorType, setEditorType] = useState<'rich' | 'code'>('rich')
  
  return (
    <div>
      <select 
        value={editorType} 
        onChange={(e) => setEditorType(e.target.value as any)}
      >
        <option value="rich">Rich Text</option>
        <option value="code">Code Editor</option>
      </select>
      
      <Suspense fallback={<div>Loading editor...</div>}>
        {editorType === 'rich' ? <RichTextEditor /> : <CodeEditor />}
      </Suspense>
    </div>
  )
}

2.4 错误处理

typescript
// src/components/LazyComponent.tsx
import { lazy, Suspense, ComponentType } from 'react'
import { ErrorBoundary } from './ErrorBoundary'

function lazyWithRetry<T extends ComponentType<any>>(
  componentImport: () => Promise<{ default: T }>,
  retriesLeft = 3,
  interval = 1000
): ReturnType<typeof lazy> {
  return lazy(() => 
    new Promise((resolve, reject) => {
      componentImport()
        .then(resolve)
        .catch((error) => {
          setTimeout(() => {
            if (retriesLeft === 1) {
              reject(error)
              return
            }
            
            lazyWithRetry(componentImport, retriesLeft - 1, interval)
              .preload()
              .then(resolve, reject)
          }, interval)
        })
    })
  )
}

// 使用
const Dashboard = lazyWithRetry(() => import('./Dashboard'))

function App() {
  return (
    <ErrorBoundary fallback={<div>加载失败</div>}>
      <Suspense fallback={<div>加载中...</div>}>
        <Dashboard />
      </Suspense>
    </ErrorBoundary>
  )
}

第三部分:图片懒加载

3.1 原生懒加载

typescript
// 使用 loading="lazy" 属性
export function ImageGallery() {
  return (
    <div>
      <img src="/image1.jpg" loading="lazy" alt="Image 1" />
      <img src="/image2.jpg" loading="lazy" alt="Image 2" />
      <img src="/image3.jpg" loading="lazy" alt="Image 3" />
    </div>
  )
}

3.2 Intersection Observer懒加载

typescript
// src/components/LazyImage.tsx
import { useEffect, useRef, useState } from 'react'

interface LazyImageProps {
  src: string
  placeholder?: string
  alt: string
  className?: string
}

export function LazyImage({ src, placeholder, alt, className }: LazyImageProps) {
  const [imageSrc, setImageSrc] = useState(placeholder || '')
  const [isLoaded, setIsLoaded] = useState(false)
  const imgRef = useRef<HTMLImageElement>(null)
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageSrc(src)
            observer.disconnect()
          }
        })
      },
      {
        rootMargin: '50px', // 提前50px开始加载
      }
    )
    
    if (imgRef.current) {
      observer.observe(imgRef.current)
    }
    
    return () => observer.disconnect()
  }, [src])
  
  return (
    <img
      ref={imgRef}
      src={imageSrc}
      alt={alt}
      className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
      onLoad={() => setIsLoaded(true)}
    />
  )
}

3.3 渐进式图片加载

typescript
// src/components/ProgressiveImage.tsx
import { useState, useEffect } from 'react'

interface ProgressiveImageProps {
  src: string
  placeholderSrc: string
  alt: string
}

export function ProgressiveImage({ src, placeholderSrc, alt }: ProgressiveImageProps) {
  const [currentSrc, setCurrentSrc] = useState(placeholderSrc)
  const [isLoading, setIsLoading] = useState(true)
  
  useEffect(() => {
    const img = new Image()
    img.src = src
    
    img.onload = () => {
      setCurrentSrc(src)
      setIsLoading(false)
    }
  }, [src])
  
  return (
    <img
      src={currentSrc}
      alt={alt}
      style={{
        filter: isLoading ? 'blur(10px)' : 'none',
        transition: 'filter 0.3s',
      }}
    />
  )
}

3.4 响应式图片懒加载

typescript
// src/components/ResponsiveLazyImage.tsx
import { useEffect, useRef, useState } from 'react'

interface ResponsiveLazyImageProps {
  srcSet: {
    mobile: string
    tablet: string
    desktop: string
  }
  alt: string
}

export function ResponsiveLazyImage({ srcSet, alt }: ResponsiveLazyImageProps) {
  const [imageSrc, setImageSrc] = useState('')
  const imgRef = useRef<HTMLImageElement>(null)
  
  const getResponsiveSrc = () => {
    const width = window.innerWidth
    
    if (width < 768) return srcSet.mobile
    if (width < 1024) return srcSet.tablet
    return srcSet.desktop
  }
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageSrc(getResponsiveSrc())
            observer.disconnect()
          }
        })
      }
    )
    
    if (imgRef.current) {
      observer.observe(imgRef.current)
    }
    
    return () => observer.disconnect()
  }, [])
  
  return <img ref={imgRef} src={imageSrc} alt={alt} />
}

第四部分:预加载策略

4.1 Link标签预加载

typescript
// src/utils/preload.ts
export function preloadImage(src: string) {
  const link = document.createElement('link')
  link.rel = 'preload'
  link.as = 'image'
  link.href = src
  document.head.appendChild(link)
}

export function preloadScript(src: string) {
  const link = document.createElement('link')
  link.rel = 'preload'
  link.as = 'script'
  link.href = src
  document.head.appendChild(link)
}

export function preloadStyle(href: string) {
  const link = document.createElement('link')
  link.rel = 'preload'
  link.as = 'style'
  link.href = href
  document.head.appendChild(link)
}

4.2 React组件预加载

typescript
// src/utils/componentPreload.ts
import { ComponentType, lazy } from 'react'

interface LazyComponent {
  preload: () => Promise<{ default: ComponentType<any> }>
}

export function lazyWithPreload<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
) {
  const Component = lazy(factory) as typeof lazy & LazyComponent
  Component.preload = factory
  return Component
}

// 使用
const Dashboard = lazyWithPreload(() => import('./Dashboard'))

// 预加载
Dashboard.preload()

// 渲染
<Suspense fallback={<Loading />}>
  <Dashboard />
</Suspense>

4.3 路由预加载

typescript
// src/utils/routePreload.ts
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

const routeComponents = {
  '/dashboard': () => import('./pages/Dashboard'),
  '/profile': () => import('./pages/Profile'),
  '/settings': () => import('./pages/Settings'),
}

export function useRoutePreload() {
  const location = useLocation()
  
  useEffect(() => {
    // 预加载相邻路由
    const adjacentRoutes = getAdjacentRoutes(location.pathname)
    
    adjacentRoutes.forEach(route => {
      const component = routeComponents[route]
      if (component) {
        // 延迟预加载
        setTimeout(() => component(), 2000)
      }
    })
  }, [location])
}

function getAdjacentRoutes(currentPath: string): string[] {
  const routeMap: Record<string, string[]> = {
    '/': ['/dashboard', '/profile'],
    '/dashboard': ['/settings'],
    '/profile': ['/settings'],
  }
  
  return routeMap[currentPath] || []
}

4.4 鼠标悬停预加载

typescript
// src/components/PreloadLink.tsx
import { Link } from 'react-router-dom'
import { lazyWithPreload } from '@/utils/componentPreload'

const routeComponents = {
  '/dashboard': lazyWithPreload(() => import('@/pages/Dashboard')),
  '/profile': lazyWithPreload(() => import('@/pages/Profile')),
}

export function PreloadLink({ to, children }: { to: string, children: React.ReactNode }) {
  const handleMouseEnter = () => {
    const component = routeComponents[to]
    if (component?.preload) {
      component.preload()
    }
  }
  
  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  )
}

第五部分:Prefetch和Preload

5.1 Webpack魔法注释

typescript
// Prefetch - 空闲时加载
const Dashboard = lazy(() => 
  import(
    /* webpackPrefetch: true */
    /* webpackChunkName: "dashboard" */
    './Dashboard'
  )
)

// Preload - 立即加载
const CriticalComponent = lazy(() => 
  import(
    /* webpackPreload: true */
    /* webpackChunkName: "critical" */
    './CriticalComponent'
  )
)

生成的HTML:

html
<!-- Prefetch -->
<link rel="prefetch" href="dashboard.chunk.js">

<!-- Preload -->
<link rel="preload" href="critical.chunk.js" as="script">

5.2 动态Prefetch

typescript
// src/utils/prefetch.ts
export function prefetchComponent(importFn: () => Promise<any>) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => importFn())
  } else {
    setTimeout(() => importFn(), 1)
  }
}

// 使用
useEffect(() => {
  // 在空闲时预获取
  prefetchComponent(() => import('./HeavyComponent'))
}, [])

5.3 优先级控制

typescript
// src/utils/loadPriority.ts
export enum LoadPriority {
  IMMEDIATE = 'immediate',  // 立即加载
  HIGH = 'high',           // 高优先级
  NORMAL = 'normal',       // 正常优先级
  LOW = 'low',            // 低优先级
  IDLE = 'idle',          // 空闲时加载
}

export function loadWithPriority(
  importFn: () => Promise<any>,
  priority: LoadPriority
) {
  switch (priority) {
    case LoadPriority.IMMEDIATE:
      return importFn()
      
    case LoadPriority.HIGH:
      return new Promise((resolve) => {
        setTimeout(() => importFn().then(resolve), 100)
      })
      
    case LoadPriority.NORMAL:
      return new Promise((resolve) => {
        setTimeout(() => importFn().then(resolve), 500)
      })
      
    case LoadPriority.LOW:
      return new Promise((resolve) => {
        setTimeout(() => importFn().then(resolve), 2000)
      })
      
    case LoadPriority.IDLE:
      return new Promise((resolve) => {
        if ('requestIdleCallback' in window) {
          requestIdleCallback(() => importFn().then(resolve))
        } else {
          setTimeout(() => importFn().then(resolve), 3000)
        }
      })
  }
}

第六部分:无限滚动和虚拟列表

6.1 无限滚动

typescript
// src/components/InfiniteScroll.tsx
import { useEffect, useRef, useState } from 'react'

interface InfiniteScrollProps {
  loadMore: () => Promise<any[]>
  hasMore: boolean
  children: React.ReactNode
}

export function InfiniteScroll({ loadMore, hasMore, children }: InfiniteScrollProps) {
  const [isLoading, setIsLoading] = useState(false)
  const loaderRef = useRef<HTMLDivElement>(null)
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      async (entries) => {
        const target = entries[0]
        
        if (target.isIntersecting && hasMore && !isLoading) {
          setIsLoading(true)
          await loadMore()
          setIsLoading(false)
        }
      },
      {
        rootMargin: '100px',
      }
    )
    
    if (loaderRef.current) {
      observer.observe(loaderRef.current)
    }
    
    return () => observer.disconnect()
  }, [hasMore, isLoading, loadMore])
  
  return (
    <div>
      {children}
      <div ref={loaderRef}>
        {isLoading && <div>Loading...</div>}
      </div>
    </div>
  )
}

6.2 虚拟列表

typescript
// src/components/VirtualList.tsx
import { useRef, useState, useEffect } from 'react'

interface VirtualListProps {
  items: any[]
  itemHeight: number
  containerHeight: number
  renderItem: (item: any, index: number) => React.ReactNode
}

export function VirtualList({ items, itemHeight, containerHeight, renderItem }: VirtualListProps) {
  const [scrollTop, setScrollTop] = useState(0)
  const containerRef = useRef<HTMLDivElement>(null)
  
  const visibleCount = Math.ceil(containerHeight / itemHeight)
  const startIndex = Math.floor(scrollTop / itemHeight)
  const endIndex = Math.min(startIndex + visibleCount + 1, items.length)
  
  const visibleItems = items.slice(startIndex, endIndex)
  const offsetY = startIndex * itemHeight
  
  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop)
  }
  
  return (
    <div
      ref={containerRef}
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={startIndex + index} style={{ height: itemHeight }}>
              {renderItem(item, startIndex + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

第七部分:性能优化

7.1 加载时机优化

typescript
// src/utils/loadTiming.ts
export function loadWhenIdle(importFn: () => Promise<any>) {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => importFn(), { timeout: 2000 })
  } else {
    setTimeout(() => importFn(), 1)
  }
}

export function loadOnInteraction(
  element: HTMLElement,
  importFn: () => Promise<any>
) {
  const loadHandler = () => {
    importFn()
    element.removeEventListener('click', loadHandler)
    element.removeEventListener('mouseenter', loadHandler)
  }
  
  element.addEventListener('click', loadHandler, { once: true })
  element.addEventListener('mouseenter', loadHandler, { once: true })
}

export function loadOnVisible(
  element: HTMLElement,
  importFn: () => Promise<any>
) {
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting) {
        importFn()
        observer.disconnect()
      }
    },
    { rootMargin: '50px' }
  )
  
  observer.observe(element)
}

7.2 缓存策略

typescript
// src/utils/cache.ts
const componentCache = new Map<string, any>()

export function cachedLazy(
  key: string,
  importFn: () => Promise<any>
) {
  if (componentCache.has(key)) {
    return Promise.resolve(componentCache.get(key))
  }
  
  return importFn().then(module => {
    componentCache.set(key, module)
    return module
  })
}

// 使用
const Dashboard = lazy(() => 
  cachedLazy('dashboard', () => import('./Dashboard'))
)

7.3 资源提示

typescript
// src/utils/resourceHints.ts
export function addPreconnect(url: string) {
  const link = document.createElement('link')
  link.rel = 'preconnect'
  link.href = url
  document.head.appendChild(link)
}

export function addDNSPrefetch(url: string) {
  const link = document.createElement('link')
  link.rel = 'dns-prefetch'
  link.href = url
  document.head.appendChild(link)
}

// 使用
useEffect(() => {
  addPreconnect('https://api.example.com')
  addDNSPrefetch('https://cdn.example.com')
}, [])

第八部分:完整示例

8.1 完整懒加载应用

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { lazyWithPreload } from './utils/componentPreload'
import { LazyImage } from './components/LazyImage'
import { InfiniteScroll } from './components/InfiniteScroll'

// 懒加载页面
const Home = lazy(() => import('./pages/Home'))
const Products = lazyWithPreload(() => import('./pages/Products'))
const ProductDetail = lazyWithPreload(() => import('./pages/ProductDetail'))
const Cart = lazyWithPreload(() => import('./pages/Cart'))

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/cart" element={<Cart />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

export default App

8.2 图片画廊示例

typescript
// src/components/ImageGallery.tsx
import { LazyImage } from './LazyImage'
import { useState } from 'react'

export function ImageGallery() {
  const [images] = useState([
    { id: 1, src: '/images/1.jpg', placeholder: '/images/1-thumb.jpg' },
    { id: 2, src: '/images/2.jpg', placeholder: '/images/2-thumb.jpg' },
    // ... more images
  ])
  
  return (
    <div className="gallery">
      {images.map(image => (
        <LazyImage
          key={image.id}
          src={image.src}
          placeholder={image.placeholder}
          alt={`Image ${image.id}`}
        />
      ))}
    </div>
  )
}

第八部分:高级预加载策略

8.1 智能预测预加载

typescript
// 基于用户行为预测
class SmartPreloader {
  private history: string[] = [];
  private patterns: Map<string, string[]> = new Map();
  
  recordNavigation(route: string) {
    this.history.push(route);
    
    // 分析导航模式
    if (this.history.length >= 2) {
      const prev = this.history[this.history.length - 2];
      const next = route;
      
      if (!this.patterns.has(prev)) {
        this.patterns.set(prev, []);
      }
      this.patterns.get(prev)!.push(next);
    }
    
    // 预加载可能的下一个页面
    this.predictAndPreload(route);
  }
  
  predictAndPreload(currentRoute: string) {
    const possibleNext = this.patterns.get(currentRoute) || [];
    
    // 找出最常访问的下一个页面
    const frequency = possibleNext.reduce((acc, route) => {
      acc[route] = (acc[route] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);
    
    // 预加载频率最高的路由
    const mostLikely = Object.entries(frequency)
      .sort(([, a], [, b]) => b - a)
      .slice(0, 2)
      .map(([route]) => route);
    
    mostLikely.forEach(route => {
      const component = routeComponents[route];
      if (component?.preload) {
        console.log(`Smart preloading: ${route}`);
        component.preload();
      }
    });
  }
}

// 全局使用
const preloader = new SmartPreloader();

function Router() {
  const location = useLocation();
  
  useEffect(() => {
    preloader.recordNavigation(location.pathname);
  }, [location.pathname]);
  
  return <Routes>...</Routes>;
}

8.2 网络感知预加载

typescript
// 根据网络状况调整预加载策略
function useNetworkAwarePreload() {
  const [connection, setConnection] = useState<{
    effectiveType: string;
    saveData: boolean;
  }>({
    effectiveType: '4g',
    saveData: false
  });
  
  useEffect(() => {
    if ('connection' in navigator) {
      const conn = (navigator as any).connection;
      
      const updateConnection = () => {
        setConnection({
          effectiveType: conn.effectiveType,
          saveData: conn.saveData
        });
      };
      
      updateConnection();
      conn.addEventListener('change', updateConnection);
      
      return () => {
        conn.removeEventListener('change', updateConnection);
      };
    }
  }, []);
  
  const shouldPreload = useMemo(() => {
    // 省流量模式下不预加载
    if (connection.saveData) return false;
    
    // 只在4G或更好的网络下预加载
    return ['4g', 'wifi'].includes(connection.effectiveType);
  }, [connection]);
  
  return shouldPreload;
}

// 使用
function App() {
  const shouldPreload = useNetworkAwarePreload();
  
  useEffect(() => {
    if (shouldPreload) {
      // 预加载资源
      preloadRoutes(['/products', '/cart']);
    }
  }, [shouldPreload]);
  
  return <Router />;
}

8.3 时间片预加载

typescript
// 利用浏览器空闲时间预加载
function useIdlePreload(resources: string[]) {
  useEffect(() => {
    if ('requestIdleCallback' in window) {
      const preloadQueue = [...resources];
      
      const preloadNext = (deadline: IdleDeadline) => {
        // 在空闲时间预加载
        while (deadline.timeRemaining() > 0 && preloadQueue.length > 0) {
          const resource = preloadQueue.shift()!;
          
          if (resource.endsWith('.js')) {
            const link = document.createElement('link');
            link.rel = 'preload';
            link.as = 'script';
            link.href = resource;
            document.head.appendChild(link);
          } else if (resource.endsWith('.css')) {
            const link = document.createElement('link');
            link.rel = 'preload';
            link.as = 'style';
            link.href = resource;
            document.head.appendChild(link);
          }
        }
        
        // 如果还有资源,继续在下次空闲时预加载
        if (preloadQueue.length > 0) {
          requestIdleCallback(preloadNext);
        }
      };
      
      requestIdleCallback(preloadNext);
    }
  }, [resources]);
}

// 使用
function App() {
  useIdlePreload([
    '/static/js/products.chunk.js',
    '/static/js/cart.chunk.js',
    '/static/css/theme.css'
  ]);
  
  return <Router />;
}

第九部分:渐进式图片加载

9.1 LQIP (低质量图片占位)

typescript
// 低质量图片占位符
function ProgressiveImage({ 
  src, 
  placeholder, 
  alt 
}: {
  src: string;
  placeholder: string;
  alt: string;
}) {
  const [currentSrc, setCurrentSrc] = useState(placeholder);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const img = new Image();
    img.src = src;
    
    img.onload = () => {
      setCurrentSrc(src);
      setLoading(false);
    };
  }, [src]);
  
  return (
    <div className="progressive-image">
      <img
        src={currentSrc}
        alt={alt}
        className={loading ? 'loading' : 'loaded'}
        style={{
          filter: loading ? 'blur(10px)' : 'none',
          transition: 'filter 0.3s'
        }}
      />
    </div>
  );
}

// 使用
<ProgressiveImage
  placeholder="/images/product-thumb.jpg" // 10KB
  src="/images/product-full.jpg"          // 500KB
  alt="Product"
/>

9.2 BlurHash占位符

bash
npm install blurhash react-blurhash
typescript
import { Blurhash } from 'react-blurhash';

function BlurHashImage({ 
  src, 
  hash, 
  width, 
  height 
}: {
  src: string;
  hash: string;
  width: number;
  height: number;
}) {
  const [imageLoaded, setImageLoaded] = useState(false);
  
  return (
    <div style={{ position: 'relative', width, height }}>
      {!imageLoaded && (
        <Blurhash
          hash={hash}
          width={width}
          height={height}
          resolutionX={32}
          resolutionY={32}
          punch={1}
        />
      )}
      <img
        src={src}
        onLoad={() => setImageLoaded(true)}
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          opacity: imageLoaded ? 1 : 0,
          transition: 'opacity 0.3s'
        }}
      />
    </div>
  );
}

// 使用
<BlurHashImage
  src="/images/product.jpg"
  hash="LGF5]+Yk^6#M@-5c,1J5@[or[Q6."
  width={400}
  height={300}
/>

9.3 响应式图片加载

typescript
// 根据设备加载不同尺寸的图片
function ResponsiveImage({ 
  srcSet, 
  sizes, 
  alt 
}: {
  srcSet: {
    small: string;
    medium: string;
    large: string;
  };
  sizes?: string;
  alt: string;
}) {
  return (
    <img
      srcSet={`
        ${srcSet.small} 400w,
        ${srcSet.medium} 800w,
        ${srcSet.large} 1200w
      `}
      sizes={sizes || "(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}
      alt={alt}
      loading="lazy"
    />
  );
}

// 使用
<ResponsiveImage
  srcSet={{
    small: '/images/product-400.jpg',
    medium: '/images/product-800.jpg',
    large: '/images/product-1200.jpg'
  }}
  alt="Product"
/>

第十部分:虚拟滚动优化

10.1 react-window虚拟滚动

bash
npm install react-window
typescript
import { FixedSizeList } from 'react-window';

// 大列表虚拟滚动
function VirtualList({ items }: { items: any[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <ProductCard product={items[index]} />
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={120}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// 动态高度列表
import { VariableSizeList } from 'react-window';

function DynamicList({ items }: { items: any[] }) {
  const listRef = useRef<VariableSizeList>(null);
  const rowHeights = useRef<{ [key: number]: number }>({});
  
  const getItemSize = (index: number) => {
    return rowHeights.current[index] || 100;
  };
  
  const setRowHeight = (index: number, size: number) => {
    if (rowHeights.current[index] !== size) {
      rowHeights.current[index] = size;
      listRef.current?.resetAfterIndex(index);
    }
  };
  
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
    const rowRef = useRef<HTMLDivElement>(null);
    
    useEffect(() => {
      if (rowRef.current) {
        setRowHeight(index, rowRef.current.clientHeight);
      }
    }, [index]);
    
    return (
      <div style={style}>
        <div ref={rowRef}>
          <ProductCard product={items[index]} />
        </div>
      </div>
    );
  };
  
  return (
    <VariableSizeList
      ref={listRef}
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </VariableSizeList>
  );
}

10.2 react-virtuoso高级虚拟滚动

bash
npm install react-virtuoso
typescript
import { Virtuoso } from 'react-virtuoso';

// 更简单的虚拟滚动
function VirtuosoList({ items }: { items: any[] }) {
  return (
    <Virtuoso
      style={{ height: '600px' }}
      data={items}
      itemContent={(index, item) => (
        <ProductCard product={item} />
      )}
      endReached={() => {
        // 加载更多
        console.log('Load more');
      }}
    />
  );
}

// 带分组的虚拟滚动
import { GroupedVirtuoso } from 'react-virtuoso';

function GroupedList({ 
  groups, 
  items 
}: { 
  groups: string[]; 
  items: any[] 
}) {
  const groupCounts = groups.map(g => 
    items.filter(i => i.category === g).length
  );
  
  return (
    <GroupedVirtuoso
      style={{ height: '600px' }}
      groupCounts={groupCounts}
      groupContent={index => (
        <div className="group-header">
          {groups[index]}
        </div>
      )}
      itemContent={index => (
        <ProductCard product={items[index]} />
      )}
    />
  );
}

第十一部分:预连接和DNS预解析

11.1 资源提示优化

html
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <!-- DNS预解析 -->
  <link rel="dns-prefetch" href="https://api.example.com">
  <link rel="dns-prefetch" href="https://cdn.example.com">
  
  <!-- 预连接(DNS+TCP+TLS) -->
  <link rel="preconnect" href="https://api.example.com">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  
  <!-- 预加载关键资源 -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/static/css/main.css" as="style">
  <link rel="preload" href="/static/js/main.js" as="script">
  
  <!-- 预获取下一页资源 -->
  <link rel="prefetch" href="/static/js/products.chunk.js">
  <link rel="prefetch" href="/static/js/cart.chunk.js">
  
  <!-- 预渲染下一页(谨慎使用) -->
  <link rel="prerender" href="/products">
</head>
<body>
  <div id="root"></div>
</body>
</html>

11.2 动态资源提示

typescript
// 动态添加资源提示
function addResourceHint(
  type: 'dns-prefetch' | 'preconnect' | 'preload' | 'prefetch',
  href: string,
  as?: string
) {
  const link = document.createElement('link');
  link.rel = type;
  link.href = href;
  
  if (as) {
    link.as = as;
  }
  
  // 避免重复
  const existing = document.querySelector(`link[rel="${type}"][href="${href}"]`);
  if (!existing) {
    document.head.appendChild(link);
  }
}

// 路由变化时添加预连接
function useRoutePreconnect() {
  const location = useLocation();
  
  useEffect(() => {
    const routeAPIs = {
      '/products': 'https://api.example.com/products',
      '/user': 'https://api.example.com/user',
      '/cart': 'https://api.example.com/cart'
    };
    
    const apiUrl = routeAPIs[location.pathname as keyof typeof routeAPIs];
    
    if (apiUrl) {
      const domain = new URL(apiUrl).origin;
      addResourceHint('preconnect', domain);
    }
  }, [location.pathname]);
}

第十二部分:实战优化案例

12.1 电商产品列表优化

typescript
// 优化前: 一次加载所有产品
function ProductListOld({ products }: { products: Product[] }) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
// 问题: 
// - 10000个产品渲染卡顿
// - 大量图片同时加载
// - 首屏渲染慢

// 优化后: 虚拟滚动 + 图片懒加载
import { Virtuoso } from 'react-virtuoso';

function ProductListOptimized({ products }: { products: Product[] }) {
  return (
    <Virtuoso
      style={{ height: '100vh' }}
      data={products}
      itemContent={(index, product) => (
        <LazyProductCard product={product} />
      )}
      overscan={200} // 预渲染200px
    />
  );
}

function LazyProductCard({ product }: { product: Product }) {
  return (
    <div className="product-card">
      <LazyLoadImage
        src={product.image}
        alt={product.name}
        threshold={100}
        placeholder={<Skeleton />}
      />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

// 性能对比:
// 首屏渲染: 5s -> 0.8s (↓84%)
// 内存使用: 500MB -> 50MB (↓90%)
// FPS: 20 -> 60 (↑200%)

12.2 文章阅读器优化

typescript
// 长文章分段懒加载
function ArticleReader({ articleId }: { articleId: string }) {
  const [sections, setSections] = useState<Section[]>([]);
  const [loadedSections, setLoadedSections] = useState<Set<number>>(new Set([0]));
  
  useEffect(() => {
    // 只加载第一段
    fetch(`/api/articles/${articleId}/sections/0`)
      .then(res => res.json())
      .then(section => setSections([section]));
  }, [articleId]);
  
  const loadSection = useCallback((index: number) => {
    if (loadedSections.has(index)) return;
    
    fetch(`/api/articles/${articleId}/sections/${index}`)
      .then(res => res.json())
      .then(section => {
        setSections(prev => [...prev, section]);
        setLoadedSections(prev => new Set([...prev, index]));
      });
  }, [articleId, loadedSections]);
  
  return (
    <div className="article">
      {sections.map((section, index) => (
        <div key={index}>
          <ArticleSection content={section.content} />
          
          {/* 占位符,用户滚动到此处时加载下一段 */}
          {index === sections.length - 1 && (
            <IntersectionObserver
              onChange={(inView) => {
                if (inView) loadSection(index + 1);
              }}
            >
              <div>加载中...</div>
            </IntersectionObserver>
          )}
        </div>
      ))}
    </div>
  );
}

// 收益:
// 初始加载: 从完整文章变为首屏内容
// 流量节省: 用户平均只读30%,节省70%流量
// 加载速度: 3s -> 0.5s

总结

本章全面介绍了懒加载和预加载:

  1. 懒加载基础 - 理解懒加载概念和优势
  2. 组件懒加载 - React组件的懒加载实现
  3. 图片懒加载 - 图片资源的优化加载
  4. 预加载策略 - 提前加载关键资源
  5. Prefetch/Preload - 资源加载优先级
  6. 无限滚动 - 动态加载列表数据
  7. 性能优化 - 加载时机和缓存策略
  8. 高级策略 - 智能预测、网络感知、时间片
  9. 渐进式加载 - LQIP、BlurHash占位符
  10. 虚拟滚动 - react-window和react-virtuoso
  11. 资源提示 - DNS预解析、预连接
  12. 实战案例 - 电商和内容平台优化

合理使用懒加载和预加载是优化应用性能的关键。

扩展阅读