Nuxt 项目性能优化调研分析

(编辑:jimmy 日期: 2024/11/13 浏览:2)

性能优化,这是面试中经常会聊到的话题。我觉得性能优化应该因具体场景而异,因不同项目而异,不同的手段不同的方案并不一定适合所有项目,当然这其中不乏一些普适的方案,比如耳熟能详的文件压缩,文件缓存,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 生命周期上去优化了,那么以上方向的第二点,控制首屏模块个数就能用到了。所以本文一开始我就说,不同的方案是适配不同的场景的,解决不同的问题会采取不同的手段

补充知识:Nuxt实现的SSR页面性能优化的进一步探索与实践

前言

本文之前,先简单介绍以下几个概念:

SSR指服务端渲染,即页面是通过服务端渲染生成后返回给客户端的,SSR主要为了提高页面加载速度,改善用户体验,也可用于SEO搜索引擎优化。

Nuxt.js 官方定义: Nuxt.js 是一个基于 Vue 的通用应用框架。 通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。我们的目标是创建一个灵活的应用框架,你可以基于它初始化新项目的基础结构代码,或者在已有 Node.js 项目中使用 Nuxt.js。

个人理解:Nuxt.js 就是预设了开发服务端渲染应用所需要的各种配置, 使用 Webpack 和 Node.js 进行封装的基于Vue的SSR框架。

背景

我们部门从事的都是面对用户的业务需求开发,面对用户,意味着对页面的体验要求会更高,最直观体验是页面首屏的加载速度,加载速度优化是我们体验优化的长期、重要的一部分;本文的起源正是首屏加载速度优化。

页面加载速度优化的核心包括三点:减少资源文件的请求数量;减小每个资源文件的大小;提高每个资源的加载速度;

诸如合并API访问,压缩混淆文件,支持webp图片,资源cdn缓存等等常用办法,都是以上面三个核心为出发点的; 这些常用办法基本都可以通过webpack配置,公司基础服务,代码较小的变更完成。

我们负责的各主流量入口页面,已基本做过以上常用的优化,但由于主入口页面资源量较大的原因,优化后并不能达到预期的效果,我们需要探索其它优化方案。 我们快速想到了用SSR的方案进一步解决加载速度问题,从零开始的搭建服务端渲染应用相当复杂,肯定会涉及到服务端的开发,作为独立的前端团队,成本较高昂; 我们决定尝试是否能找到一种成本较低的现有SSR框架,以达到目的;

因主入口页面技术栈为vue,方案调研中自然而然的看到了Nuxt.js此种基于Vue的SSR框架; Nuxt.js和项目技术栈匹配度急高,学习成本极低,自然成为我们的第一选择;

我们引入Nuxt.js,最初只是利用了服务端异步获取API接口数据和服务端渲染两项功能,去重构了我们的项目,重构后效果基本达到我们的预期,正常网络状态下,基本可以达到秒开; 入口页面,团队是作为一个长期的项目进行不定期优化的,我们逐步围绕Nuxt.js框架,对项目做了进一步优化升级,本文主要介绍我们Nuxt.js页面优化的进一步探索与实践; 至于如何搭建初步的Nuxt项目,需要感兴趣的各位自行查看官方文档及自我实践了,本文不做赘述。

探索与实践

我们主要的探索与实践可行方向主要有两个:

一、Nuxt.js特性合理应用

应用到的特性主要包括asyncData异步获取数据、mounted不支持服务端渲染、no-ssr组件不在服务端渲染中呈现;

通过相关特性做到API数据和页面结构合理拆分,首屏所需数据和结构通过服务端获取并渲染,非首屏数据和结构通过客户端获取并渲染。

示例代码:

no-ssr结构拆分

<template> 
 <div> 
 <!-- 顶部banner --> 
 <banner :banner="banner" /> 
 <!-- 非首屏所需结构,通过no-ssr组件达到不在服务端渲染目的--> 
 <no-ssr> 
  <!-- 商品列表 --> 
  <prod-list :listData="listData"/> 
 </no-ssr> 
 </div> 
</template>

API数据拆分

export default { 
 async asyncData({ app, query }) { 
 try { 
  // 获取页面顶部轮播图信息 
  const getBanner = () => { 
  return app.$axios.$get('zz/zy/banner') 
  } 
  // 获取底部配置信息 
  const getFooter = () => { 
  return app.$axios.$get('zz/zy/footer', { 
   params: { 
   smark: query.smark 
   } 
  }) 
  } 
  // 并发获取首屏数据,服务端获取 
  const [banner, footer] = await Promise.all([getBanner(), getFooter()]) 
  return {banner: banner, footer: footer} 
 } catch (e) { 
  console.log('interface timeout or format error => ', e) 
  return {} 
 } 
 }, 
 mounted() { 
 // 非首屏使用的数据, 客户端获取 
 this.loadListData() 
 }, 
 methods: { 
 loadListData() { 
  this.$axios.$get('zz/zy/list').then(() => { 
  // 数据处理逻辑 
  }) 
 } 
 } 
}

