浅谈 vue 前端同构框架 nuxt 及其性能优化

马丁路德 等级 653 0 0

前言

使用 nuxt.js 做项目也接近快一年了,从立项到内测、公测、再到正式上线,还有后面的不断维护,也陆陆续续的踩了很多坑,其中最大的问题就是 node 的渲染性能问题了。模板转换是 cpu 密集型的操作,node 又是单线程的,并发一高,cpu 就会飙到 100% 。为了提升 nuxt.js 的渲染性能,也陆陆续续的查找了很多资料,发现网上针对 nuxt.js 的性能优化的文章比较少,比较杂。所以我写下这篇文章记录下自己对 nuxt.js 做性能优化的时候采取的一些方法,算是篇总结吧,也希望能给从谷歌搜到这的朋友一些帮助。本文着重于性能优化,对概念类的东西会一概而过。

同构渲染

与传统的服务端渲染,使用模板引擎生成 html 不同。前端同构渲染在服务器端和客户端运行的是同一套代码,只有首屏是服务端直出 html ,而点击路由切换则是一个单页应用(spa)。同构渲染即能做到首屏直出,又能体验到无刷新的用户体验。使用模板引擎生成 html 可以做到首屏直出 html ,但做不到 spa 应用无刷新的用户体验,不过模板引擎不限制你的服务端语言,而同构渲染因为要在客户端和服务端跑着同一套代码,所以只能使用 node.js 做渲染服务器。

Nuxt

nuxt.js 是 vue 的一个服务端渲染的框架,把 webpack 、vue loader,vuex, router 系列配置整合到了一起,是一个比较完整的 vue 服务端渲染的方案。

生命周期

ssr 在没有做缓存的情况下,客户端的每次 request 都会到 node 服务器中,触发后端渲染。渲染服务器引入 renderer 和相应的 vue 应用,根据 route 找到相应的组件和数据,拉组件再拉数据(可能是异步的),加载组件生产 DOM,然后再使用 renderToString 吐给 response。

下图是来自 nuxt 官方文档的生命周期:

浅谈 vue 前端同构框架 nuxt 及其性能优化

性能优化

有得必有失,vue ssr 需要在服务器根据 vue 文件生成虚拟 dom 再序列化成 html,是 cpu 密集型的操作。并且为了隔离请求不同的请求,它会为每一个请求创建一个上下文 context,这样一来,vue ssr 和传统的模板引擎相比,其性能起码差了几十倍。要想 vue ssr 经得起上线的考验,高并发的情况下能正常工作,必须要采取一系列的性能优化手段,并采取明智的部署策略。

缓存

但凡是提到性能优化,第一个想到的必然是缓存。node.js 是单线程的,它的高性能是相对于异步 io 操作频繁的。针对 io 密集型而非 cpu 密集型,因此需要在渲染过程中采取合理的缓存策略。ssr 的缓存可分为 组件级别的缓存、数据级别的缓存、页面级别的缓存。当命中缓存的时候,只需将缓存中的 html 吐给 response ,不用再进行一系列的渲染活动,极大的节省 cpu 资源。

1. 组件的缓存

nuxt 的组件级别的缓存,使用的是 Component Cache module 模块,将 @nuxtjs/component-cache 从 npm 中添加到依赖中,在配置文件 nuxt.config.js 做出如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
{
modules: [
'@nuxtjs/component-cache',
[
'@nuxtjs/component-cache',
{
max: 10000,
maxAge: 1000 * 60 * 60
}
]
]
}

在需要缓存的组件中使用 serverCacheKey 函数来标识

1
2
3
4
5
6
7
export default {
name: 'ReplyItem',
serverCacheKey: props => props.postId,
props: {
postId: String
}
}

serverCacheKey 函数所返回的 key 必须有足够的信息去标识该组件,返回常量将导致组件始终被缓存,这对纯静态组件是有好处的。同时,缓存的组件必须要有唯一的 name 属性。

但值得注意的是,使用组件级别缓存的时候,不要缓存单一实例的组件。应该缓存的是那些重复使用的展示型组件,如 v-for 下的重复组件,在我所写的项目中,我使用组件级别的缓存也主要是这一类,如帖子列表、新闻列表、评论列表等。

如果 render 在渲染组件的过程中,命中缓存,则直接使用缓存结果,所以一些情况不能使用组件级别的缓存:

  • 可能拥有依赖 global 数据的子组件。
  • 具有在渲染 context 中产生副作用的子组件。

