Nuxt 项目性能优化调研
性能优化,这是面试中经常会聊到的话题。我觉得性能优化应该因具体场景而异,因不同项目而异,不同的手段不同的方案并不一定适合所有项目,当然这其中不乏一些普适的方案,比如耳熟能详的文件压缩,文件缓存,CDN,DNS 预解析,等等,但是我更希望听到的是因为不同的项目不同的需求,解决不同的问题而采取的不同的优化手段,比如 BigPipe,分段输出页面的各个部分,对于 SNS 网站是非常合适的,减少了用户的等待时间;相对应的还有一个 BigRender,这是一个大的延迟加载,360 导航首页目前还在使用,京东淘宝首页也是这个思路,对于一些类门户网站非常适用,但是如果你的网页内容不是非常多,就没有必要了
今天要说的是 Nuxt。Nuxt 是支持 Vue SSR 的一个框架,底层需要运行 Node 服务。大概描述一下 Vue 的渲染过程,首先每个组件都会被编译生成一个渲染函数(这部分基本 webpack 打包已经做掉),然后渲染函数生成虚拟 dom,最后虚拟 dom 通过 patch 方法将真实 dom 渲染到页面上。Nuxt 其实就是将这部分放到了服务端去做,在服务端拿到渲染页面所需要的 html,从而使得 html 能够直出,而客户端其实还是会运行整个 Vue 的生命周期,这就带来了一个问题,这部分操作放在了服务端其实是非常耗 cpu 的,创建组件实例和虚拟 DOM 节点的开销,无法与纯基于字符串拼接的模版的性能相当,如果是不加优化的 Nuxt 项目,高并发下是很脆弱的,毕竟 Node 运行在单线程下,不适合 cpu 操作密集型的场景
使用 Nuxt 的项目无非看中了它的两大优点,一是服务端渲染满足 SEO 的需求,二是首屏直出比 SPA 快,再加上如果如果公司是 Vue 系,使用 Nuxt 就更顺理成章。但是不要忘了性能,高并发下 Nuxt 性能确实不乐观,我测试了官网的 hackernews demo 项目,2 核 cpu + 4g 内存,400 并发下它的吞吐量不超过 50,就算是最简的 Nuxt 项目,吞吐量也就 300+,这就说明如果项目不做缓存,300+ 已经是最大的吞吐量了,而最小 express demo 可以轻松到 3000,这就决定了高流量项目并不会轻易去使用 Nuxt
我们的项目目前其实是一个不加优化的 Nuxt 项目,因为用户不多,平时并没有什么问题,但是一到展会,就会有不少用户同时访问,反馈页面会很卡。同条件下做了压测后,吞吐量也是 50 上下,平均响应时长七八秒,所以卡是正常现象
看了一下项目代码,发现了几个问题:
-
项目没做缓存,所以每次访问都会经历所有 Nuxt 生命周期,消耗 cpu,这点是最致命的
-
项目打包默认 gzip。Nuxt 项目打包会默认在服务端开启 gzip,因为我们网关层已经做了 gzip,所以这里是不必要的,测试了下关掉 gzip 吞吐量和响应时间都能提高 20% 左右。具体做法是在 nuxt.config.js 中配置(还是得看 英文文档,会告诉你如何不设置
To disable compression, use compressor: false
,中文文档当时三月份我写这文的时候还没加这个选项,而目前中文文档也没有翻译这一句 2020-07-16)render: { compressor: false }
-
API 请求比较乱。很多请求并没有很好地区分客户端和服务端,而是都由服务端去做了,造成服务端压力过大,其实多数和用户有关的请求理应放到客户端。有的接口为了方便,一次性返回了所有内容,也没有做客户端/服务端区分。另外,服务端的接口请求可以并发,用类似 Promise.all 的形式去控制
-
SEO。有的内容页面,很长,有五个部分,除了内容外,还有猜你喜欢等其他部分,询问了 SEO 同事,说这几部分都是需要 SEO 的,我不是很懂 SEO,但是在我看来,ssr 只应该渲染首屏内容,而 UI 在设计的时候应该把主要内容设计到首屏,从而满足 SEO
对此我觉得可以从两个方向去优化:
-
缓存。缓存是最重要的方案,针对 Nuxt 项目可以做三级缓存,页面缓存、组件缓存以及 API 缓存。页面缓存是最重量级的缓存方案,能不能做页面缓存可以从以下两个点判断:
- 同一个 URL,对于 登录 / 非登录 用户,服务端渲染的内容是相同的(注意是服务端渲染内容,而非前端)
- 同一个 URL,对于不同的登录用户,服务端渲染的内容是相同的,即没有一些个性化的渲染(常见的个性化渲染,比如针对不同用户渲染不同的猜你喜欢内容等)
其实也就是返回的 html 代码相同就好,主要关注下返回的全局 store 是否一致,另外也不能做一些服务端才能做的操作,比如 set-cookie 等
-
控制好首屏模块个数,对返回的结果进行精简,最小化,保证吐出到浏览器的内容足够小。这就是前面说的并不要对所有模块都做 ssr,需要首屏呈现的/需要爬虫爬的,我们直出,其他部分做 CSR 就行了
而我们的网站大部分页面是满足做页面缓存条件的,测试了下如果做页面缓存,吞吐量能到 500+,这个数据这个时候其实是和页面大小有关系了,页面缓存的性能是能满足需求的。而有另一类页面,相同的 URL 会返回不同的内容,而且整页都是不同内容,它的实现是获取 cookie 中的不同 city-id,渲染不同城市的内容,很显然这部分页面做不了页面缓存了,API 缓存和组件缓存理论上都是可以试试的
做缓存优化,至少需要访问一次,第二次才能生效,那么还有另一种情况,对于这样的路由 /store/:id
,并发打开 id 0~1000,很显然每个页面都是不一样的店铺数据,并不能命中缓存(可能命中组件缓存,暂时忽略),这个时候只能从 Nuxt 生命周期上去优化了,那么以上方向的第二点,控制首屏模块个数就能用到了。所以本文一开始我就说,不同的方案是适配不同的场景的,解决不同的问题会采取不同的手段