如何让你的 Nextjs SSR 应用更快的响应~
最近在折腾了一把我的 SSR 网站加速,顺带把公司的网站也加速了一把。
未改进之前
改进之后
效果很明显,服务端耗时是原来的的 1/20。
以下是我在这个过程中采取的措施。
服务端请求使用内网服务地址
众所周知,一个请求的流程是 DNS Lookup、TCP connect、TLS Handshake、Server Processing、Content Transfer。
由于我们的 SSR 应用服务端发出的请求全是在内网,所以不再需要 HTTPS ,减少了 TLS 握手的时间。
合并服务端异步请求
服务端可能会请求多个接口数据进行服务端渲染,如果依次获取数据会造成阻塞。
最优解是使用Promise.all 进行合并请求,一次同时发送多个请求。
移除不必要的服务端请求
很多人都会有一个误区,SSR 渲染全部数据一定要在服务端获取。
其实这是不必要的。
在服务端过多的请求数据会增加服务的不稳定性,一定要弄清做 SSR 渲染的目的,而不是为了 SSR 而 SSR。
在我的理解里,为了 SEO 或者 可视区内的渲染才需要做 SSR 。
移除全局的服务端状态注入
之前的状态管理方案使用了服务端注入,因此每次 SSR 渲染其实是需要请求一次全局状态相关的接口,然后再请求业务相关接口。
这样的流程存在较大的不合理之处,本来不需要状态的页面也被强制注入了状态,增加了耗时。
因此, 我移除了服务端相关状态注入并且改用在 _app.tsx 文件的 useEffect 中处理和用户相关的状态。
本次优化中,我把状态管理 Mobx 迁移成了 Hox。
自定义 Server 中使用缓存
我的服务并没有使用官方自带的服务器,而是自定义了一个 Server 方便我进行扩展。
SSR 服务渲染的原理大概就是服务端生成客户端能运行的页面。
在移除全局服务端状态管理后,实际上每一个用户获得的无状态页面都应当是相同的。
于是我们就可以通过缓存生成的页面来加速访问。
官方的 Examples 里有一个使用 cacheable-response 进行页面缓存的例子可以进行参考。
保险起见,我使用了 Redis 作为缓存存储而不是默认的内存存储。
此外,请注意服务端渲染中和用户 Cookies 有强相关的页面,请务必使用 Middleware 处理避免缓存。
// 简单案例
export const OptionSSRCache = (ssr: (opt: any) => any, force?: boolean) => (
req: Request,
res: Response,
next: NextFunction
) => {
// 没有token 或者 强制缓存 直接取缓存
if (force || !req?.cookies?.[TOKEN_NAME]) {
res.setHeader("X-Powered-By-Render-Type", "cache-render");
// 缓存实例
return ssr({ req, res });
}
// 存在token 服务端渲染
if (req?.cookies?.[TOKEN_NAME]) {
res.setHeader("X-Powered-By-Render-Type", "ssr-render");
return next();
}
};
移除 SSR 自带的组件跳转,添加 prerender,使用 a 标签。
SSR 渲染页面成功之后,在浏览器端就变成一个 SPA 应用。
使用内置的 next/link 之类的前端路由组件跳转,前端会使用发起 ajax 请求数据,渲染下一个页面。
但是这 不够快。
我们的目标的链接如果已经有了缓存,直接使用 a 标签是最快的方式。
问题就来了,此时目标链接可能没有缓存,那么我们怎么样让目标存在缓存呢?
prerender 预先资源加载。
示例代码如下
// 动态生成 link prerender
import { useRouter } from "next/router";
import { CSSProperties, useEffect } from "react";
interface ActiveLinkIProps {
children: JSX.Element;
href: string;
activeCSS?: CSSProperties;
defautCSS?: CSSProperties;
}
export default function PrerenderLink(props: ActiveLinkIProps): JSX.Element {
const router = useRouter();
const css =
router.pathname === props.href
? {
...props.defautCSS,
...props.activeCSS,
}
: props.defautCSS;
useEffect(() => {
if (document.getElementById(props.href)) {
return;
}
// 创建预渲染
const d = document.createElement("link");
d.setAttribute("rel", "prerender");
d.id = props.href;
d.href = props.href;
// 插入预渲染
const s = document.getElementsByTagName("script")[0];
s?.parentNode?.insertBefore(d, s);
}, [props.href]);
return (
<>
<a href={props.href} style={css}>
{props.children}
</a>
</>
);
}
此时浏览器会在合适的时间后台加载目标链接,当用户打开链接时服务器已经存在了缓存,效率极大提升。
静态资源上CDN
常规操作,不再赘述,静态资源上 cdn 之后能减小服务端的下行压力。
相关链接
特别鸣谢
感谢以下用户对本文的支持与鼓励