Appearance
懒加载与预加载
课程概述
本章节深入探讨懒加载(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 App8.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-blurhashtypescript
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-windowtypescript
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-virtuosotypescript
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总结
本章全面介绍了懒加载和预加载:
- 懒加载基础 - 理解懒加载概念和优势
- 组件懒加载 - React组件的懒加载实现
- 图片懒加载 - 图片资源的优化加载
- 预加载策略 - 提前加载关键资源
- Prefetch/Preload - 资源加载优先级
- 无限滚动 - 动态加载列表数据
- 性能优化 - 加载时机和缓存策略
- 高级策略 - 智能预测、网络感知、时间片
- 渐进式加载 - LQIP、BlurHash占位符
- 虚拟滚动 - react-window和react-virtuoso
- 资源提示 - DNS预解析、预连接
- 实战案例 - 电商和内容平台优化
合理使用懒加载和预加载是优化应用性能的关键。