MOEWAH/ ARCHIVE
第26-002期

重做个人主页:建站的技术决策

MoeWah2026.08.21
重做个人主页:建站的技术决策

重做新主页的技术决策合集:静态优先与外部数据、性能的考虑顺序、Markdown 正文统一、代码复用与独立。每条带真实返工证据,写给同样用 Astro 建站的人。

背景

上一篇《重做个人主页:先回答放什么》我从设计出发,回答了“主页应该为什么而设计”。但设计定下来之后,真正麻烦的事情才开始:怎么把这些判断落成一个不会不断返工的站点?

这篇不讲 Astro API,也不列性能优化清单,只记录这次重做主页时真正做过、又推翻过的技术决策:架构怎么取舍,性能应该先看什么,Markdown 为什么要统一渲染,以及代码到底什么时候值得复用。

每一条都带着一次真实返工。因为对我来说,技术决策最有价值的部分,从来不是“最后用了什么”,而是为什么第一次会选错,以及下一次应该在什么地方停下来重新判断

四块的共通教训一句话:别让一个站点里出现两套实现,也别造没有第二个使用方的轮子。架构上表现为“运行时和构建期混着来”,性能上表现为“架构没先定,后面全是补丁”,正文上表现为“两条渲染管线、两张脸”,复用上表现为“同一段逻辑复制多处”。

一、架构:静态优先,动态数据按需

1. 能构建期做完的,不放运行时

两个问题一次定死:

  • 这个数据会变吗?多久变一次? 不会变或低频变的,构建期(网站打包生成时)抓取落盘;只有必须实时同步的才留一条动态接口。页面访问零后端请求,是任何后续优化都比不上的起点。
  • 必须在用户浏览器里算吗? 能静态渲染的不引入前端框架。没有框架就没有 Hydration(注水:框架在浏览器端复现页面状态并接管交互的过程),JS 开销从“一整个依赖树”降到“几十行原生脚本”。

静态站点不等于不能预取——Astro 的 prefetch(预取:悬停链接时提前拉目标页)在构建版本直接可用,给导航和首屏入口更高优先级即可。

2. 外部数据接入的五个决策

静态站想展示外部动态内容(动态流、评论),先问:读者能接受这些内容旧到什么时候? 答案决定架构。默认构建期(网站打包生成时)抓取,内容与构建同节奏;只有“刚发的内容必须立刻可见”时才加运行时(用户访问页面时)同步,且这一层必须可关闭——纯静态部署没有运行时能力时,关闭后退回构建期版本。

五个通用决策,顺序不该乱:

  1. 凭据永远走环境变量。地址写配置、密钥从环境注入;不配 token 退化为匿名拉取,未配置地址不发起请求。留空必须有降级态:渲染占位空态和“内容暂时无法加载”,不报错、不白屏。
  2. 给外部 API 钉版本锚点。托管在第三方服务,它就是你一直在用、却注意不到它存在的依赖。版本号写进配置,构建期校验;服务升级前先核对版本是否漂移。真实遇过一次:服务大版本升级、接口字段悄悄变掉,差一点带隐患上线——版本锚点把这类问题提前到构建期。
  3. 拉取失败给空态,不给假数据。兜底是“内容暂时无法加载”,不是把旧快照或编造内容填回去。空态告诉用户“数据没来”,假数据让用户以为数据来了——前者可重试,后者无法察觉。
  4. 拉取与渲染分层。契约层管接口(参数、字段映射、地址拼法、渲染),页面只管用结果;配置(地址、token、白名单)与逻辑分离。凡是“只和你相关”的留在配置侧,“所有人都要遵守的契约”收在逻辑侧。
  5. 增量同步做快照对比。构建期快照 + 运行时端点只同步新增/删除/修改的条目,前端刷新触发合并。判断标准一条:有没有真实场景要求“刚发布的内容马上可见”,没有就别加。

看下来会发现,这五个决策的底色是运维常年养成的习惯:密钥不进代码、依赖钉住版本、故障留空态不造假。处理外部依赖的思路,和处理一台服务器是同一套——先想清楚它会怎么坏,再让坏的时候有人知道。

二、性能:考虑顺序,不是技巧清单

1. 图片——主战场,返工最多

