客路商品详情页前端性能优化实战
一、现状分析
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');
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
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';
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
// 懒加载详情页组件
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();
});
}
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
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';
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> = {};
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 预渲染
需要我针对某个具体优化点,比如图片优化或代码分割,提供更详细的实现方案吗?