Nuxt.js 项目部署函数计算优化(省钱)小记

Serverless说来并不是一个新鲜的东西,早在当年App Engine就提供了一个基于特定代码运行环境配合云服务的代码托管思路,但因为其各种环境的限制与迁移成本使其并没有很广泛的推广开了。而如今容器化的大行其道,云生态的丰富与完善,则为云原生应用提供了更为丰富的可能性,云厂商也不予余力的包装出新的概念、产品。

Serverless应用场景很多,但绕不开的一个问题就是Web项目可以跑吗?用Serverless跑Web项目真的能节约成本吗?于是我就开始尝试使用阿里云的函数计算来看一看。

在函数计算里运行Nuxt.js项目

为什么是Nuxt.js项目?
Nuxt.js 是基于 Vue.js 的 SSR,SSR 满足作为内容性质的Web应用的SEO需求,同时首次返回页面之后,在页面上的所有操作就都在浏览器上完成了,可以很大程度上减少网络交互。
(其实是手头目前最适合测试的项目用了Nuxt.js)

Nuxt.js项目如何跑在函数计算上?
官方文档中会告诉你,只需要安装并设置好命令行工具,并在项目目录运行就好了。
究其本质是利用了官方提供的一个custom的机制,在容器中预置好了环境,不管你是什么项目,只要给出你的项目的启动命令,并且启动后通过HTTP 9000端口进行交互,就可以通过函数计算访问到你的项目。所以不管你是Nuxt还是Express还是Wordpress,只要你能监听9000端口提供Web服务,一切就都就绪了。

官方是这么说的,对于一般个人Web项目来说也够了,(毕竟对云厂商来说,个人就是凑人头的),但本着能省则省的原则,如何进行优化呢?

函数计算的费用是怎么算的?

(截止本文撰写的时间)阿里云的函数计算主要包括了以下几个部分的费用:
1. 调用次数费用 2. 执行时长费用 3. 流量费用 4. 其他费用

调用次数费用

调用次数费用,指的是每次成功的通过HTTP请求函数计算,都会被计算请求次数,并进行计费,如果我们通过函数计算提供的是API类的服务,那就是API接口被请求的次数,但如果我们提供的是Web服务呢,在前面我们提到了,我们将整个Web项目通过9000接口监听的方式提供HTTP服务,那么相当于并不仅限于我们的Web页面,CSS、JS以及图片等等的静态资源也都是在通过这种方式在提供服务,是的,用户打开一次页面,并不会只有一次请求,还会有连带有各种静态资源的请求。

执行时长费用

执行时长既包括了使用的计算资源也包括实际运行的时长,当然 Nuxt.js 本身的资源占用以及执行渲染的时长并不会太长。

流量费用

从函数计算中流出的费用,这个与前面提到的调用次数类似,不作任何调整的情况下,所有的静态资源也都会直接从函数计算中流出,并计算费用。

优化方案

UI库的按需加载

如果在使用诸如 ant-design-vue 或者 element-ui 之内的库的话,如果直接将整个库引用到项目中的话,会很大程度上增加编译后的文件大小。
使用 ant-design-vue 的情况可以参考:Nuxt.js 中按需加载 ant-design-vue

开启 extractCSS

Nuxt.js 默认未开启 extractCSS,在引入UI组件时,进行额外编译的情况,会导致页面中大量的 style 片段,增加页面大小,且不利用复用,通过 extractCSS 将样式都输出到独立文件中,这样就可以和其他静态资源一块儿优化了。

静态资源优化

前文中提到,默认情况下,所有的静态资源(CSS/JS/图片),通过Nuxt.js内的express,直接通过监听端口的方式提供访问,这些资源从函数计算流出,带来额外的请求数和流量,这部分如果和动态流量来计算显然是不划算的。因此可以考虑静态资源使用CDN。

使用CDN可以有两种方式:
1. 全站使用:通过CDN加速整个函数计算项目,webpack编译出来的前端资源默认都在 /_nuxt/ 路径,在CDN中配置对 /_nuxt/ 的缓存策略,减少回源,这种方式实行起来相对简单,但是不够彻底。
2. 使用对象存储:通过将webpack编译后的 .nuxt/dist/client 目录下的文件,上传到对象存储,并配置CDN。然后更改 nuxt.config.js 中的 publicPath 项目,将其改为CDN地址的方式,从而实现将全部静态资源直接通过CDN进行访问。通过这种方式,在部署函数计算时,因为前端静态文件已经全部转到对象存储上了,因此代码包可以通过不包括 .nuxt/dist/client 目录,以减少代码包大小,从而提高冷启动速度