多数页面传输体积里图片占大头。三条通用做法,条条都返过工:

  • 懒加载先自研,两天后整体推翻。 起初 data-src + IntersectionObserver,还调了 500px 提前量。问题出在移动端 Safari/WebKit:无 src 的图片不按宽高预留空间,影集详情页锚点跳转时页面高度错误,图加载完把目标顶下去。最后全站改用原生 loading="lazy" 直接输出 src——支持原生懒加载的浏览器继续懒加载,图片天然占位不抖动。教训:懒加载方案不能牺牲布局稳定性。
  • 图片格式不是一次选定的。webp,同一天下午切 avif——体积约为 webp 的 7 折(切换时的实测记录)。avif 兼容要求较新(Chrome 85+)。做成配置开关:格式、质量集中一处,换格式不动模板,兼容性出事一键回退。
  • 列表缩略图和详情整图分开处理。 早期列表页渲染详情大图,一张 1200px 图缩在 600px 卡片里,传输浪费整一倍。组件加宽高参数,列表封面按实际渲染宽度压缩——体积从“源文件多大”出发改为“内容实际显示多大”。

优先级语义是“谁先被用户看到”,不是“谁体积大谁先下”:首屏 eager + fetchpriority="high",装饰大图 fetchpriority="low",原生属性即可。占位层用纯 CSS mask 图标——不白一块、不发请求,加载失败走全局错误委托统一隐藏破图。

2. JS——省,还要稳

  • 关键脚本内联进 <head>:主题切换必须首帧前生效,内联同步执行,避免闪屏和多余阻塞请求。
  • 滚动动效用一次观察:入场动画 IntersectionObserver,进入视口加类后立即 unobserve,不常驻监听。
  • head 里的脚本,DOM 就绪要显式处理:页面结构没拼完就执行脚本,会拿不到想要读的元素(直接读 document.body 可能是 null)。处理:判断页面就绪状态,没就绪就等就绪事件再启动。
  • 无 JS 时内容也必须可见:动画默认可见,只有 html.js 存在才先隐藏再播放——禁用 JS 的用户照常读全文,动画是渐进增强不是访问前提。
  • 统计脚本全部 async/defer,动态注入的第三方按需建标签。
  • 后台标签页停掉计时器:轮播这类定时任务监听 visibilitychange,隐藏就停、回来再开,不空转烧电。

3. 字体——请求与阻塞的取舍

一套字体文件常常比整页 CSS 还大。这块的决策顺序是踩坑推出来的:

  • 为低频功能引入重依赖前,先算账。 动态页曾为渲染 LaTeX 公式引 KaTeX,光字体就占全站 1.1M 加载量。公式只是偶发,这个账算不过来——移除 KaTeX。功能再顺手,架不住它让每个访客背 1.1M。
  • 字体三档策略,默认不加载:系统字体栈零请求;需要品牌感时开网络字体配 display=swap 防阻塞文本渲染;网络受限环境切镜像源。默认值选系统栈,是移动端性能换来的结论。
  • preconnect 要连两个域。开网络字体只预连样式表域、漏了字体文件域,预连接白做——一个连接一个域,别漏第二个。

4. 动效——先预载再切换

动效在设计阶段定边界:只保留入场一次触发、悬停变色/位移、切换过渡三类,全部 CSS transition。两条硬性原则:尊重 prefers-reduced-motion(用户系统声明减少动态效果时全部静止为静态图);切换类效果先预载再切换——背景轮播先探针预载下一张,加载成功才切换,失败进黑名单用当前图兜底,不闪白、不破相。

三、Markdown:让两种正文只有一种样子

1. 先选好管线

Astro 6.4 起,Markdown 渲染管线变成可插拔的 processor 架构:官方两个处理器——Sätteri(Rust 原生编译,构建更快,有自己的插件接口)和 Unified(老生态,继续支持 remark 这套插件体系)。Sätteri 的插件接口和 remark 插件不互通,老项目升级时迁移不是换个包名那么轻。建议先盘点自己的插件清单再选。

2. 样式长在“文档体”上

两条内容源(动态、周刊)此前各写各的样式,动态组件里堆了 94 行样式圆场。统一办法:都挂同一个正文作用域类,组件内样式全部删除,一组样式两端共用。轻叙事内容保留独立子集(明确不含代码块和表格)。判断标准:谁渲染不重要,长什么样统一——以后加第三种内容源样式天然跟着走。

3. 高亮引擎统一,靠共享主题变量

动态页原先 hljs 客户端高亮(异步上色会闪、两套配色),统一到 Shiki 后关键动作是让动态侧也使用与内置 Shiki 相同的主题变量(--astro-code- 前缀),明暗主题自动跟随全站 token。两处渲染可以走不同入口,但必须引用同一套高亮变量。语言懒加载、未知语言兜底 plaintext,任何内容都不会让构建崩。