2. 数据的缓存

在 node 服务器向后端请求数据的时间,也会影响到渲染的时间,所以数据层,最好也要有缓存。如果从后端 api 拉取数据的时间需要 3 秒,那这 3 秒会直接反应在首屏渲染时间上。对于数据层的缓存,应该对那些不涉及用户信息和实时性要求不高的接口进行缓存。对于数据层的缓存,使用的是 lru-cache 这个模块。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import axios from 'axios'
import LRU from 'lru-cache'
import md5 from 'md5'

const options = {
// 最大缓个数
max: 1000,
// 缓存周期
maxAge: 1000 * 60 * 5 // 5 分钟的缓存
}

const cache = new LRU(options)

// 需要缓存数据的接口
const CACHE_API = ['v1/api/xxxx', 'v1/api/xxxx', 'v1/api/xxxx', 'v1/api/xxxx']

export function get(url, params) {
const key = md5(url + JSON.stringify(params))
// 只在服务端进行缓存
if (process.server && cache.has(key)) {
// 命中缓存
return Promise.resolve(cache.get(key))
}

return axios
.get(url, { params })
.then(res => {
// 只在服务端进行缓存
if (process.server && CACHE_API.includes(url)) {
cache.set(key, res.data)
}
return Promise.resolve(res.data)
})
.catch(err => throw err)
}

3. 页面的缓存

不是每一请求都需要触发后端渲染的,当页面不涉及用户数据,就可以对整个页面进行缓存。url 命中缓存的时候,直接将缓存吐给 response ,不再触发一系列的渲染活动。在 nuxt 中,使用页面级别的缓存,使用的是服务器渲染中间件 serverMiddleware 。在这一层的缓存中可以使用 redis 进行缓存,在 nginx 层的时候就可以直接调用 redis 吐数据,缓存过期后再重新出发 node 渲染并重新缓存。当然,也可以缓存在内存中。

示例代码:

根目录下新建一个 serverMiddleware/pageCache.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const LRU = require('lru-cache')

const options = {
max: 1000,
maxAge: 1000 * 60 * 5
}

// 需要进行页面级别缓存的路由
const CACHE_URL = ['/xxx', '/xxx', '/xxx']

const cache = new LRU(options)

export default function (req, res, next) {
const url = req._parsedOriginalUrl
const pathname = !!url.pathname ? url.pathname : ''

if (CACHE_URL.includes(pathname)) {
const existsHtml = cache.get(pathname)
if (existsHtml) {
// 不要忘了设置 Content-Type 不然浏览器有时候可能不会渲染而是触发下载文件
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
return res.end(existsHtml.html, 'utf-8')
} else {
res.original_end = res.end
res.end = function (data) {
if (res.statusCode === 200) {
cache.set(pathname, { html: data })
}
res.original_end(data, 'utf-8')
}
}
}
next()
}

页面缓存渲染中间件写好之后,在配置文件 nuxt.config.js 中使用

1
2
3
4
5
module.exports = {
// ...
serverMiddleware: ['~/serverMiddleware/pageCache.js']
// ...
}

不必要的渲染开销

服务端渲染最主要的作用是 seo,但并不是所有的页面都需要进行 seo。整站式的 ssr 意味着将消耗巨大服务器 cpu 资源,如果只从后端渲染需要 seo 的页面,将极大的节省 cpu 资源,空余出来的 cpu 资源则作用于更大的并发量。例如:掘金 就仅仅是在文章的详情页做 ssr 。

那么在 nuxt.js 中,如何根据不同的路由去觉得是进行服务端渲染或是客户端渲染呢?这一点在 nuxt.js 的官方文档中并未提到这一点,但我在 nuxt.js 的源码中找到关于 url 控制是服务度渲染还是客户端渲染的代码:

nuxt.js/packages/vue-renderer/src/renderer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async renderRoute(url, renderContext = {}, _retried) {

// ...

if (renderContext.spa === undefined) {
// TODO: Remove reading from renderContext.res in Nuxt3
renderContext.spa = !this.SSR || req.spa || (renderContext.res && renderContext.res.spa);
}

// ...

return renderContext.spa
? this.renderSPA(renderContext)
: this.renderSSR(renderContext)
}

其中决定不同的路由是服务度渲染还是客户端渲染,取决于 renderContext.spa 的值,而 renderContext.spa 的值按先后顺序取决于 options.render.ssrreq.sparenderContext.res.spa 。 所以我们只需要在特定的路由将 res.spa 置为 true 即可。

