性能优化之SSR初体验

SSR是什么?

Server Side Render简称SSR。具体指html页面内容通过服务端渲染生成,浏览器接收到服务器响应时直接显示对应的内容就可以了。

ssr

了解完SSR就必须知道和它相爱相杀的CSRCSR全称Client Side Render,页面上的内容是js动态渲染出来的(VueReact等框架),服务端只返回一个html模板,其余渲染在浏览器完成。

csr

SSR也不算是一个新的概念,早期的后端PHPJAVA就已经支持服务端渲染,美其名曰模板引擎,如phtmljspjade等。最近几年前端工程化的日渐成熟,前后端开始分离,将原本的SSR模式转变成了CSR。同时也带来了一些性能上的缺陷以及SEO问题等等。

SSR优劣势

对比上面两张图SSR的优势很明显,在进行到第二阶段下载js文件时页面已经呈现,而CSR需要到第四阶段可交互时才显示页面。这对于首屏性能和**搜索引擎SEO**有着天然的好处。

当然SSR的劣势不小,对于现有的Vue或者React项目迁移成本很大;消耗服务器资源;框架生态支持不太完美。

从个人经验上来说,除非对项目首屏性能或者SEO有比较高的要求且可以容忍它的副作用时才使用,否则不建议直接使用SSR来开发项目。

SSR 实现的本质-虚拟dom

首先在SSR工程中,Vue代码(本文以vue举例,其他框架同理)会在客户端和服务端各执行一次。为什么会各执行一次呢?因为页面需要在服务端渲染完成再返回给浏览器,因此就必须在服务端先执行一次。还有一个问题就是服务端是怎么运行Vue代码的?这一点你可能觉得很好理解,毕竟Node也是基于JavaScript开发的,能够运行很正常。但是如果代码中涉及到dom的操作在Node环境是不支持的,Node环境下没有DOM的概念,甚至没有window这个全局变量的存在。

因此服务端直接操作Dom肯定是不现实的,好在Vue框架有一个虚拟Dom的概念。虚拟Dom是真实Dom的映射,简单理解为就是一个普通的js对象,我们在用Vue操作页面时其实操作的都是这个js对象,而非真实的Dom,这就使得SSR成为了可能。

经过上述分析,我们就能分析出所谓的服务端渲染就是在服务端完成虚拟Dom的组装,浏览器只要将虚拟Dom转换成真实Dom就可以了。相比CSR 的请求数据->组装虚拟Dom->渲染,少了两步,少的这两步都在服务端完成,且性能比用户浏览器更好。

那为什么说性能会更好呢?个人觉得主要有两个原因,首先服务器的性能比用户本地系统要好,而且不考虑money的情况下,可以一直加配置。其次服务器请求接口数据,远比用户浏览器发起http请求快的多。因为服务器请求是直接请求集群的,没有多余的DNS等解析过程,而且不受用户本地网络的影响。

SSR实战-大数据表格渲染

前期准备

  • Vue-cli3
  • Nuxt
  • koa (用于模拟接口数据)

1. 模拟接口数据

模拟数据来自某顺的爱基金数据,接口采用读取本地文件的形式,不采用爬虫的形式实时抓取(保护下数据源的安全)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();
const cors = require('koa-cors');
// 解决跨域问题
app.use(cors());

router.get('/jijin', async (ctx, next) => {
const fs = require('fs');
// 读取本地json文件作为接口响应数据
const data = fs.readFileSync('jijin.json', 'utf-8');
ctx.body = data;
})

app.use(router.routes());
app.listen(5000);
console.log('app started at port 5000...');

2. 基于Vue-cli3 + element UI搭建一个表格

核心代码如下,其余部分代码参考Vue CLI Element

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<template>
<div id="app">
<el-table
:data="tableData"
height="500px"
style="width: 100%">
<el-table-column
type="index"
label="序号"
fixed
width="100">
</el-table-column>
<el-table-column
prop="code"
label="代码"
fixed
width="100">
</el-table-column>
<el-table-column
prop="SYENDDATE"
label="更新日期"
fixed
width="180">
</el-table-column>
<el-table-column
prop="F005"
label="F005"
width="180">
</el-table-column>
<el-table-column
prop="F008"
label="F008"
width="180">
</el-table-column>
<el-table-column
prop="F009"
label="F009"
width="180">
</el-table-column>
<el-table-column
prop="F010"
label="F010"
width="180">
</el-table-column>
<el-table-column
prop="F011"
label="F011"
width="180">
</el-table-column>
<el-table-column
prop="F012"
label="F012"
width="180">
</el-table-column>
<el-table-column
prop="net"
label="net"
width="180">
</el-table-column>
<el-table-column
prop="net1"
label="net1"
width="180">
</el-table-column>
<el-table-column
prop="prerate"
label="prerate"
width="180">
</el-table-column>
<el-table-column
prop="typename"
label="类型">
</el-table-column>
</el-table>
</div>
</template>