二、服务端引入缓存

服务端开发意味着缓存可作为性能优化的最直接法门,Nuxt.js作为一种服务端渲染框架,也不例外;针对不同的页面,不同的数据状态,可主要区分为下面三类缓存:

1、API接口数据缓存

将服务端获取的数据,全部缓存到node进程内存中,定时刷新,有效期内请求都通过缓存获取API接口数据,减小数据获取时间;

此种缓存适用于缓存的部分API数据,基本保持不变,变更不频繁,与用户个人数据无关。

示例代码:

 import LRU from 'lru-cache' 
 const CACHED = new LRU({ 
 max: 100, // 缓存队列长度 
 maxAge: 1000 * 60 // 缓存时间 
 }) 
 export default { 
 async asyncData({ app, query }) { 
  try { 
  let banner, footer 
  if (CACHED.has('baseData')) { 
   // 存在缓存,使用缓存数据 
   let data = CACHED.get('baseData') 
   data = JSON.parse(data) 
   banner = data.banner 
   footer = data.footer 
  } else { 
   // 获取页面顶部轮播图信息 
   const getBanner = () => { 
   return app.$axios.$get('zz/zy/banner') 
   } 
   // 获取底部配置信息 
   const getFooter = () => { 
   return app.$axios.$get('zz/zy/footer', { 
    params: { 
    smark: query.smark 
    } 
   }) 
   } 
   [banner, footer] = await Promise.all([getBanner(), getFooter()]) 
   // 将数据写入缓存 
   CACHED.set('baseData', JSON.stringify({ banner: banner, footer: footer})) 
  } 
  return {mods: mods, footer: footer} 
  } catch (e) { 
  console.log('interface timeout or format error => ', e) 
  return {} 
  } 
 } 
 }

2、组件级别缓存

将渲染后的组件DOM结构存入缓存,定时刷新,有效期通过缓存获取组件DOM结构,减小生成DOM结构所需时间;

适用于渲染后结构不变或只有几种变换、并不影响上下文的组件。

示例代码:

nuxt.config.js配置项修改

const LRU = require('lru-cache') 
module.exports = { 
 render: { 
 bundleRenderer: { 
  cache: LRU({ 
  max: 1000, // 缓存队列长度 
  maxAge: 1000 * 60 // 缓存1分钟 
  }) 
 } 
 } 
}

需要做缓存的 vue 组件, 需增加 name 以及 serverCacheKey 字段,以确定缓存的唯一键值。

export default { 
 name: 'zzZyHome', 
 props: ['type'], 
 serverCacheKey: props => props.type 
}

如果组件依赖于很多的全局状态,或者状态取值非常多,缓存会因频繁被设置而导致溢出,这样的组件做缓存就没有多大意义了;

另外组件缓存,只是缓存了dom结构,如created等钩子中的代码逻辑并不会被缓存,如果其中逻辑会影响上下边变更,是不会再执行的,此种组件也不适合缓存。

3、页面整体缓存

当整个页面与用户数据无关,依赖的数据基本不变的情况下,可以对整个页面做缓存,减小页面获取时间;

页面整体缓存前提是在使用Nuxt.js脚手架工具create-nuxt-app初始化项目时,必须选择集成服务器框架,如express、koa,只有这样才具有服务端中间件扩展的功能。

示例代码:

服务端中间件middleware/page-cache.js

const LRU = require('lru-cache') 
let cachePage = new LRU({ 
 max: 100, // 缓存队列长度 
 maxAge: 1000 * 60 // 缓存1分钟 
}) 
export default function(req, res, next){ 
 let url = req._parsedOriginalUrl 
 let pathname = url.pathname 
 // 通过路由判断,只有首页才进行缓存 
 if (['/home'].indexOf(pathname) > -1) { 
 const existsHtml = cachePage.get('homeData') 
 if (existsHtml) { 
  return res.end(existsHtml.html, 'utf-8') 
 } else { 
  res.original_end = res.end 
  // 重写res.end 
  res.end = function (data) { 
  if (res.statusCode === 200) { 
   // 设置缓存 
   cachePage.set('homeData', { html: data}) 
  } 
  // 最终返回结果 
  res.original_end(data, 'utf-8') 
  } 
 } 
 } 
 next() 
}

nuxt.config.js配置项修改,引入服务端中间件

//针对home路由做缓存 
serverMiddleware: [ 
 { path: '/home', handler: '~/middleware/page-cache.js' }, 
]

总结

本文主要是针对Nuxt.js框架实现的页面,性能优化方案进一步探索和实践的总结,汇总一些思路与方向;期望各位小伙伴在其它SSR相关页面优化过程中,能起到一定的启发作用。

以上这篇Nuxt 项目性能优化调研分析就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。