新建渲染服务器中间件 serverMiddleware/spaPage.js:

1
2
3
4
5
6
7
8
9
10
11
12
export default function (req, res, next) {
const { _parsedOriginalUrl } = req
const pathname = _parsedOriginalUrl.pathname
? _parsedOriginalUrl.pathname
: ''

res.spa = true
if (pathname.includes('/post') || pathname === '/') {
res.spa = false
}
next()
}

只在文章详情页和首页进行 ssr ,在配置文件 nuxt.config.js 中使用

1
2
3
4
5
module.exports = {
// ...
serverMiddleware: ['~/serverMiddleware/spaPage.js']
// ...
}

首屏最小化

为了进一步的节省服务器性能,我们可以分析需要服务端渲染的页面布局。进行合理的页面结构的拆分,首屏所需的数据和页面结构在服务端获取并渲染,非首屏所需的数据和结构在客户端拉取并渲染。需要在服务端拉取的数据可以使用 asyncData 方法来异步获取数据,不需要在服务端拉取的数据在 mounted 这个钩子获取。分割结构可以使用 no-ssrclient-only(nuxt 版本不小于 2.9) 标签包裹不需要在服务端进行渲染的结构。

使用 no-ssr 包裹不需要服务端渲染的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="post-detail">
<main>
<post-head />
<post-body />
<post-foot />
</main>

<aside>
<!-- 不需要服务端渲染的内容 -->
<no-ssr>
<f-block />
<f-block />
<f-block />
</no-ssr>
</aside>
</div>
</template>

服务端渲染所需的数据在 asyncData 中获取,客户端渲染所需的数据在 mounted 钩子中获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
// 需要在服务端拉取的数据
async asyncData({ app, params }) {

// 发送请求
const threadResultPromise = app.$api.thread.getThreadDetail(parseInt(params.tid, 10))
const replyListResultPromise = app.$api.thread.getThreadReplyList({ threadId: parseInt(params.tid, 10) })

return Promise.all([
threadResultPromise,
replyListResultPromise
] => {
return {
// ...
}
})
},

// 在客户端获取的数据
mounted() {
this.getSpecialColumnInfo()
this.setContentTypeName()
}
}

区分登录和非登录情况

搜索引擎的爬虫,访问你的服务器的时候并不会携带用户相关的信息。所以我们可以针对这一特性来做进一步的优化,进一步的减少 node 服务器的渲染压力。这一步可以参考掘金社区,只在非登录的情况下做服务端渲染,用户登录的情况下就是客户端渲染。在 nuxt.js 中,可以编写一个渲染服务器中间件,来对登录用户和非登录用户进行不同的处理。如采用 cookie 来对用户是否登录进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cookieparser = require('cookieparser')

export default function (res, req, next) {
try {
const parsed = req.headers.cookie
? cookieparser.parse(req.headers.cookie)
: ''

// cookie 中存在用户信息
if (parsed && parsed.userInfo) {
// 置为 spa 模式
res.spa = true
next()
return
}
} catch (err) {
throw err
}
next()
}

ssr 和 spa 的切换

nuxt.js 的配置文件中,导出的一个 mode 属性,它的默认值为 universal 同构应用程序,提供的另外一个属性 spa 单页。想要在 ssr 和 spa 之间切换只需要更改 mode 的值,再重新编译即可:

1
2
3
4
5
6
7
module.exports = {
// 服务端渲染 前端同构
mode: 'universal'

// 客户端渲染 单页应用
// mode: 'spa'
}

部署策略

服务端渲染,cpu 密集型的操作,高并发下很容易造成 cpu 满载,在我负责的服务端渲染的项目中,我采用的 pm2 来部署项目。pm2 是一个 node 进程管理工具,它可以对 node 应用进行监控,自动重启,负载均衡等。pm2 可以启动多个实例来用于负载均衡,多个 node 实例可以实现前端应用不停机更新。

除了每个服务器启动 node 集群实现每个进程之间之外,还要多个服务器之间实现负载均衡,使用 nginx 实现多个服务器间的负载均衡。我们公司在部署的时候,就采用了 3 台 8 核的服务器进行负载均衡,每台服务器又开启 8 个 node 进程进行负载均衡。

node 集群

