服务端渲染大揭秘
前言
还在替弟弟学业操心的我听到一声:SSR!姐我 SSR 了!
我:???
小学生都会 SSR 了?让我清醒一下。
哦不,是这个啊~~
忙于 coding
的你一定听说过 SSG
、SSR
这些名词,究竟是啥一起来揭秘。
那什么是 SSR?Super Super Rare?
SSG
全名 Static-Site Generator
,静态站点生成,听着名字就晓得是静态的、在构建的时候就生成了,那么想要更新网站内容就要重新构建,这适合企业官网 or 个人博客等,没有频繁更新的诉求。优点想想就很明显,速度快(连 api
都没肯定快了),部署方便(就静态文件丢上去就好使),安全(纯静态也没有 sql
注入)..
SSR
全名 Server-side rendering (SSR)
,服务端渲染,
CSR
的优点很明显,那缺陷也很明显(这不是废话),即使你做了 dynamic import
,split chunk
, 但也免不了 bundle
,一个做了 3 年的大应用 bundle
大小 1m 是很常见的,浏览器首次构建 HTML
拿到的是空的,要等 js
执行完了再开始动态渲染改变 DOM
树,这个期间还会请求 api
,据获取数据将数据渲染到页面,完成显示,想想时间就很长。
由于服务器(针对任何页面)提供的初始 HTML
不包含任何特定于应用程序的 HTML
,搜索引擎将该网站视为空白,没有任何内容。因此,尽管你的网站有巨大的流量或相关内容,但它可能不会出现在搜索结果的顶端。
大致脑海里知道 SSR
快,究竟什么快?
如果在服务器上执行 js
,HTML
在服务端就可以装配好了,返回给浏览器渲染,页面就可以有初步的展示,但是在服务端是没有 Window
没法进行绑定的,因此在客户端还要执行一遍脚本,执行生命周期方法,对事件就行绑定,对 DOM
进行 diff,在这段事件完成之前,
服务端和客户端要执行一套代码,就是同构,以 react
为例,hydrate
不会再重新渲染 HTML
.
要解决的问题
同构(Isomorphic rendering)
,就是服务端和客户端一套代码,服务端去渲染,客户端来负责交互。
路由的同构与数据预取
客户端使用 BrowserRouter
,服务端使用 staticRouter
,在 node
端没有 history
对象,只是根据请求的路由返回匹配的 React.createElement
,matchRoutes
方法实现路由匹配
数据的预取
声明路由的时候把数据请求方法关联到路由中,比如定一个 loadData
方法,然后在查找到路由后就可以判断是否存在 loadData
这个方法。
// routes.ts
const routes = [
{
path: "/",
component: loadable(() => import("./Com")),
loadData: () => getData(),
},
];
const loadData = () => {
const promises: Promise<unknown>[] = [];
routes.some((route) => {
const match = matchPath(ctx.request.path, route);
// 调用定义的获取数据的方法
if (match && route.loadData) promises.push(route.loadData());
return match;
});
return Promise.all(promises).then(() => {
return Promise.resolve(
<StaticRouter>
<App />
</StaticRouter>
);
});
};
// 预取的数据写入HTML(ejs)
动态加载以及资源获取
使用 loadable
库 主要原因是获取资源映射,当路由匹配到 key
获取 value
资源 塞到 HTML
返回字符串给客户端。并且 dynamic import
在客户端可以用 React.lazy
,但在 18
之前不能用,如果用 React.lazy
主 js
加载并执行之后才能加载对应页面的 bundle
,增加了 TTI(首次交互)的时间,即使 react18
可以了资源映射还是需要自己来获取( loadable
逃不掉了),@loadable/webpack-plugin
可以打出来资源映射的 map
,交给 ChunkExtractor
,思路是首先匹配路由,根据匹配到的路由取相应的映射资源,加载资源
// webpack配置
const LoadablePlugin = require('@loadable/webpack-plugin');
module.exports = {
module: {
rules: [],
},
plugins: [
new LoadablePlugin(),
...
],
};
// 资源映射
const statsFile = path.resolve(__dirname, '../dist/asset/loadable-stats.json'); // 上面的loadaer默认打出来这个名字
...
const extractor = new ChunkExtractor({
statsFile,
publicPath: '/',
});
// extractor.getLinkTags(), extractor.getStyleTags(), ...
渲染同构
React.hydrate
水合 这个 api
,对节点进行对比,客户端执行生命周期方法,不会再重新渲染 HTML
,比对客户端和服务端的 HTML
节点做 diff
,比对结果不一致的时候,HTML
上的属性不会被替换,会把不一样的子节点替换,会抛出指向出错节点 warning
,需要手动处理。
数据同构
吐槽一下比较蛋疼的解释(国内大多数文章会出现的概念),从数据层面,把数据放到 Window
上叫注水,把数据从 Window
取出来叫脱水,属实比较难理解,简单理解就是服务端将数据写到 ejs
的模版里,作为全局变量,服务端就从 Window
上取这个变量,实现数据的同构。
///rendux的数据
// ejs模版
<body>
<div id="root"><%- html %></div>
<script type="text/javascript">
window.REDUX_PRELOAD_DATA = <%- preloadState %>
</script>
<%- reload -%>
<%- scriptTags %>
</body>
// server/app.ts
ejs.renderFile(
template,
{
...
// 将preloadState变量写入ejs模版
preloadState: JSON.stringify(ctx.store.getState()),
...
},
{},
(err, str) => {
...
},
);
});
取到路径,key
(路由路径) value
(页面所需的资源)
loadable 如何知道的页面路径?
const jsx = extractor.collectChunks(reactApp);
其实这块是创建 provider
,然后下面的 loadable(() => import(''))
相当于 consumer
。
纸巾一擦,咱继续,继续,接着 wu,接着 tiao。
性能监控
监控 nodejs
v8 堆内存,内存超出 80%进行服务降级,以及在时间范围内(可能半小时检查一次,试业务情况而定),将不健康的容器部署到其他实例。
process.memoryUsage()
返回一个对象,描述 Node.js
进程的内存使用量(以字节为单位)。
import { memoryUsage } from "process";
console.log(memoryUsage());
// 打印:
// {
// rss: 4935680,
// heapTotal: 1826816,
// heapUsed: 650472,
// external: 49879,
// arrayBuffers: 9386
// }
heapTotal
和heapUsed
指的是 V8 的内存使用情况。external
指的是绑定到 V8 管理的JavaScript
对象的C++
对象的内存使用。rss
,Resident Set Size
,是进程在主内存设备(即总分配内存的一个子集)中占用的空间量,包括所有C++
和JavaScript
对象和代码。arrayBuffers
是指为ArrayBuffer
和SharedArrayBuffer
分配的内存,包括所有 Node.js Buffer。 这也包含在external
值中。 当 Node.js 用作嵌入式库时,此值可能是 0,因为在这种情况下可能不会跟踪ArrayBuffer
的分配。
抛开一切,我们用 Next.js 吧
上面提出的问题,Next.js
都可以完美解决,通用级 SSR
解决方案,虽然上面说了一堆原理,但作为企业级解决方案依然不够,Next.js
大量的代码在处理各种兼容性的问题,作为体量较大的react项目,选取通用性方案更为推荐。
混合渲染
我们在实际业务中常常是部分页面需要 SSR
,其余的依旧 CSR
。
对于我们自己搭的简易 SSR
可以在配置白名单,做请求匹配的时候, 位于白名单的吐空的 HTML
字符串(仅有 css、js 资源的);或者在 nginx
一层做拦截,白名单转发到 CSR
的地址。
Next.js
为这种混合渲染提供了更为简单的方式,提供 getStaticProps
静态生成的 api
,在这里面的请求会被在构建的时候请求好,写入数据到Window
。如果没有 export getServerSideProps
方法,就会默认走 SSG
渲染,getServerSideProps
是只会在服务端执行的 api
,因此在静态生成的时候不会导出getServerSideProps
,下面是静态生成的代码。
export async function getStaticProps() {
// Call an external API endpoint to get posts.
// You can use any data fetching library
const res = await fetch("https://.../posts");
const posts = await res.json();
// By returning { props: { posts } }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
posts,
},
};
}
服务降级
在 node
服务器不健康的时候,达到毫秒级 CSR
,即不需要依赖
对于我们自己搭的框架,可采取 server
和 client
分开打包,给 client
打包产出加上 HTML
文件,就可以单独托管了。下图是 nginx
配置样例及优雅降级的原理图,解释下就是用户请求到 nginx
,如果服务器正常就会转发到 node
渲染服务器,如果异常返回异常状态码,拦截异常状态码,并重写成 200
,转发到 HTML
静态文件服务器。
自然而然优雅的服务降级
既然自带 SSG
,那么我们的服务降级便可采取他的静态生成,官方提供 next export
命令,可以直接生成 SSG
产出,将每个路由都打出一个 HTML
文件,里面会引入所需要的 css
和 js
,需要注意的是想要一套代码就需要种植环境变量,因为 SSG
要求是不能暴露getServerSideProps
,兼容处理代码如下
// .sh
export NEXT_SSG = SSG
// .tsx
let getServerSideProps =
process.env.NEXT_SSG === "SSG"
? undefined
: async () => {
const res = await fetch(`url`);
const post = await res.json();
return { props: { name: post?.data?.token } };
};
export { getServerSideProps };
将打出来的产出丢到 nginx
,如下配置
server {
root /www/data;
location / {
try_files $uri ;
}
}
如果取到了返回路径下的 HTML
,客户端拿到 HTML
字符串之后再 hydrate
,实际上还是 CSR
,实现了不走 node
服务器的优雅降级。
自带的性能分析
对于 web
应用的性能分析总是绕不开Web Vitals
的几大指标,Next.js Analytics
为我们提供了非常方便的 api 来获取这些指标数据。
对于部署在托管在Vercel
的项目,在其Analytics
tab 页签中就可以看到可视化的指标数据。
对于自托管的项目可以通过也是可以进行 web 性能分析的。
仅仅只需要创建一个_app.js
在其中暴露一个名为reportWebVitals
的方法。
Next.js
会在完成任何一个指标计算的时候调用该函数。
//_app.js
export function reportWebVitals(metric) {
console.log(metric);
}
// 打印
// {
// id: "1628518848412-9295257969280",
// label: "web-vital",
// name: "TTFB",
// startTime: 0,
// value: 815.5,
// }
id
:指标唯一的标识符;label
: 是指标类型,分别是web-vitals
和custom
;name
:指标名称;startTime
: 以毫秒为单位,所有记录该指标的时间戳;value
: 以毫秒为单位,指标的值或者持续的时间。
web-Vitals
是谷歌提出的用来统一衡量web
页面用户体验和质量的指标。Next.js
为我们提供了一下五种指标数据:
- 首字节时间
TTFB
- 首次内容绘制
FCP
- 衡量加载性能
LCP
- 衡量可交互性
FID
- 衡量视觉稳定性
CLS
custom
这是Next.js
提供的独有的指标,用来衡量 hydrate
和 render
时间
Next.js-hydration
:页面开始和完成hydrate
所需的时间(以毫秒为单位)Next.js-route-change-to-render
:页面在路由改变后到开始渲染的时间(以毫秒为单位)Next.js-render
: 路由更改后到页面完成渲染的时间(以毫秒为单位)
通过这个函数我们就可以创建自己的性能分析报告,这还不香么!
其他
动态加载、动态路由匹配等 next10
均已经支持,需要的可以移步文档哦~
www.nextjs.cn/docs/gettin…
试情况选择 serverless
什么是 serverless
呢,广义来说,自动扩容 按需计费 noops
无需运维符合这些就算是了,本来 serverless
就是很抽象的概念,大致步骤是 SSR
应用放在函数中,serverless
有触发器,选用 http 触发器,触发器中可以添加路由,接收到的路由传递给 Next.js,再返回 HTML
给客户端。
Serverless
能解决什么问题?
Serverless
可以使应用在服务端免运维。在没有流量的时候缩容为 0
,节省流量。可以节省不少开支。是性价比高的方案。
对于落地页可能在特定情况流量增多,以及边缘服务等,使用 Serverless+SSR
可谓是完美配合~。
啥叫免运维呢,将一个服务 部署在服务商给我们提供的 运行环境中,不需要关心运维相关的东西,只需要关心业务代码,我们也不需要维护物理机 虚拟机 之类的 Linux
。
各大云服务厂商有封装好的 Next.js
服务,可以简单操作快速部署 Next.js
。
有需要可以自行搜索哦,就不贴了~
总结
介绍了 ssr、ssg 是啥 解决了什么问题、原理以及性能监控,通用级别 ssr 框架 nextjs 如何做优雅服务降级,(是不是已经跃跃欲试想实操),nextjs 依旧在持续更新中并在几天前发布了 11,支持 module federation,微前端也可以用 nextjs(手动狗头)。
番外
我是萱酱,是个 lo 娘 FE(鼓励师划掉),路过的朋友给个三连叭~你的支持是萱酱创作的动力(说的我都感动了),我会持续更新~