0x1 说说旧版博客的问题
事实上,我想重写博客也不是一天两天的事情了,早在 3 年前,我就写了一篇文章聊这个问题,当时的根本原因是 Vuetify 这个库资源释放不完全,VuePress 在渲染页面的时候直接在内存里用几个库模拟 dom 跑,一旦 ui 库的资源释放有问题,文章页多起来内存占用就会爆炸,但是事实上我写这篇文章的时候博客文章数也才十几篇,编译性能可以说是拉得不能再拉了。
后来换了一个叫 VueMaterial 的库凑合一下,主要是组件风格差不多,不用重写大量的样式,但是好景不长,20 年中左右 VuePress 出毛病了,估计是它依赖的某个依赖没有遵循语义版本,在小版本 break change 了,只要我升级任何 VuePress 相关的依赖,编译立马就爆炸。于是直到 2023 年的今天,我的旧版博客依然赖在 nodejs 14,因为 VuePress 的版本还在 1.x,在高版本 nodejs 也会爆炸,而且我也升级不了依赖,这让我感到很烦。
另一个问题在于,VuePress 作为一个 ssg,与自身主题绑定太深,很多接口都没有暴露,你要么用官方主题魔改,要么翻源码 hack 接口,这导致我想有任何文章渲染相关的二次开发都很难做,我不清楚新版有没有改进,但是我自从用了 react 之后就再也不想写 vue 了。
各种原因之下,我终于在 20 年决定重写,不过技术选型也是个很麻烦的事情,和工作不一样,自己的坑当然要用自己喜欢的技术选项,所以于是我开始走进第一个深坑。
0x2 第一次选型:NextJS SSG
NextJS 作为近几年大火的框架,有其独到之处,文档十分健全,API 设计相比 VuePress 也合理很多。我的第一次重构始于 20 年 3 月,那时候 next 的版本还在 9.x,SSR 和 SSG 远没有现在完善,当然这不是问题,问题在于,当时的很多 ui 框架仍然没有准备好支持 SSR 和 SSG,大量的 UI 元素需要使用 lazy load 延迟加载以避免在 SSR 时加载。
这个问题导致,虽然我们能够在服务端用 MDX 之类的格式把文章正文渲染出来,但是实际的页面加载仍然需要还在大量的 ui 组件 js。这也就失去了 ssr/ssg 的意义了,因为文章正文所占用的包大小相比 ui 库只能算是九牛一毛,我还不如写成 SPA 页面,让它在客户端加载呢,这样我的包大小其实没什么变化,还省掉了编译过程。
0x3 第二次选型:React SPA + Rust Prerender
仔细分析需求,其实一个博客的内容页大部分情况下是不需要变的,需要变的只有 3 部分:首页、内容页、标签/分类页。
第二次选型发生在 22 年 1 月。我正在公司主导一个新项目,她被称为 JWT,是 OctoBase 的前身。JWT 的目的是为了管理 AFFiNE 所产生的文档,并提供全文搜索、双向链接、智能推荐等功能,这意味如果我在 AFFiNE 中编写了一系列的文章,我将可以通过 JWT 将它们读取出来,并做任何的后处理。那么我需要做的只是在 React SPA 中实现从 JWT 中读取文章就行了。
后来,随着我司项目的推进,JWT 在 Rust 中重写,这时候她已经被改名为 JWST,于是我又有了新想法,如果我在 Rust 中直接通过 JWST 读取,并将文章渲染到某个模版上,这不就是 SSG 了么,我只需要预先把一个 React SPA 准备好,就可以在 Rust 中把整个站点渲染出来了,这样做可比在 nodejs 中跑一个 React SSR 服务器,然后把页面抓下来快多了。
相比上一次选型,这一次直到 22 年 9 月左右其实已经大体上写完了,但是当时界面库用了 MUI,这个库别的都挺好,就是它用的样式库 Emotion 太大了,这导致 SPA 里的很多样式是 js 加载完后再渲染的,这很不好,如果在弱网环境下,博客的样式很容易烂掉,而且 js 加载完后 Emotion 重载样式在低配机子上会让页面闪烁一次,这个很影响观感。
第二个问题是我发现有一个问题是单靠在 Rust 中预渲染内容解决不了的:代码高亮/公式高亮。Rust 中并非没有公式渲染库,但是代码高亮库似乎还没有成熟的方案。最终因为这两个问题,我走向了最终方案。
0x4 最终方案:React + vite-plugin-ssr + Data Warehouse(OctoBase)
最终的方案仍然是一个 SSG 方案,只是我并没有选择 NextJS。NextJS 作为一个商业项目,尤其是它的目的其实是吸引用户使用昂贵的 Vercel 平台,而我对此并不感冒,近两年 NextJS 的发展路径都在围绕着如何让用户把他们的网站往 Vercel 迁移--哪怕只是个静态网站,因此对 SSG 的支持也逐渐放下,很多功能都只能在服务器版本中使用(例如图片优化等,这个我在 OctoBase 里其实只要不到 100 行代码就实现了),因此我选择跳过 NextJS,转而选择其他框架。
经过一圈试用,我认为 vite-plugin-ssr 这个插件是比较符合我的口味的,插件没有太多的黑魔法,只需要做简单的适配就可以做 SSR 和 SSG,走的也是正常的 React Hydration,而且我可以按需做很多自定义。与时俱进地,我抛弃了仍未上轻量 CSS in JS 的 MUI,选择 Radix UI 作为 UI 库--不过其实用到的地方不多,这个库可以按需加载,我只需要在样式写起来比较麻烦的菜单、头像等地方按需加载它的组件即可,剩余的地方全部都是手写的 CSS in JS,并且支持 SSR,不需要在客户端由 js 加载样式。
最终的效果是十分好的,拿 PageSpeed 跑了一下,分数直接拉满,相比以前用 VuePress 的 70 多分好太多了,使用浏览器模拟,即使在 3G 网络下也只需要一两秒即可全页加载出来,很丝滑。
这仅仅是前端的部分,我在后端直接使用了我正在负责的公司项目 OctoBase 作为数据源,这意味着我可以在 AFFiNE 上编辑文章,并按需公开,公开的文章将自动通过 Cloudflare Worker 渲染并更新网站,整个过程都是自动的:
当然,自己做 SSR 的另一个好处是可以更自由地优化文章页的内容,譬如说,我在编译链路中使用 shiki 作为代码高亮 Renderer,这意味着我的页面其实在 SSR 的时候就已经包含了代码高亮,我不再需要为了让代码高亮在客户端渲染而引入一坨 highlight.js 或者 prism.js。
从目前博客加载的 js 可以看到,目前整个博客的 js 加起来也就 100k 出头,其实里面 react 和 react-dom 占了很大一部分,我觉得目前这样已经挺不错了,不过还有优化空间,之后有空可以继续优化下。