架构 · · 2 分钟阅读
静态优先:把运行时压到零
内容站不需要服务器。真正需要的是想清楚哪一小块确实需要写操作。
静态站最容易犯的错,是把它当成”不能有动态功能”。
其实不是。静态站的意思是:读路径上没有你的进程。写路径上有没有,是另一个问题。
读路径:能编译就不要计算
一篇文章从 Markdown 到 HTML,需要做的事是确定的:解析、高亮、生成目录、算阅读时长、生成 RSS。这些事的结果只取决于输入,所以只该做一次,而不是每次有人访问都做一遍。
这就是构建期渲染的全部理由。它不是”性能优化”,是把计算放到它该在的位置。
我在这套站上的具体做法是:
- 文章、相册、鱼种资料全部进 Git,构建期用 Zod 校验,字段错了构建直接失败。
- 图片在构建期转 AVIF/WebP 并生成多尺寸,产物里只有优化后的图。
- 站内搜索的索引也在构建期生成,运行时只是在一个数组里做子串匹配。
最后一条值得多说一句:文章少于一百篇的时候,任何”搜索引擎”都比不上一个 Array.filter。引入一个索引库、一个分词器、一套运行时,换来的是几十篇文档的检索——这个交换不划算。
写路径:收窄到一个端点
有些东西确实不能编译。比如”今天钓到一条鱼,记一下”。
这件事的本质是运行时产生的状态,它不可能在构建期存在。所以架构里必然有一个写入口。问题是这个入口要多大。
我的答案是:一个端点族、一张表、一个图片桶、一道口令。没有业务层、没有服务层、没有仓储层。它只做三件事——收一个请求、验一道门、算一个派生值。
一个人的系统不需要分层架构,需要的是”这一层只干一件事”。
划分之后,代价在哪
诚实的说,代价有三处:
- 新写的文章要等一次构建才可见。 对我完全可以接受,因为构建只要一分多钟。
- 动态那部分数据有延迟。 我给它 5 分钟的缓存,也就是说记录完可能过几分钟才在图鉴上看到。这不是交易系统,没关系。
- 构建机和运行时是两套东西。 但如果构建期完全不碰数据库,这两套就永远不会互相拖累——文章发不出去,绝不会是因为数据库挂了。
第三条是这套设计里我最满意的一处。它不是一个”优化”,是一个结构性保证:把失败隔离开,比让它更快更重要。