记录一次 website 性能优化与思考

『23年07月11日』

GFM 是我司的一个电商站点,主要技术架构是基于 Next.js 做的 SSR App。

一直以来存在着一些性能问题:

  • 页面跳转很慢 2-3s
  • 商品详情页访问很慢 2-3s
  • 商品分类、集合、seller site 访问很慢 2-3s

随着对项目的进一步熟悉,也渐渐的发现了导致访问慢的一些原因,然后针对性的做了一些优化。

原因:

  1. 站点有通用的头部和底部菜单栏,抽象出了一个Layout组件。每个页面都会复用这个Layout,但是 Layout 所需的菜单数据都是在每个页面各自的getServerSideProps function 里去请求,总共四条接口,即每个页面都要重复的去请求这些接口,而Node层串行的去请求这三个 Graphql 接口大概需要 1.5s;
  2. 菜单由第三方系统配置和管理,其提供的接口访问速度慢;
  3. 整个站点几乎不做任何的 cache control 策略;

怎么处理比较好?

  1. 把所有的串行接口改为并行执行,这个比较简单。效果如下:

fetch menu 接口串行:
fetch menu

fetch menu 接口并行:
fetch menu

但是由于第三方实在是慢,就请求几个菜单数据响应时间都要几百毫秒级别。:) 这也可能跟其服务是在美国有关,而我们在天朝,加了墙的天朝。。 总之,串行改并行后大概节省了 1s 的时间。当然,我们经常会在客户端做这样的事情,但是这里,需要跳脱客户端的思维,以服务端的角度去思考这个问题,即一台 Node 服务器需要同时接受成百上千条请求,那么 node 的网络 I/O 并发瓶颈是多少?所以串行的实际效果是怎么样的?

  1. 结合实际,头部菜单和底部菜单都是一些不常变的数据,我们可以做一个 API 数据的缓存。

处理 1 的基础上,抽象出一个公共的fetchMenu function,并将结果缓存为 Node 全局变量。


const cacheTime = 60 * 60 * 1000;
let lastCacheTime: number = 0;

async function fetchMenu(context: NextContext) {
  const { referer } = context.req.headers;
  const cacheTimeout = Date.now() - lastCacheTime > cacheTime;
  if (cache && referer && !cacheTimeout) {
    return cache;
  }

  // merge fetch menu requests

  // const response = await Promise.all([q1, q2, q3]);

  // xxx

  cache = {
    bannerData,
    menuData,
    secondaryMenuData,
  };
  lastCacheTime = Date.now();
  return cache;
}

使用了缓存策略:如果有人刷新了页面,即 HTTP header 的 referer 为空,这时就去重新请求菜单数据,并替换缓存。否则,直接使用缓存数据。并设置了一个一个小时的最大缓存时间。
再来看一下效果:
fetch menu3
可以看到,如果缓存命中的话,那响应就非常快了。

  1. Stable-While-Revalidation

Stable-While-Revalidate 是一个很好的缓存策略,浏览器也原生支持。SWR 就是基于这个思想去做的 API 请求的缓存策略。


context.res.setHeader("Cache-Control", "public, max-age=10, stale-while-revalidate=300");

我们会给几乎所有的页面加上这条缓存策略。public代表这条策略适用于所有客户端(浏览器,代理等),max-age=10代表当前请求缓存 10s,stale-while-revalidate=300代表在第10s-300s内重复请求会先返回缓存,并在background发起一条 revalidate 的请求,然后将请求结果替换为当前缓存,供下次返回给客户端,然后缓存时间会重新计算。

当我们使用了这样的缓存策略,基本能做到相同页面在第二次访问能够秒开了。