用React和Node.js打造高可用商业案例的实战经验分享
优化前:卡得不行
上个月接手一个移动端商品列表页,用户反馈“点进去就卡死”“滑动像PPT”。我本地跑起来一看,好家伙,首页加载完要5秒多,列表滚动掉帧严重,手指一松就卡在半空。更离谱的是,部分低端安卓机直接白屏,连错误日志都打不出来。
这页面其实逻辑不复杂:顶部轮播图 + 中间筛选栏 + 下方无限滚动商品列表。但问题就出在“简单”上——当初赶工期,谁都没做性能压测,数据一多就崩。
找到瓶颈了!
先用 Chrome DevTools 的 Performance 面板录了一次加载过程,发现主线程被 JS 占满,尤其在解析商品数据时有个长达 1.2 秒的长任务。再看 Network,图片资源没压缩,首屏 8 张图全高清,光图片就占了 3MB+。
接着用 Lighthouse 跑分,Performance 才 28 分。主要扣分项:未优化的图片、未延迟加载非关键 JS、DOM 节点爆炸(列表项每个都带 3 层嵌套,100 条数据就有 1000+ 节点)。
最致命的是,滚动监听里直接操作 DOM,还绑了节流但间隔设成 16ms(等于没节流),导致每次滚动都触发重排重绘。
核心优化:三板斧干下去
折腾了几天,最后靠这三招把性能拉回来了:
1. 虚拟滚动救大命
原方案是直接渲染所有商品,100 条数据全塞进 DOM。改成只渲染可视区域 + 缓冲区,比如屏幕高 600px,每项 120px,那就只渲染 7 项(5 可视 + 2 缓冲)。滚动时动态替换内容,DOM 节点从 1000+ 降到 10 个以内。
这里注意我踩过好几次坑:一开始用第三方库,结果和现有状态管理冲突;后来手写了一个轻量版,核心就维护一个 startIndex 和 endIndex,配合 transform 滚动偏移。
// 简化版虚拟滚动核心逻辑
const ITEM_HEIGHT = 120;
const BUFFER_SIZE = 2;
function getVisibleRange(scrollTop, containerHeight) {
const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER_SIZE);
const endIndex = Math.min(
totalItems - 1,
Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER_SIZE
);
return { startIndex, endIndex };
}
// 渲染时只 map 这个范围
const visibleItems = items.slice(startIndex, endIndex + 1);
2. 图片懒加载 + WebP 格式
原代码直接用 <img src="xxx.jpg">,首屏加载所有图。改成 Intersection Observer 监听进入视口再加载,同时后端接口支持 WebP 格式(通过 Accept 头判断)。
// 图片懒加载
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
imgObserver.unobserve(img);
}
});
});
// HTML 中
// <img data-src="https://jztheme.com/api/image/1.webp" loading="lazy" />
另外,构建时用 sharp 压缩图片,WebP 比 JPEG 小 40% 左右。首屏图片体积从 3MB 降到 800KB。
3. 滚动事件彻底解耦
原来的滚动监听里干了三件事:更新筛选栏吸顶状态、记录滚动位置、触发埋点。现在全部拆开:
- 吸顶用 CSS
position: sticky实现,零 JS - 滚动位置记录改用 passive 事件 + requestIdleCallback 延迟执行
- 埋点合并到离开页面时统一上报
// 滚动事件加 passive 提升性能
window.addEventListener('scroll', handleScroll, { passive: true });
// 非关键操作放 requestIdleCallback
function handleScroll() {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
saveScrollPosition(window.scrollY);
});
} else {
// 降级用 setTimeout
setTimeout(() => saveScrollPosition(window.scrollY), 100);
}
}
其他小修小补
还有一些次要但有效的改动:
- 把轮播图从 JS 库换成纯 CSS 动画(用
@keyframes),减少 JS 执行 - 商品卡片组件用 React.memo 包裹,避免重复渲染
- 接口响应加缓存,30 秒内重复请求走内存缓存
这些改动单独看效果不大,但堆起来也有 100-200ms 的提升。
性能数据对比
优化前后实测数据(中端安卓机,4G 网络):
- 首屏加载时间:5.2s → 780ms
- 滚动 FPS:12-18 → 稳定 55-60
- Lighthouse Performance 分数:28 → 89
- 白屏率:15% → 0%
最明显的是用户反馈变了:“现在滑得飞起”“终于不用等半天了”。虽然还有个别低端机偶发卡顿,但无大碍,业务方已经很满意了。
最后说两句
这次优化让我深刻体会到:移动端性能不是“有空再搞”,而是直接影响转化率。很多问题其实早有成熟方案,关键是要动手测、动手改。
以上是我个人对这个商品列表页的完整优化过程,有更优的实现方式欢迎评论区交流。比如虚拟滚动是否该用现成库?WebP 兼容性怎么兜底?这些细节都值得讨论。这个技巧的拓展用法还有很多,后续会继续分享这类实战博客。

暂无评论