T. / Journal

从一张背景图开始:最近给 selfweb 补的几块地基

个人网站Next.jsSanity性能优化
/back

Personal Notes

从首页背景加载、文章与随心的统一,到 Sanity 发布和缓存刷新。这次没有大改页面,而是把一个个人网站长期写下去时最容易出问题的几段链路补齐了。

这轮改动一开始只是想让首页那张莫奈背景图快一点。后来一路往下查,发现真正需要补的不是某一个组件,而是一个个人网站从“能打开”到“可以长期写、长期维护”中间缺的几段连接。

个人网站刚做出来的时候,很多问题都不明显。首页能打开,卡片能翻转,文章也能写;真正开始频繁打开、在手机上看、准备持续更新后,才会发现一些小地方一直在拖后腿:首屏图片慢,内容只能靠本地 Markdown,文章发布后还要等缓存,RSS 和 sitemap 也不知道有没有跟上。

这次没有重做视觉,也没有引入一套很重的架构。主要做的是把几个原来“暂时能用”的地方,改成能一直用下去的样子。


首页最慢的东西,往往就是最显眼的东西

首页背景是一张全屏铺开的画。它决定了进站第一眼的氛围,但也天然会变成首屏最大的资源。

之前的处理很直接:放一张大图,让它铺满屏幕。问题也很直接——浏览器先要下载比较大的原图,之后才能把首屏完整画出来。背景图不是普通配图,不能简单裁成很小的缩略图;一旦比例不对,整张画会被拉伸,网页里最容易被注意到的部分反而会变形。

最后没有走“换一张更小的图”这条路,而是保留全屏背景需要的比例,把资源整理成适合网页使用的 3840px 版本。文件体积从原先接近 7 MB 的量级降到了约 2.3 MB,同时仍然能覆盖大屏幕。

渲染方式也从普通图片换成了 Next.js 的 Image

<Image
  src={monetBackground}
  alt=""
  fill
  preload
  sizes="100vw"
  quality={75}
  className="object-cover"
/>

这里几个配置各自负责一件事:

  • fill + object-cover 让图片始终按比例覆盖全屏,不会被强行拉伸;
  • sizes="100vw" 明确告诉浏览器,这张图就是整个视口宽度,避免它按错误尺寸挑选资源;
  • preload 把首屏背景放进更靠前的加载队列;
  • quality={75} 再压一层体积,但不把画面压得发糊。

这类优化很难只用一句“快了百分之多少”概括,因为网络、设备和缓存都会影响结果。但改完后,浏览器不再把原始大图当成普通静态文件慢慢等,首屏最关键的资源有了明确的优先级和尺寸信息。对这个网站来说,这比继续堆动画更值。


文章和“随心”,不应该长成两套页面

接着做的是“随心”。

一开始它只是一个想法:文章更适合完整的技术整理,但很多零碎的感受、读书记录、实习里的片段,并不值得为了凑一篇文章而硬写成长文。如果没有一个低压力的出口,这些东西通常就留在 Obsidian 里,然后慢慢被忘掉。

最容易的实现方式,是复制一份文章目录页和详情页,改改文案,再接一个 notes 目录。这样短期看很快,但后面一定会出问题:文章页修了 SEO,随心页忘了;文章页多了 RSS,随心页没有;两边的日期、目录、返回链接开始一点点不一致。

所以这次没有复制页面,而是先把内容抽成共同的 ContentCollection。文章和随心只是两类内容,底层都走同一套读取、排序、详情页和目录组件:

posts  ─┐
        ├─ ContentCollection / JournalIndex / JournalEntry
notes  ─┘

这样做的结果不是“代码更高级”,而是以后改一个小细节时,只需要改一次。文章和随心可以有不同的名字、入口和内容气质,但它们的阅读体验应该是一致的。

顺手也补了首页入口、RSS 和 sitemap。现在随心不是一个藏在某个路由里的页面,而是和文章一样会出现在内容流、订阅源和搜索引擎站点地图里。


静态缓存不是“内容发出去以后等一会儿”

内容站点很适合静态缓存。大多数读者看到的是同一篇文章,没必要每一次访问都重新去 CMS 拉一遍数据。问题在于,缓存如果只有“缓存一小时”,写作者的体验会很差:刚发布一篇随心,打开网站却还是空的,不知道是没发布成功,还是缓存还没过期。