优化 node_modules

除了前端静态文件外,对于node项目来说 node_modules 犹如文件地狱,当引入的依赖不断增加时,node_modules 也会日趋增加,打包时带上 node_modules 会大幅增大代码包大小,影响冷启动时间。因此在部署前,需要对 node_modules 进行优化(使用yarn会在更新依赖后自动进行优化)
此外,如果 node_modules 确实过大,则建议引入 NAS,将 node_modules 放入 NAS中,容器启动挂载NAS提供服务,以减少冷启动时间。

服务端请求接口优化

Nuxt.js 中使用 axios 发起请求,获取接口数据进行页面渲染,对于服务端渲染的请求来说,是由服务端向上游接口进行请求,前面也提到,关于执行时长和资源使用来说,服务端渲染其实占用的时间并不多,但不可忽视的就是请求上游接口。
在 nuxt.config.js 的 axios 设置中,baseURL 和 browserBaseURL 分别指定了服务端和浏览器请求上游接口时使用的地址。在可能的情况下 baseURL 指定为接口内网地址,而 browserBaseURL 使用接口外网服务的地址,也能在一定程度上减少延迟,缩短执行时长。

成本如何

相较于传统的Web托管,静态资源部分的CDN使用成本与使用函数计算相差无异,同时在优化过程中,也已经对静态资源进行了优化。那么使用函数计算的成本如何呢?

当然,托Nuxt的福,请求方面用户在不刷新页面的情况下,仅第一次加载时的请求落在函数计算服务进行 SSR,此后的请求都是在前端请求接口,同时,我们也将静态资源全都转移到了对象存储上,因此对于函数计算的请求量其实是远小于整个Web项目,相当于用函数计算做了一个 SSR 服务。

实际开销来说,目前迁移到函数计算上的一个项目大致情况如下:

函数实例内存规格:192M(平均内存使用月130M)
WEB统计PV:18w(含前端路由跳转的PV)
函数计算调用次数:16w
函数计算资源使用量:1.2w CU·s
以本文截稿时的价格,若无免费额度,成本约为1.5元。(实际并未超过当前的免费额度)

可以说,对于流量不大的个人Web项目来说,开支基本上就是流量费用,计算成本相对来说可以忽略不计,不过国内的云服务,流量费用依旧不便宜,要省钱还是一门学问啊。

Nuxt.js 中按需加载 ant-design-vue

使用Nuxt.js构建网站时,使用UI库,能够更快的完成页面的构建,当盲目的引用,则会带入大量项目中不需要的组件,极大的增加构建后的项目大小,导致用户加载页面时间过长,文件过大,影响用户体验。因此按需加载就非常必要。

Ant Design 提供了多种按需加载的方案,对于 Nuxt.js 项目来说,因为SSR是在服务端进行,因此组件是在服务端和客户端都需要引入的,因此如果在单文件组件中引入指定的组件,通过loader加载时,会加载组件的css,但是服务端编译的时候是不认识css的,会导致报错,因此还是基于插件文件的方式,将插件文件中原本的:

import Antd from 'ant-design-vue/lib'

更改为

import {
  Form, Button // ... 你所用到的组件
} from 'ant-design-vue';

并在 nuxt.config.js 中的 css 里面去掉引用 antd 的 css 文件,改为通过build里面的babel插件引入

{
  // ...
  css: [
     // 'ant-design-vue/dist/antd.less',
  ],
  // ...
  build: {
    babel: {
      plugins: [
        [
          'import',
          {
            libraryName: 'ant-design-vue',
            libraryDirectory: 'es',
            style: true,
          }
        ]
      ]
    },
    transpile: [/ant-design-vue/],
  }
}

如果有在使用 Icons ,ant-design 的 Icons 会将全部用到没用到的图标全部引入,文件相当大,可以考虑新增一个 ant-icons.js 插件文件,指定项目中需要用到的 Icons 图标

export { 
  // 需要使用到的 Icons
  InfoCircleFill, DownOutline, UpOutline
} from '@ant-design/icons'

更改 nuxt.config.js 中 webpack 的设置:

{
  // ...
  build: {
    extend(config) {
      config.resolve.alias['@ant-design/icons/lib/dist$'] = path.resolve(__dirname, './plugins/antd-icons.js') // 引入需要的
    }
  }
}