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 相关文章推荐
javascript 动态生成私有变量访问器
Dec 06 Javascript
js网页版计算器的简单实现
Jul 02 Javascript
javascript的函数作用域
Nov 12 Javascript
js改变Iframe中Src的方法
May 05 Javascript
javascript实现动态统计图开发实例
Nov 21 Javascript
灵活使用数组制作图片切换js实现
Jul 28 Javascript
微信小程序 解决swiper不显示图片的方法
Jan 04 Javascript
javascript常用的设计模式
Feb 09 Javascript
解决Node.js使用MySQL出现connect ECONNREFUSED 127.0.0.1:3306的问题
Mar 09 Javascript
详解vuex数据传输的两种方式及this.$store undefined的解决办法
Aug 26 Javascript
Node.js API详解之 vm模块用法实例分析
May 27 Javascript
微信小程序连接服务器展示MQTT数据信息的实现
Jul 14 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
php jq jquery getJSON跨域提交数据完整版
2013/09/13 PHP
PHP实现简单ajax Loading加载功能示例
2016/12/28 PHP
PHP异常处理定义与使用方法分析
2017/07/25 PHP
PHP数组式访问接口ArrayAccess用法分析
2017/12/28 PHP
php设计模式之组合模式实例详解【星际争霸游戏案例】
2020/03/27 PHP
JS 无限级 Select效果实现代码(json格式)
2011/08/30 Javascript
jquery单行文字向上滚动效果示例
2014/03/06 Javascript
Node.js事件循环(Event Loop)和线程池详解
2015/01/28 Javascript
javascript常用的方法分享
2015/07/01 Javascript
js实现3D图片逐张轮播幻灯片特效代码分享
2015/09/09 Javascript
JS检测移动端横竖屏的代码
2016/05/30 Javascript
使用jQuery5分钟快速搞定双色表格的简单实例
2016/08/08 Javascript
jQuery实现导航滚动到指定内容效果完整实例【附demo源码下载】
2016/09/20 Javascript
bootstrap模态框消失问题的解决方法
2016/12/02 Javascript
JavaScript使用atan2来绘制箭头和曲线的实例
2017/09/14 Javascript
vue按需引入element Transfer 穿梭框
2017/09/30 Javascript
原生JS与jQuery编写简单选项卡
2017/10/30 jQuery
Vue自定义过滤器格式化数字三位加一逗号实现代码
2018/03/23 Javascript
NodeJS实现一个聊天室功能
2019/11/25 NodeJs
Vue $emit()不能触发父组件方法的原因及解决
2020/07/28 Javascript
Node.JS如何实现JWT原理
2020/09/18 Javascript
基于elementUI竖向表格、和并列的案例
2020/10/26 Javascript
[19:24]DOTA2客户端使用指南 一分钟快速设置轻松超神
2013/09/24 DOTA
[03:24]DOTA2超级联赛专访hao 大翻盘就是逆袭
2013/05/24 DOTA
Python中表示字符串的三种方法
2017/09/06 Python
使用python实现链表操作
2018/01/26 Python
TensorFlow实现简单卷积神经网络
2018/05/24 Python
Pandas读取并修改excel的示例代码
2019/02/17 Python
pytorch 在网络中添加可训练参数,修改预训练权重文件的方法
2019/08/17 Python
html5基础教程常用技巧整理
2013/08/20 HTML / CSS
Fox Racing英国官网:越野摩托车和山地自行车服装
2020/02/26 全球购物
饲料采购员岗位职责
2013/12/19 职场文书
质量管理标语
2014/06/12 职场文书
毕业生找工作求职信
2014/08/05 职场文书
关于十八大的演讲稿
2014/09/15 职场文书
公积金贷款承诺书
2015/04/30 职场文书