客路商品详情页前端性能优化实战
客路商品详情页前端性能优化实战
一、现状分析
1.1 页面特征
商品详情页是典型的重页面,包含:
高清商品图片/视频
复杂的SKU选择器
富文本商品描述
用户评价列表
推荐商品模块
埋点统计
1.2 常见性能问题
FCP (First Contentful Paint): > 3s LCP (Largest Contentful Paint): > 4s CLS (Cumulative Layout Shift): > 0.1 TTI (Time to Interactive): > 5s二、加载策略优化
2.1 骨架屏优化
// React + Tailwind 骨架屏组件 const ProductSkeleton = () => ( <div className="animate-pulse"> {/* 图片区域 */} <div className="aspect-square bg-gray-200 rounded-lg mb-4" /> {/* 标题 */} <div className="h-6 bg-gray-200 rounded w-3/4 mb-2" /> {/* 价格 */} <div className="h-8 bg-gray-200 rounded w-1/3 mb-4" /> {/* SKU选择 */} <div className="space-y-3"> <div className="h-4 bg-gray-200 rounded w-1/4" /> <div className="flex gap-2"> {[1, 2, 3].map(i => ( <div key={i} className="h-10 w-20 bg-gray-200 rounded" /> ))} </div> </div> </div> );2.2 渐进式加载策略
// 图片懒加载 + 优先级控制 interface ImageConfig { src: string; priority?: 'high' | 'low' | 'auto'; placeholder?: string; } const LazyImage = ({ src, priority = 'auto', placeholder }: ImageConfig) => { const imgRef = useRef<HTMLImageElement>(null); useEffect(() => { if ('loading' in HTMLImageElement.prototype) { // Native lazy loading if (imgRef.current) { imgRef.current.loading = priority === 'high' ? 'eager' : 'lazy'; } } }, [priority]); return ( <img ref={imgRef} src={src} loading={priority === 'high' ? 'eager' : 'lazy'} decoding="async" fetchPriority={priority === 'high' ? 'high' : 'auto'} onLoad={(e) => { e.currentTarget.classList.add('loaded'); }} style={{ backgroundImage: placeholder ? `url(${placeholder})` : undefined, backgroundSize: 'cover' }} /> ); };三、资源优化
3.1 图片优化策略
// webpack/image-loader.config.js module.exports = { module: { rules: [ { test: /\.(png|jpe?g|webp|avif)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 8 * 1024, // 8kb 内联 }, }, generator: { filename: 'images/[name].[hash][ext]', }, }, ], }, };/* 响应式图片 */ .product-image { /* 移动端优先 */ background-image: url('product-mobile.jpg'); background-size: cover; } @media (min-width: 768px) { .product-image { background-image: url('product-tablet.jpg'); } } @media (min-width: 1200px) { .product-image { background-image: url('product-desktop.webp'); } }3.2 WebP 转换策略
// 服务端图片处理中间件 const sharp = require('sharp'); async function optimizeImage(req, res, next) { const { format = 'webp', quality = 80, width } = req.query; try { const image = sharp('./uploads/' + req.params.filename); if (width) { image.resize(width); } const optimizedBuffer = await image .toFormat(format, { quality }) .toBuffer(); res.set({ 'Content-Type': `image/${format}`, 'Cache-Control': 'public, max-age=31536000', }); res.send(optimizedBuffer); } catch (error) { next(error); } }四、渲染优化
4.1 SKU选择器虚拟化
import { FixedSizeGrid as Grid } from 'react-window'; # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex // SKU网格虚拟化 - 支持上千个SKU组合 const SkuSelector = ({ skus, columns }) => { const rowCount = Math.ceil(skus.length / columns); const Cell = ({ columnIndex, rowIndex, style }) => { const index = rowIndex * columns + columnIndex; const sku = skus[index]; if (!sku) return null; return ( <div style={style}> <button className={`sku-btn ${selectedSku === sku.id ? 'active' : ''}`} onClick={() => handleSelect(sku)} > {sku.name} </button> </div> ); }; return ( <Grid columnCount={columns} columnWidth={100} height={300} rowCount={rowCount} rowHeight={40} width={columns * 100} > {Cell} </Grid> ); };4.2 虚拟列表评价组件
import { VariableSizeList as List } from 'react-window'; # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex const ReviewList = ({ reviews }) => { const listRef = useRef<List>(null); const getItemSize = (index) => { const review = reviews[index]; // 根据内容动态计算高度 const baseHeight = 120; const textHeight = review.content.length > 100 ? 60 : 30; return baseHeight + textHeight; }; const Row = ({ index, style }) => { const review = reviews[index]; return ( <div style={style}> <ReviewItem review={review} /> </div> ); }; return ( <List ref={listRef} height={500} itemCount={reviews.length} itemSize={getItemSize} width="100%" > {Row} </List> ); };五、代码分割与按需加载
5.1 路由级代码分割
// routes/product.tsx import { lazy, Suspense } from 'react'; // 懒加载详情页组件 const ProductDetail = lazy(() => import('../components/ProductDetail')); const SkuSelector = lazy(() => import('../components/SkuSelector')); const ReviewSection = lazy(() => import('../components/ReviewSection')); const ProductPage = () => { return ( <Suspense fallback={<ProductSkeleton />}> <ProductDetail /> <Suspense fallback={<div>Loading SKU...</div>}> <SkuSelector /> </Suspense> <Suspense fallback={<div>Loading Reviews...</div>}> <ReviewSection /> </Suspense> </Suspense> ); };5.2 组件级动态导入
// 大体积组件动态加载 class ProductService { static loadVideoPlayer() { return import('../components/VideoPlayer').then(({ default: VideoPlayer }) => { return new VideoPlayer(); }); } static loadARViewer() { return import('../components/ARViewer').then(({ default: ARViewer }) => { return new ARViewer(); }); } } // 用户交互后加载 const handlePlayVideo = async () => { const videoPlayer = await ProductService.loadVideoPlayer(); videoPlayer.show(product.videoUrl); };六、缓存策略
6.1 Service Worker 缓存
// sw.js - 商品详情页离线缓存 const CACHE_NAME = 'product-detail-v1'; const ASSETS_TO_CACHE = [ '/styles/main.css', '/scripts/vendor.js', '/images/placeholder.jpg', ]; # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE)) ); }); self.addEventListener('fetch', (event) => { // API请求网络优先,静态资源缓存优先 if (event.request.url.includes('/api/')) { event.respondWith(networkFirst(event.request)); } else { event.respondWith(cacheFirst(event.request)); } }); function cacheFirst(request) { return caches.match(request).then( (response) => response || fetch(request) ); } function networkFirst(request) { return fetch(request).then( (response) => { const cloned = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, cloned)); return response; } ).catch(() => caches.match(request)); }6.2 HTTP 缓存配置
# nginx.conf location ~* \.(js|css|woff2?|ttf|eot)$ { add_header Cache-Control "public, max-age=31536000, immutable"; add_header Vary Accept-Encoding; } location /api/product/ { add_header Cache-Control "public, max-age=60, s-maxage=300"; add_header Vary User-Agent; } location /api/reviews/ { add_header Cache-Control "public, max-age=300, stale-while-revalidate=60"; }七、数据层优化
7.1 请求合并与去重
// api/client.ts class ApiClient { private pendingRequests = new Map<string, Promise<any>>(); # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex async request(url: string, options?: RequestInit): Promise<any> { const key = `${options?.method || 'GET'}:${url}`; // 去重相同请求 if (this.pendingRequests.has(key)) { return this.pendingRequests.get(key); } const promise = fetch(url, options).then((res) => res.json()); this.pendingRequests.set(key, promise); try { return await promise; } finally { this.pendingRequests.delete(key); } } // 批量获取商品数据 async getProductsBatch(ids: number[]): Promise<Product[]> { const query = ids.map(id => `id=${id}`).join('&'); return this.request(`/api/products/batch?${query}`); } }7.2 数据预取
// 智能预取策略 class PrefetchManager { private prefetchedUrls = new Set<string>(); # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex // 视口内预取 observeElements(selectors: string[]) { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const dataUrl = entry.target.getAttribute('data-prefetch-url'); if (dataUrl && !this.prefetchedUrls.has(dataUrl)) { this.prefetchData(dataUrl); } } }); }, { rootMargin: '200px' } ); selectors.forEach((selector) => { document.querySelectorAll(selector).forEach((el) => { observer.observe(el); }); }); } private prefetchData(url: string) { this.prefetchedUrls.add(url); const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; link.as = 'fetch'; document.head.appendChild(link); } }八、监控与度量
8.1 Core Web Vitals 监控
// web-vitals.ts import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals'; # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex export function initWebVitals(callback: (metric: any) => void) { getCLS(callback); getFID(callback); getLCP(callback); getFCP(callback); getTTFB(callback); } // 发送到监控系统 function sendToAnalytics(metric: any) { navigator.sendBeacon('/api/analytics/web-vitals', JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, page: window.location.pathname, timestamp: Date.now(), })); }8.2 自定义性能指标
// performance-monitor.ts class PerformanceMonitor { private marks: Record<string, number> = {}; # 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex start(label: string) { this.marks[label] = performance.now(); } end(label: string) { const start = this.marks[label]; if (start) { const duration = performance.now() - start; console.log(`[Performance] ${label}: ${duration.toFixed(2)}ms`); delete this.marks[label]; return duration; } } // 测量首屏可交互时间 measureTTI() { const tti = performance.timing.domInteractive - performance.timing.navigationStart; return tti; } // 资源加载时间 getResourceTimings() { return performance.getEntriesByType('resource') .filter(r => r.name.includes('product')) .map(r => ({ name: r.name, duration: r.duration, size: (r as any).transferSize, })); } }九、实战优化清单
9.1 上线前检查清单
## 性能优化 Checklist ### 资源加载 - [ ] 图片压缩 (TinyPNG/Sharp) - [ ] WebP 格式转换 - [ ] 字体子集化 - [ ] CDN 部署 ### 代码层面 - [ ] Tree Shaking 启用 - [ ] Code Splitting 配置 - [ ] Gzip/Brotli 压缩 - [ ] 无用代码移除 ### 渲染优化 - [ ] 虚拟化长列表 - [ ] 骨架屏实现 - [ ] CSS Containment - [ ] GPU 加速动画 ### 缓存策略 - [ ] HTTP 缓存头配置 - [ ] Service Worker 缓存 - [ ] IndexedDB 本地存储 - [ ] API 响应缓存 ### 监控体系 - [ ] Core Web Vitals 上报 - [ ] 错误监控集成 - [ ] 性能基线设定 - [ ] A/B 测试准备9.2 预期收益
优化项 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
FCP | 3.2s | 1.1s | 65% ↓ |
LCP | 4.5s | 2.3s | 49% ↓ |
TTI | 5.8s | 2.9s | 50% ↓ |
包体积 | 450KB | 180KB | 60% ↓ |
首屏流量 | 1.2MB | 450KB | 62% ↓ |
十、持续优化建议
建立性能预算:设置 LCP < 2.5s, CLS < 0.1 的硬性指标
自动化检测:CI/CD 流程中集成 Lighthouse CI
用户感知优化:关注 First Input Delay 和 Interaction to Next Paint
边缘计算:考虑将部分计算下沉到 CDN 边缘节点
预渲染:对热门商品使用 SSG 预渲染
需要我针对某个具体优化点,比如图片优化或代码分割,提供更详细的实现方案吗?
