Colophon
本站技术说明
既然整个站都在讲「为什么这么设计、代价是什么」,那它自己也该经得起同样的追问。下面的数字全部是构建产物的实测值,可以用浏览器开发者工具当场核对。
- 4.4 KB
- 首页 HTML
- 19.8 KB
- 最重页面的 JS 总量
- 8.6 KB
- 交互运行时
- 0
- 第三方脚本
brotli 后
含交互运行时
Preact,替代 49.6 KB 的 React
无统计、无字体 CDN、无外链
技术栈
- 框架
- Astro · Preact islands · Tailwind · TypeScript
- 图表
- 手写 SVG,无图表库
- 字体
- Source Serif 4 · Inter · 思源宋体子集 · JetBrains Mono,全部自托管
- 服务端
- Caddy · Docker Compose · Debian
- 部署
- GitHub Actions → 构建 → 脱敏门禁 → rsync 原子切换 → 健康检查 → 失败回滚
- 托管
- 阿里云轻量 2 核 / 893 MiB / 东京,域名在 Cloudflare(DNS-only)
决策与代价
每条都写了放弃的方案和付出的代价 —— 只写选了什么,说明不了判断力。
静态优先:内容是死的,交互才需要 JS
问题
这个站 95% 是文字与图表,只有链路图、时序回放、解码器三处需要交互。用一套 SPA 框架渲染全部内容,等于让读者为三个组件下载整个运行时。
备选方案
- 传统 SPA:一次性下发整个应用,首屏要等 JS 执行完
- 全静态 HTML:最快,但三个交互组件没法做
- Astro islands:整页静态 HTML,只有标注为交互的组件下发并各自独立激活
选择
Astro islands。页面以静态 HTML 交付,三个交互组件用 client:visible 声明,滚动到才加载。没滚到解码器那一节,它的代码就不会被下载。
代价
交互组件之间不能直接共享内存状态,跨组件通信要走事件或 URL。本站没有这类需求,代价为零。
把 React 换成 Preact,省下 41 KB
问题
三个交互组件加起来只有 6.5 KB,React 运行时却要 49.6 KB —— 运行时是自身代码的近八倍,比例不合理。
备选方案
- 保留 React:生态最大,但这个站一个生态特性都没用到
- 改用原生 Web Components:运行时为零,但要自己实现状态与差量更新
- Preact:API 兼容,三个组件只用了 useState / useEffect / useRef / useMemo
选择
Preact(compat 模式)。组件代码一行未改,运行时从 49.6 KB 降到 8.6 KB。
代价
少数依赖 React 内部实现的三方库不兼容。本站没有这类依赖;若将来需要,可以只把那一个组件切回 React。
图表全部手写 SVG,不引入图表库
问题
需要三种图:数据链路图、时序回放、状态机。现成方案里 React Flow 约 120 KB,Mermaid 约 500 KB。
备选方案
- React Flow:拖拽、缩放、自动布局都有 —— 但这三张图都是固定布局,一个都用不上
- Mermaid:写法简单,但输出静态图,无法逐步播放,也点不开单条消息看报文
- 手写 SVG:完全可控,但要自己算路径与布局
选择
手写 SVG。三个组件合计 6.5 KB,比单个图表库小一到两个数量级,且三张图共用同一套视觉语言。时序回放的逐步播放、点消息展开报文,用现成库反而做不到。
代价
节点坐标是手工排的,图变复杂时维护成本会上升。20 个节点以内手排比自动布局更好看,超过就该换方案了。
中文标题字体:按实际用字在构建期切子集
问题
思源宋体完整字库 25 MB。中文站想用衬线标题,这是首屏性能的头号杀手 —— 而正文若也用网络字体,代价还要再翻几倍。
备选方案
- 全用系统字体:零成本,但 Windows 上的中文衬线体观感很差
- 按 unicode-range 分片:浏览器按需取片,但一页仍可能触发十几个请求
- 构建期扫描产物,只把标题里实际出现的汉字打进子集
选择
正文走系统字体栈,零下载;标题在 astro build 之后扫描产物中 h1–h3 与显示级元素的实际用字,用 harfbuzz 生成子集,字重钉死输出静态实例。当前 609 字、104 KB,是全站唯一预加载的字体。
代价
新增页面若引入大量新字,子集会变大。构建脚本内置 160 KB 硬门禁,一旦扫描范围跑偏(比如误抓正文)直接构建失败。
压缩放在构建期,不放在请求时
问题
Caddy 官方镜像不含 brotli 编码器;即使自己编译进去,运行时压缩为了控制延迟也只能用中等级别。
备选方案
- 自编译 Caddy 加 brotli 插件:每次请求现压,CPU 换带宽
- 只用 gzip:镜像自带,但压缩率明显差一截
- 构建期用最高级别压好,服务端直接发压缩文件
选择
构建期生成 .br / .gz / .zst 三份,Caddy 按 Accept-Encoding 直接发对应文件。静态站的内容在构建时就定了,没有任何理由每次请求重压一遍。整体省 74%。
代价
构建时间增加几秒,产物文件数变成四倍。都不重要。
数据库选 SQLite,不选 Postgres
问题
站点要存的只有文章与访问统计,几万行量级、单写入者。服务器物理内存 893 MiB,已用约 466 MiB。
备选方案
- PostgreSQL:功能完整,但默认配置常驻 150–200 MiB,且要多维护一个容器与一套备份恢复流程
- SQLite:进程内库,常驻内存接近零,备份就是复制一个文件
选择
SQLite(WAL 模式)。这个负载下它的写入吞吐远超需要,省下的内存留给构建与突发流量。
代价
单机绑定,没法水平扩展,也不支持多进程并发写。这个站不会有第二台机器,所以代价不成立 —— 真需要时再迁移,schema 是通用 SQL。
静态资源不经过后端
问题
后端一旦崩溃或重启,整站是否跟着不可访问?对简历站来说,可用性优先于一切动态功能。
备选方案
- 全部请求走后端:路由统一,但后端挂了就白屏
- 反向代理只把 API 路径交给后端,静态文件由代理直接读磁盘返回
选择
Caddy 直接读文件返回静态内容,只有 /api 与后台路径转发给后端。后端挂掉时,页面照常打开,只是统计不再记录。
代价
后端无法对静态页面做鉴权或改写。本站不需要。
服务器:标称 1 GB,实际只有 701 MiB
问题
开机后 free 显示物理内存只有 701 MiB,比标称少了近三分之一。第一反应是被少配了。
备选方案
- 找服务商申诉:先确认是不是真的少给
- 自己查内存去了哪
选择
查出来是内核启动参数里的 crashkernel 预留:镜像的规则让 1 GB 机器落进「1–4 GB」档,为崩溃转储预留了 192 MiB。硬件层面 1 GB 一点没少。个人站点永远不会去分析崩溃转储,改成不预留后重启,可用内存回到 893 MiB。
代价
失去内核崩溃时自动保存转储的能力。对一台只跑静态站的机器,这个能力换 192 MiB 内存不划算。
源码
仓库暂为私有:其中包含简历原始数据。若你想看某一部分的实现, 发邮件告诉我是哪一块,我可以单独整理出来。