记录一次 website 性能优化与思考
GFM 是我司的一个电商站点,主要技术架构是基于 Next.js 做的 SSR App。
一直以来存在着一些性能问题:
- 页面跳转很慢 2-3s
- 商品详情页访问很慢 2-3s
- 商品分类、集合、seller site 访问很慢 2-3s
随着对项目的进一步熟悉,也渐渐的发现了导致访问慢的一些原因,然后针对性的做了一些优化。
原因:
- 站点有通用的头部和底部菜单栏,抽象出了一个
Layout
组件。每个页面都会复用这个Layout
,但是 Layout 所需的菜单数据都是在每个页面各自的getServerSideProps
function 里去请求,总共四条接口,即每个页面都要重复的去请求这些接口,而Node层
串行的去请求这三个 Graphql 接口大概需要1.5s
; - 菜单由第三方系统配置和管理,其提供的接口访问速度慢;
- 整个站点几乎不做任何的
cache control
策略;
怎么处理比较好?
- 把所有的串行接口改为并行执行,这个比较简单。效果如下:
fetch menu 接口串行:
fetch menu 接口并行:
但是由于第三方实在是慢,就请求几个菜单数据响应时间都要几百毫秒级别。:) 这也可能跟其服务是在美国有关,而我们在天朝,加了墙的天朝。。 总之,串行改并行后大概节省了 1s
的时间。当然,我们经常会在客户端做这样的事情,但是这里,需要跳脱客户端的思维,以服务端的角度去思考这个问题,即一台 Node 服务器需要同时接受成百上千条请求,那么 node 的网络 I/O 并发瓶颈是多少?所以串行的实际效果是怎么样的?
- 结合实际,头部菜单和底部菜单都是一些不常变的数据,我们可以做一个 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 为空,这时就去重新请求菜单数据,并替换缓存。否则,直接使用缓存数据。并设置了一个一个小时的最大缓存时间。
再来看一下效果:
可以看到,如果缓存命中的话,那响应就非常快了。
- 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 的请求,然后将请求结果替换为当前缓存,供下次返回给客户端,然后缓存时间会重新计算。
当我们使用了这样的缓存策略,基本能做到相同页面在第二次访问能够秒开了。