技术变迁
WEB 网页的开发在较早时期由服务端工程师代劳的,比如使用 PHP 从数据库获取数据之后通过模板引擎生成的网站,比如 Python 中 jinja 模板。
随着网页对于交互的需求的增加,最重要的一点是随着移动互联网的发展,终端设备的性能增强,我们可以通过 JS 脚本去生成界面的 DOM 元素,通过 XHR 请求数据完成页面填充。比如我们常用的框架如 Vue、React 就是这种方式。 但是这种处理方式也有对应的弊端,比如 html 文件内容为空,具体的表现就是白屏,另外搜索引擎的爬虫也是通过爬取 HTML 文件的内容从而收集关键词,这种方式不利于网站的 SEO 表现。
随后为了弥补上述存在的这些问题,出现了 Nuxt.js、Next.js 这些服务端渲染框架。
渲染方式
上述的这些技术对应的渲染方式可分为三种:服务端渲染、客户端渲染以及同构渲染。
服务端渲染
服务端渲染也被称为 SSR(Server Side Render),即通过服务端字符串拼装的方式组成 HTML 文件,以一个简单的 NODE 服务为例:
js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
const content = req.query.content || 'Hello, SSR'
res.contentType('text/html')
res.send(`<html>
<body>${content}</body>
<html>`)
})
app.listen(3000, () => {
console.log('server is running at 3000')
})
在服务接受到请求之后会返回一段拼接的 HTML 字符串,返回的类型为 text/html
标明为一段 HTML 文本,这种渲染方式的优点就是首屏渲染速度较快因为 HTML 结构是在服务端处理的,但过度依赖服务端会导致服务端压力过大,访问流量过大的情况会导致服务挂掉,所以需要对服务进行容灾处理,兜底兼容异常场景。
此外由于每一次请求的路由地址都对应服务端对应的路由处理,所以在页面路由切换时,会出现一段时间白屏等待服务端返回页面内容,从前端的角度可以近似理解为每一次切换都会重新调用 document.body.innerHTML
。
客户端渲染
服务端渲染一般简称为 CSR(Client Side Render),顾名思义,请求路径返回的 HTML 的 body 结构中没有填充对应的 dom 节点,这些节点是通过异步请求执行 js 文件动态挂载的,这个过程是在浏览器(客户端)中完成的,目前主流的 SPA(单页面应用)框架都是这种渲染方式,只是框架具体实现细节不同,如是否使用虚拟 DOM,目前由于大部分使用的都是 React 和 Vue,所以我们以虚拟 DOM 渲染出 DOM 节点的方式看一下这种渲染方式:
html
<html>
<body>
<div id="app"></div>
</body>
<script>
const domList = [
{
tag: 'div',
children: '1111',
props: {
id: 'test',
},
},
]
const render = (domList, container) => {
for (let i = 0; i < domList.length; i++) {
const el = document.createElement(domList[i].tag)
el.innerHTML = domList[i].children
for (let key in domList[i].props) {
el.setAttribute(key, domList[i].props[key])
}
container.appendChild(el)
}
}
render(domList, document.getElementById('app'))
</script>
</html>
虚拟DOM本质上是对DOM结构的抽象,使用JS对象描述一个DOM的全部属性,根据这些信息在执行JS脚本时对节点进行挂载,从而实现页面的渲染。
这种渲染方式由于需要在加载HTML模板文件之后再解析的过程中异步下载JS文件并解析执行之后才能显示出网页内容,所以在初始化时会存在一定时间的白屏时间,当然这个加载性能可以通过一定手段进行优化,参考我的文章。 juejin.cn/post/732599...
但是这种渲染方式的优点就是在路由切换时路由地址的变化并不会真正的刷新整个页面,仅是对节点进行替换,不存在重新加载页面的过程,所以访问过程中使用体验比较好。
同构渲染
上面介绍了两种渲染方式,但是这些方式都会存在一些比较明显的缺点,同构渲染就是用于弥补这些比较明显的缺点,解决方式就是通过首屏的页面加载通过SSR的方式渲染,后续的访问流程基本与CSR的渲染一致。
当然理想的情况下是这样,但是框架实现还存在一些问题,以Nuxt.js举例,在服务端渲染时,虚拟DOM的渲染本质是也是模版字符串的拼接:
js
const domList = [
{
tag: 'div',
children: '1111',
props: {
id: 'test',
},
},
]
const render = (domList, container) => {
for (let i = 0; i < domList.length; i++) {
let props
for (let key in domList[i].props) {
props += `${key}="${domList[i].props[key]}"`
}
// 拼接标签属性
const el = `<${domList[i].tag} ${props}>${domList[i].children}</${domList[i].tag}>`
container += el
}
return container
}
首屏的渲染可以通过类似的方式生成一个快照,从而减少FCP的时长,但是这个模板是没有任何事件响应处理的,所以还需要一个"激活"的过程,这个过程会遍历目前所有DOM元素与虚拟DOM一一对应,激活对应的事件进行交互,所以页面的TTI(交互时长)是没有发生变化的。
这个问题目前也有一些对应的方案,如Qwik这个框架,有兴趣可以了解一下。
总结
不同的渲染方式适用于不同的业务场景,比如管理后台、数据平台这些面向B端用户的平台一般不需要SEO,所以可以使用CSR的渲染方式,但是一些C端的项目,如活动页面、官网等这些对SEO或首屏加载速度有较高要求的网页可以使用同构渲染的方式。