现在的策略是两层:

  1. 正常情况下,文章和随心静态缓存一小时;
  2. Sanity 发布、更新或删除内容时,用 webhook 请求网站的 revalidate 接口,主动刷新相关页面。

刷新范围不只是一篇详情页,还包括首页、文章目录、随心目录、RSS 和 sitemap。这样不会出现“详情页已经是新内容,但首页卡片还是旧的”这种很难解释的状态。

开发环境则不保留这层缓存。写内容、改 schema 的时候直接读取最新数据,避免本地调试时因为一个空结果被缓存住,又以为是自己代码写错了。

这个过程里我比较在意的是:缓存应该让读者更快,而不是让作者更困惑。定时缓存解决访问成本,发布时失效解决更新时效,两件事最好分开处理。


从 Markdown 到 Sanity,不是把写作交给后台

有了“随心”之后,本地 Markdown 的问题开始变得明显。

Markdown 很适合在编辑器里写,也适合版本管理;但如果每次发布都要打开项目、建文件、写 front matter、提交 Git、等 Vercel 部署,它更像一个开发流程,而不是写作流程。尤其是随心,本来就应该足够轻。

于是把内容迁到了 Sanity,但没有把 Markdown 一次性扔掉。

Sanity 里定义的是一份统一的内容 schema:标题、URL 标识、发布日期、更新时间、摘要、标签、封面和正文。文章与随心共用这些基础字段,只用类型区分。原有 Markdown 通过迁移脚本先预览,再确认写入;网站读取 CMS 失败时,仍然会回退到本地 Markdown。

这个回退看起来有点保守,但我觉得很有必要。个人网站的内容不应该因为 CMS 临时连不上、环境变量配错,或者某次迁移漏了数据就全部消失。Sanity 是新的发布入口,Markdown 仍然是内容的本地备份和兜底。

最后把 Studio 嵌进了 /studio。以后写随心不用进 GitHub,也不用重新部署网站:打开后台,填写标题、URL、日期、摘要、标签和正文,发布即可。发布成功后 webhook 会通知网站刷新缓存,前台内容就会更新。

这不是在追求“像大网站一样有后台”,而是把写作这件事从部署流程里抽出来。内容值得有自己的入口。


SEO 不是关键词堆砌,是让内容有正确的出口

之前我对 SEO 的理解有点粗:页面能被搜到,应该就差不多了。实际补起来才发现,它更多是在做内容之间的边界说明。

这轮把几个基本的东西补齐了:

  • 页面有稳定的 canonical 地址,避免同一内容被不同 URL 当成多份;
  • robots.txt 指向站点地图;
  • sitemap 同时收录文章、随心和各自的详情页,并用发布日期或更新时间作为 lastModified
  • RSS 把两类内容放进同一个订阅源,但仍保留“文章 / 随心”的分类;
  • 日期展示统一到“年、月、日”,不把 CMS 的完整时间戳直接丢到阅读页面上。

这些东西读者平时不一定能直接看到,但它们决定了搜索引擎、RSS 阅读器和分享链接看到的是不是一个完整的网站。对个人站来说,SEO 不需要装得很复杂;至少应该让每一篇内容有唯一地址、准确标题、摘要、时间和可被发现的路径。


这轮改动最后留下了什么

回头看,最开始只是首页背景图加载慢。现在留下来的东西却不止一张压缩过的图片:

  • 首页的最大资源有了更合理的加载方式;
  • 文章和随心共用一套阅读与索引结构;
  • 内容既能从 Sanity 发布,也保留本地 Markdown 作为兜底;
  • 缓存有发布时的主动刷新,不再只能被动等待;
  • RSS、sitemap、metadata 和日期格式都跟上了新的内容类型。

当然,网站还没到“什么都不用管”的程度。留言板的管理入口、CMS 内容的编辑体验、移动端细节,都还有继续修的空间。但这次之后,selfweb 至少不再只是一个一次性展示页:它开始有了适合持续写作、持续维护的基本节奏。

我现在比较喜欢这种改法——不追求一次把所有功能做满,而是每次遇到一个真实的不顺手,就顺着它往下看,找到应该补在哪一层。背景图慢,表面上是图片问题;继续往下,最后补到的是内容、缓存和发布链路。

个人网站大概就是这样慢慢长出来的。


作者:T | 汕头大学光电信息科学与工程 | AI Agent 方向 GitHub: github.com/iuyup

讨论