用React和Node.js打造高可用商业案例的实战经验分享

振巧 移动 阅读 786
赞 18 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上个月接手一个移动端商品列表页,用户反馈“点进去就卡死”“滑动像PPT”。我本地跑起来一看,好家伙,首页加载完要5秒多,列表滚动掉帧严重,手指一松就卡在半空。更离谱的是,部分低端安卓机直接白屏,连错误日志都打不出来。

用React和Node.js打造高可用商业案例的实战经验分享

这页面其实逻辑不复杂:顶部轮播图 + 中间筛选栏 + 下方无限滚动商品列表。但问题就出在“简单”上——当初赶工期,谁都没做性能压测,数据一多就崩。

找到瓶颈了!

先用 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 兼容性怎么兜底?这些细节都值得讨论。这个技巧的拓展用法还有很多,后续会继续分享这类实战博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论