<script>


export default {
name: 'App',

data() {
return {
tableData: []
}
},

created() {
this.getData();
},

methods: {
getData() {
fetch('http://127.0.0.1:5000/jijin').then(res => {
return res.json();
}).then(res => {
const { data } = res.data;
const MAX_LIST = 200; // 数据太多控制下数量
let i = 0;

this.tableData = [];
for(let key in data) {
if(i === MAX_LIST) {
break;
}
this.tableData.push(data[key]);
i++;
}
})
}
}
}
</script>

运行效果:

运行效果

3. nuxt 改造表格实现SSR

nuxt.js 食用方式见Nuxt.js - Vue.js 通用应用框架 | Nuxt.js 中文网 (nuxtjs.cn)

nuxt的代码结构与Vue-cli差别不大,不需要过多的改动(仅限于简单demo项目,复杂项目迁移是比较麻烦的),所以只需要实现服务端异步获取数据这个逻辑即可。

nuxt提供了服务端异步获取数据的方法asyncData,但是有局限性只适用于页面级组件,肤浅的理解就是项目pages目录下的组件,具体实现如下:

pages/Index.vue

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
<template>
<Tutorial :tableData2="tableData"/>
</template>

<script>
export default {

data() {
return {
tableData: []
}
},
// 异步获取数据,并传递给子组件
async asyncData() {
return await fetch('http://localhost:5000/jijin').then(res => {
return res.json();
}).then(res => {
let tableData = [];
const { data } = res.data;
let i = 0;
const MAX_LIST = 200;
for(let key in data) {
if(i === MAX_LIST) {
break;
}
tableData.push(data[key]);
i++;
}
return {
tableData
}
})
}
}
</script>

components/Tutorial.vue,该组件是在浏览器执行,因为上级页面组件已经在服务端取到了数据,所以不需要调用接口,直接通过props获取数据渲染即可。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<div id="app">
<el-table
:data="tableData2"
height="500px"
style="width: 100%">
<el-table-column
type="index"
label="序号"
fixed
width="100">
</el-table-column>
<el-table-column
prop="code"
label="代码"
fixed
width="100">
</el-table-column>
<el-table-column
prop="SYENDDATE"
label="更新日期"
fixed
width="180">
</el-table-column>
<el-table-column
prop="F005"
label="F005"
width="180">
</el-table-column>
<el-table-column
prop="F008"
label="F008"
width="180">
</el-table-column>
<el-table-column
prop="F009"
label="F009"
width="180">
</el-table-column>
<el-table-column
prop="F010"
label="F010"
width="180">
</el-table-column>
<el-table-column
prop="F011"
label="F011"
width="180">
</el-table-column>
<el-table-column
prop="F012"
label="F012"
width="180">
</el-table-column>
<el-table-column
prop="net"
label="net"
width="180">
</el-table-column>
<el-table-column
prop="net1"
label="net1"
width="180">
</el-table-column>
<el-table-column
prop="prerate"
label="prerate"
width="180">
</el-table-column>
<el-table-column
prop="typename"
label="类型">
</el-table-column>
</el-table>
</div>
</template>

<script>
export default {
props: ['tableData2'],
}
</script>

运行效果:

运行效果

4. 对比结果

  • 未优化的表格有很长时间的空态,SSR优化后感知不到空态

  • ssr优化后页面html文件请求速度变慢,但是页面展现数据速度有很大提升

  • 性能数据(finish_time)没有太大改善,只是用户体验上提升了

一些想法

  • SSR的部署必然涉及到服务器,那么就需要考虑到一些容灾处理,比如ssr服务被打爆了如何做降级等
  • 旧项目迁移成本很大,耗时耗力,最好有自动迁移的工具或者脚本支持
  • 对于没有服务端知识的前端开发来说学习成本略大,需要清楚服务端的一些配置等等
  • 容忍不了SSR缺点的情况下不建议用SSR