4. 动态内容的安全默认值,别信渲染库默认

渲染库(负责把 Markdown 变 HTML 的库)升级时移除了一个安全开关:以前会自动把特殊字符转成安全形式,新版默认把作者手写的原始 HTML 原样放行。动态内容托管在第三方服务,服务一旦被攻破,可以向你的站注入可执行代码。处理:接管渲染器的输出,把作者手写的原始 HTML 转义成纯文本。动态内容的渲染默认不信任

5. 横切能力构建期收敛

外链 rel/target、正文图片说明这类横切能力,能构建期做的就不留给运行时:静态内容由构建期插件处理(生成 HTML 时顺手补 rel、给有描述的图片包说明层),产物静态化、用户侧零开销;运行时只兜动态 UGC。判断标准:静态内容不依赖浏览器执行补丁

6. 格式清单里的两个易漏决策

标题层级、强调、引用、分隔线、行内/块级代码、列表、任务列表、表格、删除线、mark、kbd、图片宽度约束的全覆盖清单之外,两个易漏:表格窄屏撑破布局display: block + overflow-x: auto,横向滚动而非挤压换行;表头等宽字体 + nowrap);行内长代码撑破卡片overflow-wrap: anywhere)。

四、复用与独立:什么时候抽,什么时候不抽

三幕剧:一个自研轮子的失败

图片懒加载的自研方案(技术细节和回退原因在性能节讲过)换个角度看还是复用失败的样本:方案上线后,三天里被七个组件各初始化一遍,七份初始化代码各写各的;最后整体回退原生、模块删除时,七处跟着一起删。抽象的教训有两层:平台已有语义时先证明自研方案的必要性——原生属性在布局稳定性上有浏览器替你兜底;轮子一旦造了,每个使用点都是一份责任——七处复制比一处原生属性难维护得多。

正例一:规则放共享工具,组件只管调度

外链处理第一版就分三层:规则(单一函数)、配置(域名/链接定性)、调度(组件执行)。动态正文需要同样处理时直接调用同一个函数,规则只此一份。能不能复用,取决于第一版有没有把“规则”和“调度”分开——把规则埋在组件里,第二处用到时就得复制或反向重构。

正例二:同一意图出现第二次,抽公共件

分页两端共用、正文样式收进一个作用域、板块编号由单一工具生成且随开关自动顺延。同一意图出现第二次先抽再复制;只出现一次的写在哪算哪。复制第三次再抽就晚了,两处实现已经长成两棵不一样的树。

独立性:功能能拆装,数据能演进

  • 功能独立,行为联动:各板块有启用开关,关闭后导航、编号、RSS、元数据全部收敛,不留死链。
  • 职责分层,数据可演进:配置只存数据、类型外置、文案归 i18n、行为进共享工具——一次重命名牵扯十几个文件却零功能变化,靠分层的承受力。
  • 单一事实源:被多个页面用到的标识只能一处定义,页面引用变量不写死。

集权与自治

全局行为一个归集点(布局层承载字体、主题、全局脚本、结构化数据,所有页面走同一入口),板块各自独立开关独立数据。集权管协议,自治管内容

三个判断标准

  1. 同一意图第二次出现,才抽公共件;只有一个使用方的,内联。
  2. 规则与调度分层,纯逻辑进共享工具——能不能复用,取决于第一版怎么放。
  3. 平台有成熟语义的(懒加载、图片格式),默认用平台的,自研要证明必要性。

最后

技术决策合到一个主题里看,共性比差异更明显:先定边界(架构/顺序/作用域/抽件时机),再填细节。边界定对了,细节大多有平台原生方案可以抄;边界定错了,后面全是返工——而返工换来的这些决策,就是这篇的每个段落。上篇讲完设计,这篇讲完技术,这次重做主页我还有两块经验想与大家分享——《静态站的 SEO 基建》《把规则写给 AI 看》——分别单独成篇。

# Astro# 架构# 性能# Markdown# 代码复用

RSS / 周刊订阅

在阅读器中接收新期刊

打开订阅
CORR

评论

评论加载中…

POWERED BY ARTALK
NOTICE / 声明本人不会主动邀请或联系任何人,任何冒用本人名义的一切事物,请务必谨防受骗!
END OF FILE · 卷终