node 是基于 v8 引擎实现的在操作系统中运行 JavaScript 的工具,JavaScript 的单线程在多核的 cpu 上运行无法发挥多核 cpu 的性能,只有一个 cpu 核心在运行,其他的核在闲置,效率很低。在我负责的项目中,使用的是 pm2 实现 node 的集群,pm2 可以把你的应用以 集群(cluster)模式来运行(仅限 node 程序),部署到服务器 cpu 的所有核心上。

使用 pm2 来启动 nuxt 服务器可以在根目录下新建一个 pm2 启动描述文件:pm2.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"apps": [
{
"name": "feng-pc-render-server",
"max_memory_restart": "1200M",
"script": "server/index.js",
"env": {
"NODE_ENV": "production",
"PORT": 3000,
"HOST": "0.0.0.0"
},
"instances": 0,
"exec_mode": "cluster",
"autorestart": true
}
]
}

其中 max_memory_restart 自动重启的最大内存,当进程内存超过该值时重启该进程,可以在一定程度上解决内存泄露,但这明显不是明智的选择,exec_mode 表示启动的模式,值为 cluster 时已集群的模式启动。instances 字段表示需要启动的 node 进程,当值为 0 或 max 时,pm2 已当前服务器所能启动的最多实例来启动项目,仅在 cluster 模式下生效。

浅谈 vue 前端同构框架 nuxt 及其性能优化

当使用 pm2 的集群模式启动后,使用 top 命令或 htop 查看服务器运行信息,可以看到服务器的 8 个核心,均有负载。

浅谈 vue 前端同构框架 nuxt 及其性能优化

此外,如果 cpu 核心少的话,也可以启动多个端口使用 nginx 进行负载均衡,但这样也起不了多少效果。

nginx 集群

在我所负责的服务端渲染项目中,是使用 nginx 对 3 台 8 核的服务器进行负载均衡。当用户访问服务端时,服务端通过 nginx 负载到其中资源利用率较低的一台,再反向代理到负载均衡的 node 集群,然后随机将用户的请求发给比较闲的node服务。

在这个过程中也碰到了很多坑,如怎么在 3 台 服务器间进行状态同步,保持登录。由于我们公司的网络配置层是由运维去管理的,如何配置 nginx 负载均衡在这里不进行赘述,有兴趣的朋友可以自行搜索。

防止 cc 攻击

我们公司的网站也是国内流量比较大的社区类站点,项目在上线几个月后,遭遇了一次大规模的 cc 攻击,针对这种情况,我们也做了大量的处理:配置 ip 黑名单,对单个 ip 限制最大并发数、写 lua 脚本拦截 cc 攻击,最后还上了高防服务器。

总结

将 vue 渲染成 html 是 cpu 密集型的操作,node 又是单线程的,所以性能不是很好,想要提高并发,就得做缓存。
在 node 渲染服务器中做三层的缓存:页面级别的缓存、组件级别的缓存、api 级别的缓存。由于搜索引擎的爬虫不会携带用户信息,还可以区分用户登录和非登录的情况,针对非登录用户做服务端渲染 ssr ,对登录用户做客户端渲染 spa。也不是所有页面都需要服务端渲染,可以仅针对特定的路由做服务端渲染,还要控制首屏的大小,非必要的组件使用懒加载的方式在客户端渲染,再加上多层的集群处理。

