Nuxt 项目性能优化调研分析


Posted in Javascript onNovember 07, 2020

性能优化,这是面试中经常会聊到的话题。我觉得性能优化应该因具体场景而异,因不同项目而异,不同的手段不同的方案并不一定适合所有项目,当然这其中不乏一些普适的方案,比如耳熟能详的文件压缩,文件缓存,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 项目性能优化调研分析就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Jquery Ajax学习实例7 Ajax所有过程事件分析示例
Mar 23 Javascript
AJAX使用了UpdatePanel后无法使用alert弹出脚本
Apr 02 Javascript
Javascript 网页黑白效果实现代码(兼容IE/FF等)
Apr 23 Javascript
加载 Javascript 最佳实践
Oct 30 Javascript
jQuery获取(选中)单选,复选框,下拉框中的值
Feb 21 Javascript
jquery输入数字随机抽奖特效的简单实现代码
Jun 10 Javascript
利用Angularjs和bootstrap实现购物车功能
Aug 31 Javascript
使用Vue实现图片上传的三种方式
Jul 17 Javascript
关于在vue 中使用百度ueEditor编辑器的方法实例代码
Sep 14 Javascript
mock.js模拟前后台交互
Jul 25 Javascript
JS表格的动态操作完整示例
Jan 13 Javascript
javascript实现数字时钟效果
Feb 06 Javascript
js实现日历
Nov 07 #Javascript
工作中常用js功能汇总
Nov 07 #Javascript
解决VUE 在IE下出现ReferenceError: Promise未定义的问题
Nov 07 #Javascript
解决Element中el-date-picker组件不回填的情况
Nov 07 #Javascript
解决element-ui的下拉框有值却无法选中的情况
Nov 07 #Javascript
解决VUE项目使用Element-ui 下拉组件的验证失效问题
Nov 07 #Javascript
详解datagrid使用方法(重要)
Nov 06 #Javascript
You might like
实用函数7
2007/11/08 PHP
php in_array 函数使用说明与in_array需要注意的地方说明
2010/04/13 PHP
php is_executable判断给定文件名是否可执行实例
2016/09/26 PHP
php将服务端的文件读出来显示在web页面实例
2016/10/31 PHP
Linux下源码包安装Swoole及基本使用操作图文详解
2019/04/02 PHP
Jquery Ajax学习实例7 Ajax所有过程事件分析示例
2010/03/23 Javascript
腾讯UED 漂亮的提示信息效果代码
2011/09/12 Javascript
特殊情况下如何获取span里面的值
2014/05/20 Javascript
使用FlexiGrid实现Extjs表格效果方法分享
2014/12/16 Javascript
jQuery+PHP实现动态数字展示特效
2015/03/14 Javascript
纯javascript判断查询日期是否为有效日期
2015/08/24 Javascript
JS+CSS实现带有碰撞缓冲效果的竖向导航条代码
2015/09/15 Javascript
简单实现js页面切换功能
2021/01/10 Javascript
javascript获取网页各种高宽及位置的方法总结
2016/07/27 Javascript
jQuery常用样式操作实例分析(获取、设置、追加、删除、判断等)
2016/09/08 Javascript
Angular 通过注入 $location 获取与修改当前页面URL的实例
2017/05/31 Javascript
AngularJS自定义过滤器用法经典实例总结
2018/05/17 Javascript
发布一款npm包帮助理解npm的使用
2019/01/03 Javascript
浅谈Vue项目骨架屏注入实践
2019/08/05 Javascript
Python2.7简单连接与操作MySQL的方法
2016/04/27 Python
详解python之简单主机批量管理工具
2017/01/27 Python
Python中支持向量机SVM的使用方法详解
2017/12/26 Python
python logging模块书写日志以及日志分割详解
2019/07/22 Python
Django 源码WSGI剖析过程详解
2019/08/05 Python
Python 实现训练集、测试集随机划分
2020/01/08 Python
Python制作数据预测集成工具(值得收藏)
2020/08/21 Python
使用CSS禁止textarea调整大小功能的方法
2015/03/13 HTML / CSS
美国最大的香水连锁店官网:Perfumania
2016/08/15 全球购物
出纳的岗位职责
2013/11/09 职场文书
高校教师思想汇报
2014/01/11 职场文书
初中化学教学反思
2014/01/23 职场文书
大学生毕业鉴定
2014/01/31 职场文书
公司财务人员岗位职责
2015/04/14 职场文书
python自动化调用百度api解决验证码
2021/04/13 Python
spring boot中nativeQuery的用法
2021/07/26 Java/Android
Java版 简易五子棋小游戏
2022/05/04 Java/Android