本文转自 [https://tflin.com/2020/05/13/浅谈 vue 前端同构框架 nuxt 及其性能优化](https://tflin.com/2020/05/13/浅谈 vue 前端同构框架 nuxt 及其性能优化),如有侵权,请联系删除。

收藏
评论区

相关推荐

android 面试题总结
Java部分 一、多线程   Join()  线程加入,执行此方法的线程优先使用cpu   Yeild()  线程释放资源使所有线程能有相等的机会使用cpu   Sleep() 相当于让线程睡眠,交出CPU,让CPU去执行其他的任务(不会释放锁)。 Wait()方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
CPU负载过高异常排查实践与总结
昨天下午突然收到运维邮件报警,显示数据平台服务器cpu利用率达到了98.94%,而且最近一段时间一直持续在70%以上,看起来像是硬件资源到瓶颈需要扩容了,但仔细思考就会发现咱们的业务系统并不是一个高并发或者CPU密集型的应用,这个利用率有点太夸张,硬件瓶颈应该不会这么快就到了,一定是哪里的业务代码逻辑有问题。 1、排查思路 1.1 定位高负载进程 首先
TS 的脚步已经拦不住,代码撸起来
前言 vue3已经发布了,ts的脚步已经阻拦不住了,还只会es6?别想了,人家都已经在行动了,以下是ts的基本系列教程,ts的基本语法,高级语法等,以及在vue项目中如何应用ts,跟着我赶紧撸起来吧。 基本数据类型 数字 const a: number  3; 字符串 const b: string  "1
EaseJs 中 regX / regY 的用法
EaseJs 中 regX / regY 的用法 by 凹凸曼大力
python中的各种锁
一、全局解释器锁(GIL)   1、什么是全局解释器锁       在同一个进程中只要有一个线程获取了全局解释器(cpu)的使用权限,那么其他的线程就必须等待该线程的全局解释器(cpu)使    用权消失后才能使用全局解释器(cpu),即时多个线程直接不会相互影响在同一个进程下也只有一个线程使用cpu,这样的机制称为全局    解释器锁(GIL)。  
Kubernetes自定义调度器 — 初窥门径
通过上一篇文章对schedulerframework调度框架已经有了大致了解,根据我们的实际生产的一些问题(如计算服务没有被调度到实际CPU最优的节点)和需求,来实现一个简单的基于CPU指标的自定义调度器。自定义调度器通过kubernetes资源指标服务metricsserver来获取各节点的当前的资源情况,并进行打分,然后把Pod调度到分数最高的节
浅谈 vue 前端同构框架 nuxt 及其性能优化
前言使用 nuxt.js 做项目也接近快一年了,从立项到内测、公测、再到正式上线,还有后面的不断维护,也陆陆续续的踩了很多坑,其中最大的问题就是 node 的渲染性能问题了。模板转换是 cpu 密集型的操作,node 又是单线程的,并发一高,cpu 就会飙到 100% 。为了提
vue cli3打包部署,浏览器缓存问题
问题:每次部署后,页面不更新总是有缓存问题。解决中心思想:在打包文件的文件名中添加一个版本号。方法:在vue.config.js中添加以下代码,vue cli3.x生成的项目默认没有这个文件,需要自己在根目录创建,和package.json同级。 代码块const Timestamp  new Date().getTime();m
「Vue — 插件」导入导出excel表格vue-xlsx-table
1:npm install vuexlsxtable save(excel转json)2:在main.js中 jsimport vueXlsxTable from 'vuexlsxtable';Vue.use(vueXlsxTable, {rABS: false});3:在需要使用的页面中 js<vuexlsxtable @
Vue开发指南:你都需要学点啥?
如果您是Vue开发的新手,您可能已经听过很多关于它的专业术语了,例如:单页面应用程序、异步组件、服务器端呈现等。 另外您可能还经常听到和Vue一起提到的工具和库,如Vuex、Webpack、Vue CLI和Nuxt。 也许您在面对这些未知的术语和
商业数据分析从入门到入职(5)Python基本语法和数据类型
一、从计算机到Python 1.计算机与程序思维计算机最核心的三个部分为CPU、内存和硬盘,都在主板上面,除此之外,还包括键盘、鼠标等输入设备和屏幕等输出设备,如下:CPU用于进行计算,硬盘用于存储数据和文件,内存(包括缓存)用于连接CPU和硬盘,作为两者的缓冲,可以加快读取和处理速率。冯·诺依曼架构如下:程序是指定如何执行计
cpu分析利器 — async-profiler
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。 简介asyncprofiler是一款采集分析java性能的工具,翻译一下github上的项目介绍:asyncprofiler是一款没有Safepoint bias problem的低开销java采集分析器,它利用HotSpot特殊的api来收集栈信息以及
Vue引入mavon-editor插件实现markdown功能
Vue引入mavoneditor插件实现markdown功能说明 mavoneditor是一款基于Vue的markdown编辑器,因为当前项目是采用Nuxt,所以这里所展示的教程是针对Nuxt引入mavoneditor插件,如果你只是采用了Vue,没有用Nuxt框架,那么你可以看mavoneditor官方文档,有详细说明,其实它们只有在引入mavonedit
Vue父子组件
几种常见的通信方式:1、prop 属性父组件通过绑定属性的方式,给子组件传值,同时子组件通过设置 props 来接收 let Child Vue.extend( template: 'content', props: content: type: String, default: () r
写了一年golang,来聊聊进程、线程与协程
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。 进程在早期的单任务计算机中,用户一次只能提交一个作业,独享系统的全部资源,同时也只能干一件事情。进行计算时不能进行 IO 读写,但 CPU 与 IO 的速度存在巨大差异,一个作业在 CPU 上所花费的时间非常少,大部分时间在等待 IO。为了更合理的利用