2026-10-05
维护记录
完成的工作
- 删除博客底部右侧的
powered by hugo与hugo-paper两个外链,保留左侧© {{ 年份 }} {{ 站点标题 }}版权段 ✅- 新增覆写文件
layouts/partials/footer.html,照搬 vendor 主题themes/hugo-paper/layouts/partials/footer.html的结构,仅去掉右侧两个<a>标签 - 顶部加注释说明:与
themes/hugo-paper/layouts/partials/footer.html同步、只尾部两个<a>不同,下次主题 bump 时按此对比 diff - 走覆写模式(与
partials/bg.html/partials/header.html/_default/baseof.html同套路),vendor-in 的themes/hugo-paper/不动,避免下次 sync hugo-paper 时冲突
- 新增覆写文件
- 验证
hugo --minify通过(265 页)、deno task test通过(23/23)、deno task validate-posts通过 - 构建产物
public/中新生成的页面 footer 正确收敛为:1<footer 2 class="mx-auto flex h-[4.5rem] max-w-(--w) items-center px-8 text-xs tracking-wider uppercase opacity-60" 3> 4 <div class="mr-auto">© 2026, sdttttt</div> 5</footer>
遇到的问题
- 调试时
which deno找不到 —— 本机 mise 装了deno 2.9.7但 cwd 不在用 deno 的项目根,shell 没自动激活。直接用绝对路径~/.local/share/mise/installs/deno/2.9.7/bin/deno跑即可 ⚠️ - 下次可直接mise exec -- deno task test public/里能看到一些旧 slug 文件(20240615-immortalwrt的编译踩坑-vc6/、20200402-只需要服务中心-b4y/等)渲染成 PaperMod 风格 footer —— 这是上一次部署留下的旧产物(rename-posts.ts每次重生成随机 4 位 hash),新构建会用新 slugt2s/6v0,CI 下次 deploy 整树覆盖后即消失
下次建议
- vendor-in 主题 + override 模式现已稳定(4 个覆写:
_default/baseof.html、partials/bg.html、partials/header.html、partials/footer.html),下次 sync hugo-paper 时记得逐一对照 diff - 如未来想完全自定义 footer 文案(比如加 RSS / ICP 备案号),直接改
layouts/partials/footer.html即可;版权段目前读site.Copyright,未配置所以走默认© {{ now.Year }} {{ site.Title }}
维护记录(续)
完成的工作
- 清理
static/bg/下的 3 张 JPG(原图为抠图前的备份,按 claudelog 14 续计划"如果不再需要回滚可手动删除")✅- 删除:
F_tZfZBbwAALItU.jpg(256KB) +FxC7q1YaUAE8SOL.jpg(197KB) +GiG69PDa8AAvoeS.jpg(127KB),共 ~580KB - 模板
layouts/partials/bg.html没做 basename 去重,同名 JPG/PNG 会被全部加载,所以这次清理同时节省仓库体积和访客下载量 hugo --minify重新构建,public/index.html现在只有 3 个.page-bg__imgdiv(之前是 6 个,jpg+png 各一份)
- 删除:
- 顺手验证:仓库内其它位置无
.jpg引用;.DS_Store在.gitignore里
遇到的问题
- claudelog 14 续写过 “按 basename 优先 PNG、忽略同名 JPG” 的 baseof 重构,但读
layouts/partials/bg.html实际没有这个过滤逻辑(readDir直接 append 所有 jpg/png)⚠️ - 当时要么 claudelog 描述超前于代码改动,要么被回滚过 - 这次没去补这个去重逻辑 —— 因为直接删 JPG 已经解决了"多倍加载"问题(一个 div 一份 URL);如果以后再加新图,要么用纯 PNG,要么重新审视这个模板
下次建议
- 当前
static/bg/留下.DS_Store(gitignored,不会进仓库),3 张 PNG 各 ~200KB。如果以后想再瘦身,可以用sharp/oxipng对 PNG 做无损压缩,预计能再省 20-30% - claudelog 14 续描述的 baseof 重构可能从未落地;如果未来想严格按"按 basename 去重"的语义加回来,注意当前"删 JPG"已经让模板自然只剩 3 张图,加上去重后行为不变
维护记录(再续)
完成的工作
- 修复
layouts/partials/bg.html模板的后缀白名单- 根因:Go template 第 27 行用
(in (slice ".jpg" ".jpeg" ".png" ".webp" ".gif" ".svg") (path.Ext .Name)),把 JPG/PNG 都接受,static/bg/ 里同名.jpg/.png都会被加入$bgImages,最终页面同时下载 2 倍体积 - 修复:改为
(eq (path.Ext .Name) ".png"),只接受.png后缀 - 位置:
layouts/partials/bg.html:27(readDir 分支) - 防御性收益:以后误传 JPG 到
static/bg/也不会被加载 hugo --minify重建验证:仍是 3 张 png,渲染正确
- 根因:Go template 第 27 行用
遇到的问题
- claudelog 14 续里的描述是"按 basename 优先 PNG、忽略同名 JPG",但代码里没体现。这次不按"basename 去重"的方案做(更复杂),而是直接把白名单收敛到
.png—— 因为 static/bg/ 事实只使用 PNG,白名单语义最清晰;要支持别的格式再去改也方便 ⚠️
下次建议
- 如果以后真的需要支持多种格式(webp/svg/jpg),可以加一个 helper:
basename → format priority,既保留多格式又避免双倍加载
维护记录(三续)
完成的工作
AGENTS.md「提交与 PR 规范」节追加一条规范:推送前必须先检查远端是否有新提交 ✅- 触发原因:今天这次 push 遇到了 rebase 冲突 + 需要
--force-with-lease才能推上去 —— 根因是本地 commit876248f完成后 CI bot 又推了b71c015 chore(format): prettier markdown,远端在我推上之前已经领先一步 - 新规范内容:
- 推送前必跑:
git fetch origin && git log HEAD..origin/master --oneline - 看到新提交(常见来源:CI bot 的
chore(format)格式化了你刚改的 markdown)必须先 rebase / merge 解决冲突再 push - 避免与远端历史分叉;
--force-with-lease仍是 rebase 后的合规选项,但能避免就避免
- 推送前必跑:
- 触发原因:今天这次 push 遇到了 rebase 冲突 + 需要
下次建议
- 如果 CI race 频繁发生(如 deploy.yml 的
Commit auto-fix results步骤在 push 完成后又产生新 commit),可以考虑:- 给 auto-fix 脚本加
git fetch origin --quiet && git rev-parse HEAD检查:如果本地 HEAD 跟 fetch 后的 origin/master HEAD 不一致,说明已有人推送,则跳过 auto-fix commit - 或者把
Commit auto-fix results步骤拆到 deploy job 之后单独跑,避免跟用户 push 抢顺序
- 给 auto-fix 脚本加
- 当前
git-commit-push.ts的pull --rebase已经会捕获到远端领先,但没有"在 add -A 之前先 fetch 检查"的逻辑 —— 如果有上游 race 担忧,可以在脚本里加一步 dry-rungit fetch+ 比对HEAD与origin/master,有差异时先 dry-run 报错而不是直接 add + commit
维护记录(四续)
完成的工作
- 主题架构重构:layouts/ override → themes/sdttttt-paper/ 子主题(从方案 A/B/C 中选 C)✅
- 背景:此前用
layouts/覆写 + vendor-inthemes/hugo-paper/两层结构,维护时需要同步跟踪两个目录;想统一到"自己的主题" - 新结构:
themes/hugo-paper/— 父主题,原样 vendor-inthemes/sdttttt-paper/— 子主题,通过 Hugo[parent] name = "Paper"继承父主题,5 个 override 文件 + 1 个theme.toml+ 1 个README.mdgo.mod— 新增,Hugo modules 入口(module github.com/sdttttt/sdttttt.github.io)hugo.toml—theme = "sdttttt-paper"+ 新增[module.imports]块让父主题作为 module 加载- 旧的
layouts/_default/baseof.html/layouts/partials/{bg,header,footer}.html/assets/custom.css全部git mv到themes/sdttttt-paper/(git 识别为 R,保留 blame)
- 每个 override 文件头注释已更新:明确说明这是 sdttttt-paper 的文件、原件在父主题哪、sync 上游时如何 reapply diff
themes/sdttttt-paper/README.md新建:说明基于什么、override 清单、同步上游两步指南- 删除空的
layouts/和assets/目录(里头仅.DS_Store,已被 .gitignore) - AGENTS.md「项目结构」节 同步重写:描述新两层主题布局、go.mod 入口、主题同步两步走
- 背景:此前用
遇到的问题
[parent]block 在 directory-based 主题下不生效:起初sdttttt-paper/theme.toml写了[parent] name = "hugo-paper"(按目录名猜),但 [parent] 在 Hugo directory-based 加载路径下完全被忽略——构建结果只有 14 页(仅 sdttttt-paper 自带那 4 个 layout 有效页面),其余页面 fallback 到 0 layout,3 个 WARN(taxonomy / term / search 没 layout)⚠️[parent] name实际匹配的是父主题 theme.toml 的name字段而不是目录名:父主题themes/hugo-paper/theme.toml里是name = "Paper",不是hugo-paper。改成[parent] name = "Paper"后仍然不行——说明 [parent] 这个字段本身在 directory-based 加载路径里就是失效的- 主题继承要求 Hugo modules,不允许 directory-based 加载:查 Hugo 文档后发现
theme = "X"是 directory-based 加载、不参与组件继承;要让 sdttttt-paper override hugo-paper 的 layout,必须双方都作为 module 加载 - Hugo modules 加载
themes/hugo-paper的 path 写法踩坑:path = "themes/hugo-paper"→ Hugo 拼成themes/themes/hugo-paper(错)path = "./themes/hugo-paper"→ 同上(错)path = "hugo-paper"→ ✅ Hugo 按短名从 themesDir 找,识别为 module- 实际成功的配置:
1[[module.imports]] 2 path = "sdttttt-paper" 3[[module.imports]] 4 path = "hugo-paper"
- 构建验证:266 页 / 测试 23/23 / validate-posts 通过;footer / bg / 默认亮色 三个 override 都正确生效
下次建议
- go.mod 与 Hugo modules 体系的引入是这次迁移的最大认知成本。AGENTS.md 已说明 go.mod 是 Hugo modules 入口、不是真的依赖 Go 工具链,但要小心 CI 上 Hugo 版本是否 >= 0.93(项目锁在 0.161.1,无问题)
- 以后 sync 上游 hugo-paper 时会面临两层 diff:父主题
themes/hugo-paper/直接 patch;子主题themes/sdttttt-paper/的 override 文件按每个文件头注释的指引 reapply diff。建议在 sync 前先git diff themes/hugo-paper/ /tmp/hp-clone/看清楚上游改动面再动手 - 可以考虑:在 sync 上游脚本(未来如果有)中自动检查
themes/sdttttt-paper/的每个 override 文件是否需要重同步——比如基于 “diff themes/hugo-paper/X vs /tmp/hp-clone/X,如果上游 X 有变且 sdttttt-paper 也存在同名 override,则报警” - 后续可能简化:如果 sync 上游的成本大于 override 的价值(override 变化频繁),可以考虑拆分成多个更小的子主题(一个子主题负责一个 override 关注点),但现在 4 个 override 不复杂,没必要拆
维护记录(五续)—— 审计与修正
对四续的迁移做了事后审计,发现并修正了几个问题。
完成的工作
- 修正“四续”中对主题继承机制的错误描述 ⚠️→✅
- 四续写得「通过 Hugo
[parent]block 继承父主题」并引入go.mod+[module.imports];审计实测后发现:[parent]block 在目录式主题下完全冗余(分别注释掉 → 仍 266 页、override 仍生效)go.mod冗余(删除后干净缓存构建仍正常)[module.imports]虽能工作,但并非必要——真正的最小机制是theme列表:Hugo 按列表顺序查找 layout/asset,第一个主题的同名文件覆盖第二个;单个1theme = ["sdttttt-paper", "hugo-paper"]theme = "sdttttt-paper"(无列表)才会只剩 15 页 —— 真正必需的是“两个主题都在列表里”。
- 最终配置:只用 theme 列表;删除
go.mod;theme.toml去掉[parent]block;hugo.toml去掉[module]块(净减 4 个概念)
- 四续写得「通过 Hugo
- 重写根
README.md:“仓库结构”与“同步主题”两节仍写的是旧的layouts/override 结构(四续时漏改)—— 补上themes/sdttttt-paper/结构 + 两张同步清单(父主题 patch / 子主题 reapply) - 重写
themes/sdttttt-paper/README.md:去掉[parent]与“sub-theme via parent”说法,改为 theme 列表机制说明 + 顺序不能颠倒的警告 - 修正
AGENTS.md三处机制描述(顶部 / 项目结构 / hugo.toml 条目) - 修正 claudelog 笔误:
git fetch < origin多余的<;比比→比对 - 补跑
deno task format-markdown:修正 3 个 markdown 不符合 prettier 的问题 - 验证:新配置 266 页、页面集与迁移前完全一致(无丢页);footer / bg / custom.css / 默认亮色 四个 override 全部生效;
deno task test/validate-posts通过
遇到的问题
- 审计中我自己的两个实验脚本都写错了,差点得出错误结论:
- V2 实验用
h[:h.find('theme = ')]截断 hugo.toml,把[params]/[taxonomies]等全部删了,导致构建出多余的categories/页,页数与正式配置不符 - 修正重测后才得出可靠结论(theme 列表可用)⚠️ - 教训:改配置文件做对照实验时永远不要字符串截断,要用
re.sub只替换目标行
- V2 实验用
- 页数一致性的验证方法:不能只比
hugo输出的 “Pages” 数字,要对比find . -name index.html的完整 URL 集合(diff <(...) <(...)),确认没丢页 - “推送前跑 format-markdown-check”这次没做:prettier 不在 PATH 时脚本报
spawn prettier ENOENT,我就直接跳过没排查 —— 实际 prettier 在~/.deno/bin/prettier(需配合 deno 在 PATH)。结果 CI 会多推一个chore(format)commit,正是我在三续里刚记录过的 race。下次 PATH 先设export PATH="$HOME/.local/share/mise/installs/deno/2.9.7/bin:$HOME/.deno/bin:$PATH"
下次建议
- 环境:本机跑 deno / prettier 任务前,先
export PATH="$HOME/.local/share/mise/installs/deno/2.9.7/bin:$HOME/.deno/bin:$PATH"(mise 的 deno + deno install 的 prettier wrapper);或用mise exec -- deno task ... - 推送前清单(AGENTS.md 已有,本次执行不完整):
test→validate-posts→format-markdown-check→git fetch origin && git log HEAD..origin/master --oneline,四步都不能省 - 改配置做对照实验的套路:
re.sub只改目标行;每次用干净HUGO_CACHEDIR;用 URL 集合 diff 而非页数计数 - theme 列表机制比 Hugo modules 简单很多;除非将来要发布到 themes.gohugo.io 或跨仓库引用,否则没必要重新引入 modules
维护记录(六续)—— 替换 favicon
完成的工作
- 把博客 favicon 换成 pi.dev 的 mark ✅
- 新增
static/favicon.ico(透明底 + 黑 mark,16/24/32/48/64 五个层) - 重写
static/favicon.svg(原样复制 pi.dev 的 SVG,带prefers-color-scheme自动黑白切换) - 重写
static/apple-touch-icon.png(180x180,白底 + 黑 mark) - 重写
static/mstile-150x150.png(150x150,白底 + 黑 mark) - 重写
static/safari-pinned-tab.svg(单色,去掉 media query —— Safari pinned tab 只把形状当 mask) hugo.toml [params]新增favicon = "favicon.svg"
- 新增
- 顺带修了一个潜在 bug:替换前
hugo.toml里根本没有favicon参数,而head.html默认取favicon.ico—— 所以线上实际用的是 hugo-paper 主题自带的 Hugo logo,那个自定义的“R”图标(static/favicon.svg)从来没被引用过(死文件)。现在显式设了favicon参数,并把static/favicon.ico也一并替换,彻底覆盖主题默认。
遇到的问题
qlmanage渲染 SVG→PNG 不可靠:它会命中 SVG 内的@media (prefers-color-scheme: dark)分支(本机当前是 dark),把 mark 渲染成#f6f6f6(近白色)叠在浅底上,几乎不可见 ⚠️- 改用 PIL 精确位图化:pi.dev 的 SVG 全部是轴对齐矩形多边形,直接用 PIL
ImageDraw.polygon在 8x 超采样后 LANCZOS 降采样,完全可控且无外部依赖 - 验证方法:因当前模型不能看图,改用像素采样 —— 按 560 坐标系重算三个 path 的覆盖区域,对 svg/ico/apple-touch/mstile 各采样 14 个点,全部命中(同时自检时发现自己第一次的期望区域写错了,path1 的第二个矩形是
280≤x≤420不是0≤x≤280)
下次建议
- ⚠️ 法律提醒:pi.dev 的 favicon 是它自己的品牌标识,把它原样用作本站 favicon 可能涉及商标/著作权问题。如果只是个人喜好且不涉及商业用途,风险不高,但要心里有数;若日后有人异议,替换回去只需 revert 这一个 commit
- 若以后要换回自设计图标,可直接用同一套 PIL 脚本生成各尺寸;脚本可固化到
scripts/(Python 而非 Deno,需额外确认依赖策略) head.html只引用favicon与appleTouchIcon两个;mstile-150x150.png与safari-pinned-tab.svg在本仓库中无人引用(PaperMod 时代遗留),但作为完整图标集仍然保留并同步更新了
维护记录(七续)—— favicon 改为自设计“R”
完成的工作
- 把 pi.dev 的 logo 换成自设计的字母“R”,风格与 pi.dev 保持一致,同时彻底解决六续提到的商标问题 ✅
- 沿用同一套网格:4×4 网格、cell = 140、viewBox
0 0 560 560 - 填充格(■ 实、□ 空):(左竖笔全高;首行满宽;右上 (3,1);中横 (1,2)(2,2);右下竖足 (2,3))
1■ ■ ■ ■ 2■ □ □ ■ 3■ ■ ■ □ 4■ □ ■ □ - 风格要素全部对齐 pi.dev:单色
#111111、直角矩形块、无曲线、方形 viewBox、<style>+.markclass +@media (prefers-color-scheme: dark)白变#f6f6f6 - SVG 用单条
<path class="mark">多子路径表达(与 pi.dev 同构):M0 0H140V560H0Z M140 0H560V140H140Z M420 140H560V280H420Z M140 280H420V420H140Z M280 420H420V560H280Z - 由同一份网格定义(
GRID/RECTS)同步生成 5 个资产:favicon.svg、favicon.ico(五层)、apple-touch-icon.png(180,白底)、mstile-150x150.png(150,白底)、safari-pinned-tab.svg(单色) - 脚本内置
assert cover == GRID自检:由RECTS反推覆盖矩阵与GRID逐格比对,避免手写矩形与设计意图不一致
- 沿用同一套网格:4×4 网格、cell = 140、viewBox
遇到的问题
- 无法目视预览(当前模型不支持看图),因此用两种方式兜底验证:
- 终端打印 ASCII 网格(■/□)让改动“可读”
- 按 560 坐标系对 16 个格子中心逐点采样,比对
fill()判定,三份位图均为 16/16 通过
- 之前
qlmanage渲染 SVG 会命中 dark 分支导致近白色 mark,本次仍用 PIL 位图化(同六续方案)⚠️
下次建议
- 若想微调字形(例如把竖足 (2,3) 换成斜向阶梅 (3,3)),只需改脚本里的
GRID矩阵,RECTS与 5 个资产会自动跟随(但得同步改RECTS,或写一个从GRID自动笛卡尔分解成矩形的函数) - 可考虑把这份生成脚本固化到
scripts/(Python 而非 Deno,需确认依赖策略:PIL 是 pip 包,CI 不一定有) - 现占位定义:
favicon = "favicon.svg"在hugo.toml [params];若以后想换回纯.ico,删掉这一行即可让head.html回退默认
维护记录(八续)—— R 的底脚右移一格
完成的工作
根据反馈微调字形:把底行(行3)的黑格从
列2右移到列3✅1之前 之后 2■■■■ ■■■■ 3■□□■ ■□□■ 4■■■□ ■■■□ 5■□■□ → ■□□■- 变更的矩形:
(280,420,420,560)→(420,420,560,560)(即 RECTS 最后一项(2C,3C,3C,VB)→(3C,3C,VB,VB)) - 新 path:
M0 0H140V560H0Z M140 0H560V140H140Z M420 140H560V280H420Z M140 280H420V420H140Z M420 420H560V560H420Z - 5 个资产(svg / ico / apple-touch / mstile / safari-pinned-tab)同步重生
- 构建 266 页正常;三份位图各 16/16 采样通过,并从 apple-touch 反向采样打印 ASCII 与实际设计逐格一致
- 变更的矩形:
遇到的问题
- 需求描述有歧义(“右下角那个格子白色往右移动一格”按字面会移出画布,因为右下角白格是
行3列3,已在最右);来回确认后按“行3列2 的黑块右移到列3”实现 ⚠️ - 下次改图时用「行X列Y 从■改□」这种无歧义描述更高效 - 右移后
(3,3)与主体仅对角相邻(邻接的(2,3)、(3,2)均为白),小尺寸下会看成“脱离的一格”;如果要更连贯,可改为保留(2,3)并补上(3,3)(底脚变两格宽贴右边缘)
下次建议
- 如果还是觉得孤立,推荐备选:
行3 = ■ □ ■ ■(保留原脚 + 补右下角),字形更稳 - 真的要把生成脚本固化进
scripts/的话,建议同时写一个从GRID自动分解矩形的函数(而不是手维RECTS),避免两处不同步
维护记录(九续)—— 粒子效果页 /particles/
完成的工作
- 用
static/bg/的三张 cutout 做了一个 HTML5 Canvas 粒子效果页/particles/(纯原生,无依赖)✅content/particles.md:页面(layout: particles)themes/sdttttt-paper/layouts/_default/particles.html:布局;readDir "static/bg"自动发现 PNG,把绝对 URL 通过<script type="application/json" id="pt-config">交给 JS(与partials/bg.html同一套发现规则)themes/sdttttt-paper/assets/js/particles.js:引擎(~280 行),Hugo Pipesminify | fingerprint加载themes/sdttttt-paper/assets/custom.css:追加.pt-stage/.pt-bar/ 按钮 / 暗色主题样式- 交互:鼠标推开粒子(半径 92px 厖力)、点击炸散、【换一张 / 重新聚合 / 炸散】按钮
- 工程细节:DPR 缩放、自适应 step 逼近目标粒子数(6000)、静止后停帧(sleep/wake)、IntersectionObserver 离屏暂停、
prefers-reduced-motion直接出成品、<link rel=preload as=image>预加载
遇到的问题
- 颜色分桶是性能的关键:先实测了三张图的颜色分布 —— 精确色仅 ~230 种,5bit 量化后 仅 ~47 桶。于是把粒子按颜色桶排序,每帧只需 ~50 次
fillStyle赋值而不是 ~5000 次。这也是“不需要 WebGL”的数据依据 - Hugo 把 JSON 在
<script>里双重编码了:{{ $cfg | jsonify }}在 script 上下文被 JS 转义,输出成"{...}"(字符串而不是对象),JSON.parse会得到 string,demo 直接不工作。修:| jsonify | safeJS⚠️ - 验证环境比想象中难得多:本机只有 Firefox,而
firefox --headless --screenshot在首次绘制就截图,早于外部 CSS / 异步图片加载 —— 所以 http 页面拍出来是未样式化的白页(用file://+ 内联资源对照实验证明)。而且当前模型不支持看图,只能靠像素统计“看”结果 - 最终验证用三重手段:
- Deno DOM stub 跑真实
particles.js:粒子数 64/帧(与期望一致)、fillStyle仅 2 色/帧(分桶生效)✅ - 自包含 HTML(内联 CSS + 真实 JS + data-URI 图片)+ 把 rAF 改成同步跑 400 帧 → 截图后能看到粒子聚合成人物轮廓(上半身大块 + 底部双腿,宽高比 0.79 vs 源图 0.73)
- SRI 校验:页面
integrity与实际内容 sha256 完全一致(排除被拦的可能)
- Deno DOM stub 跑真实
下次建议
- 去看一眼再说:
hugo server然后开/particles/。参数都在两个地方可调:particles.html里的targetCount(默认 6000)与particles.js顶部的TARGET、弹簧系数0.075、阻尼0.87、厖力半径92、粒子尺寸系数0.76 - 如果觉得粒子太密/太疏:改
targetCount;想更像“粒子”而非“像素块”:把0.76调小(如0.55)让粒子之间留缝 - 可选的下一步(待定):把这套引擎接进现有的
partials/bg.html,接替现在“滚到底显示静态图”的逻辑;或者只把某一张图放到首页 hero - ⚠️ 未验证项:真实浏览器里的动画观感(飞入节奏、鼠标手感)无法在无头环境评估,需要你自己跑一下感受
维护记录(十续)—— WASM 版粒子引擎(Rust 裸导出)
完成的工作
- 用 Rust 裸导出的 WASM 重写了粒子引擎,接管采样 + 物理 + 软件光栅化 ✅
wasm/particles/—— Rust crate(Cargo.toml+src/lib.rs,307 行)themes/sdttttt-paper/assets/wasm/particles.wasm—— 构建产物(22.7 KB),随仓库提交themes/sdttttt-paper/assets/js/particles-wasm.js—— 加载器 + 渲染循环themes/sdttttt-paper/assets/js/particles.js—— 原 Canvas 2D 引擎,保留为降级路径deno.json新增build-wasm任务;.gitignore忽略wasm/target/
- 架构:WASM 把「源图 RGBA」变成「每帧渲染好的 RGBA 帧缓冲」,JS 只做两件事:
- 一次性把源图写进线性内存 →
build() - 每帧
tick()→ctx.putImageData(frame, 0, 0)(每帧唯一一次画布调用)
- 一次性把源图写进线性内存 →
- 为什么这条路线让 WASM 有意义:Canvas 2D 版的瓶颈是「每帧 4 万次
fillRect」——那是 native 调用开销,JS 优化不动;WASM 版把它换成「往内存写像素 + 1 次putImageData」,把 4 万次跨界调用压成 1 次 - 裸导出接口(全 C-ABI,无 wasm-bindgen、无 JS glue):
alloc/dealloc/build/resize/tick/settle/fb_ptr/fb_len/fb_width/fb_height/particle_count/sample_step/particle_size - 降级与开关:
instantiateStreaming失败 / 不支持 WASM /?engine=js时,动态注入原 2D 引擎
遇到的问题
memory.grow会让 TypedArray 视图 detach:build()/resize()会重新分配,可能撑大内存 →memory.buffer换块 → 旧视图byteLength变 0,ImageData直接失效。解法:帧视图必须在 build/resize 之后重建,且tick()内零堆分配保证循环中不会 grow ⚠️instantiateStreaming要求application/wasm:实测 Hugo dev server 和产物确实给的是application/wasm(curl 验证过),但仍写了 fallback 到fetch().arrayBuffer()+instantiate,防其他宿主 MIME 不对- DPR 是软光栅化的成本放大器:帧缓冲 =
W*dpr × H*dpr × 4,DPR 2 时是 8.8MB(DPR 1 的 4 倍)。加了dprCap: 1.5限制 - 验证方法又踩了一遍无头浏览器的坑:Firefox
--headless --screenshot在首次绘制就截图,早于外部资源加载。解法:把 wasm 和源图像素都 base64 内联,用同步的new WebAssembly.Module()+new WebAssembly.Instance()跑完整条链 → 截图能拍到 - 拿不到浏览器 console:用「像素条编码」(每个数值 16bit,画成黑/白像素行)写到画布上,再截图用 PIL 解码 —— 拿到了真实耗时数据
实测数据(Firefox headless,DPR 1.5,画布 1275×1020)
| 项 | 耗时 |
|---|---|
WASM tick()(物理 + 全屏光栅化) | 0.160 ms/帧 |
putImageData()(新增成本) | 0.270 ms/帧 |
| 合计 | 0.430 ms/帧 = 16.7ms 预算的 2.6% |
对比参照:Deno 里测真图(800×1098、31,025 粒子、DPR2)的 tick() = 0.208 ms/帧;JS 版光物理就 0.178 ms/帧(还没算 4 万次 fillRect)。
注:浏览器基准用的是降采样测试图(2,811 粒子),但因为成本大头是「帧缓冲
fill(0)+putImageData的整块 memcpy」,两者都与粒子数无关,所以量级可信。
下次建议
- 在本机看一眼真实对比:
hugo server→/particles/(WASM) vs/particles/?engine=js(Canvas 2D)。如果真的想看帧率,开 DevTools 的 Performance 面板录一段 —— 我能给的是 CPU 侧数据,真实帧率还依赖合成/上屏,那是另一个环节 - 想再提性能的话,下一步是:① 用
putImageData的 7 参数版本只更新脏矩形;② 把弹簧物理搬进顶点着色器(WebGL 路径,CPU 每帧归零) - 改 Rust 源码后记得
deno task build-wasm(需cargo在 PATH;CI 不需要 Rust,因为 .wasm 成品已提交) - Rust 工具链本次顺手升级:1.91.1 → 1.99.0,rustup 1.28.2 → 1.29.1;
~/.cargo/config→config.toml(消 deprecation 警告,备份在config.toml.bak)
维护记录(十一续)—— 左下角徽标改用 WASM 粒子 + 双击切回 PNG
完成的工作
.page-bg从「静态 PNG」改为「WASM 粒子渲染」,双击可切回真实 PNG ✅partials/bg.html重写:DOM 变成.page-bg > .page-bg__box > (img.page-bg__png + canvas.page-bg__canvas),不再输出 N 个背景图 div- 新增
assets/js/page-bg.js:引擎驱动 + 滚动门控 + 双击切换 - 新增
assets/js/pt-wasm.js:共享的 wasm 加载器 assets/custom.css:.page-bg段落整体重写
- 共享加载器为什么共享 Module 而不共享 Instance:wasm 里的引擎是
static mut ENGINE单例,左下角徽标和/particles/大画布会在同一页共存,共用 Instance 会互相覆盖状态。所以pt-wasm.js只缓存编译结果,每个消费方各自new WebAssembly.Instance()—— 代码只编译一次,线性内存隔离 - 状态机(
<html>上的类名):.pt-bg-ready(WASM 就绪 → canvas 接管)/.pt-bg-photo(双击切回 PNG)/.at-bottom(滚到底揭示)。跨层切换用 CSS 交叉淡入,不靠 JS 改样式 - 降级路径很自然:WASM 加载 / 构建 / 建帧任何一步失败,JS 什么都不做,盒子就一直是那张 PNG(= 改造前的行为)。所以不需要额外写一套 2D 降级
- 徽标粒子数按面积自适应:
density: 26→ 目标粒子数 = 画布 CSS 面积 / 26,钳在 [600, 12000]。大画布用的 40000 在 320×448 的徽标上会让粒子细到看不出来 - Rust ABI 新增
fit_w/fit_h两个参数(适配边距):徽标传1/1让粒子恰好填满盒子,和<img object-fit:contain>的几何完全一致;大画面传0.8/0.86留呼吸空间(原来的硬编码值)。这样双击切换在两种渲染之间没有尺寸跳变
遇到的问题
- 验证时差点误判为 bug:量到粒子实际 bbox 的顶部留白是 35px,而按“整图不透明 bbox”推算应是 6.6px。查下去发现是期望值算错了 —— 之前那个
getbbox()是 alpha>0 的 bbox(含 alpha=1 的近乎透明像素),实际 alpha>128 的内容从y=49才开始;按 step=10 采样后映射到画布是y 37..661,与实测35..663完全吸合(差的 2px 正是粒子半边长)⚠️→✅ page-bg.js的 DOM stub 测试第一轮at-bottom为假:我的 stub 里documentElement漏了scrollHeight,导致top+innerHeight >= undefined-8永远为 false。补上后 ✅(代码本身没问题)instantiateStreaming的 MIME 依赖再次确认:Hugo dev server / 产物都给application/wasm,但仍保留arrayBuffer + compile的 fallbackpublic/里残留旧 hash 产物:连续两次 Hugo 构建到同一目录不会清理旧的 fingerprint 文件,验证时要换新目录(或用hugo --cleanDestinationDir)
下次建议
- 双击切换现在是开-关切换(再双击回到粒子),且切回时直接呈现已聚合的成品、不重播飞入动画(避免打扰)。想要重播就把
page-bg.js里drawSettled()改成reseed+wake - 徽标现在可点击(
pointer-events: auto只开在两个子层上)——它在左下角、又是fixed,宽屏下内容不会到那里,但窄屏上如果跟正文重叠可能会挡选择。真出问题就把--page-bg-w再调小或改回纯装饰 pt-wasm.js现在是“每个消费方一个 instance”,一页最多 2 个(徽标 + /particles/)。如果以后再加消费方,内存会线性涨(每个 instance 一份帧缓冲),必要时改成“一个 instance + 多引擎句柄”的 ABI
维护记录(十二续)—— 修正密度参数:总数驱动 → 间距驱动
完成的工作
把 WASM
build()的密度参数从「粒子总数 target」换成「网格间距 pitch」 ✅- 原因:用户反馈「粒子效果有点粗」。查下来是我的参数设计错了:徽标的粒子数是按「画布 CSS 面积 / 26」算的,320×448 只分到 ~5,000 颗 → step=10、间距 4 CSS px(320px 宽只有 80 颗横排),确实粗
- 根本问题:用总数当输入与画布尺寸耦合 —— 同样 5 千颗,放小画布上就粗、放大画布上就细
- 新参数:
pitch(设备像素的网格间距)+max_particles(安全阀)。step = round(pitch / scale),所以实际粒子边长 ≈pitch × ratio,与画布/图片尺寸解耦 resize(cw, ch, pitch)也一并改签名,因为 devicePixelRatio 可能随显示器变化
实测改善(同一份
pitchCss: 2):改造前 现在 徽标 step 10 5 徽标粒子数 4,956 19,849 徽标间距 4.00 CSS px 2.00 CSS px 大画布 step 4 / 31,025 / 2.13px 不变 徽标 tick() — 0.351 ms/帧 大画布 tick() — 0.548 ms/帧 两处视觉密度现在一致(2.00 vs 2.13 CSS px)——这就是间距驱动的意义。
布局配置:
targetCount/density全部换成pitchCss: 2+maxParticles(徽标 60000 / 大画布 150000);小屏自动把pitchCss× 1.5(粒子数降到 ~44%)移除 Engine 里已无用的
pitch字段(resize改成每次传入,避免 dpr 变化时用旧值)
遇到的问题
- 整数像素块让
sizeRatio在小尺寸下失效:徽标上step*scale = 3设备 px,3 × 0.85 = 2.55四舍五入回 3 → 粒子边长 == 间距,缝隙归零(退化成无缝隙拼块而非“粒子”)。但这恰好符合用户先前“更清晰”的偏好(他当时主动让我把sizeRatio从 0.7 提到 0.85),所以保留round不动,只把间距减半 ⚠️- 若以后想要明显的颗粒感,把
particle_size()里的round改成floor即可(保证 size < spacing)
- 若以后想要明显的颗粒感,把
- 本地伺服构建产物时图片跨域:用
python -m http.server伺服public/时,页面里的图片指向sdttttt.online,跨域会让getImageData报 SecurityError → 徽标静默降级成 PNG。用hugo server(URL 会被改写成 localhost 同源)或看生产环境都正常 ✅
下次建议
- 调视觉密度只改一个数:
pitchCss(现在 2)。调小 = 更细、粒子更多,但要注意 tick() 与maxParticles - 想看当前实际参数,可在 DevTools 里读:
wasm侧全在assets/js/page-bg.js/particles-wasm.js的PITCH_CSS;Rust 侧有sample_step()/particle_size()/particle_count()三个诊断导出(尚未接到 JS,需要时可加到 config 里输出到 console)
维护记录(十三续)—— 修 bug:双击切不回原图
完成的工作
- 修复“双击无效”:监听器从
.page-bg__box改挂到document+ 矩形坐标判定 ✅- 根因:
.page-bg是z-index: -1(刻意压在正文之下,避免遮挡阅读),而负 z-index 会让元素完全收不到指针事件 —— 隔离实验:document.elementFromPoint(徽标中心)返回MAIN(正文),而不是徽标。所以我挂在box上的dblclick从未被触发过 - 修法:监听
document的dblclick,再用box.getBoundingClientRect()判断坐标是否落在徽标内(额外要求at-bottom,避免误触看不见的角落) - 同时清掉 CSS 里已失效的
cursor: pointer/cursor: default,并把pointer-events的注释改成实际语义
- 根因:
- 验证方式:隔离页面复刻真实堆叠(
z-index:-1装饰层 +height:300vh的<main>盖在上面),用合成MouseEvent('dblclick')派发到elementFromPoint命中的元素上,再把结果编码成像素条截图解码:检查 结果 徽标中心命中 MAIN(确认收不到事件)监听器触发次数 2(区域内 2 次 / 区域外被矩形判断挡掉)✅ 第 1 次双击 → 切原图 ✅ 第 2 次双击 → 切回粒子 ✅ 区域外双击被忽略 ✅
遇到的问题
- 第一版修复验证脚本自己写错了:把
toggled数组的下标混用(监听器内写[0]、外部写[1..3]),导致解码出的 3 个状态全是 1,误以为修复失败。换成“直接统计监听器实际触发次数hits”的信号量式写法后才拿到干净的结论 ⚠️→✅ - 用 iframe 测真实页面没成功:截图仍卡在
load事件,而我等pt-bg-ready最多 8s,慢资源只拖了 6s。结论:无头 Firefox 的--screenshot不适合这类“需要等异步完成”的验证;改用同步的隔离最小页面验证具体行为更可靠 - 本地用 python 伺服
public/时图片跨域(-b指定域名可绕过):再次踩到,已在十二续记录
下次建议
- 双击交互现在没有视觉提示:徽标收不到 hover,所以
cursor: pointer/title都无效。若想要反馈,可在document上挂节流的mousemove,指针落入徽标矩形时临时给<body>加cursor: pointer(cursor可继承)。没加是因为每个页面都常驻mousemove监听不划算 - ⚠️ 同一类陷阱:以后任何需要交互的元素,都不能放在
z-index: -1的容器里。要么把它提到z-index: 0以上(会盖住正文),要么像本次一样把事件挂到document上做区域判定
维护记录(十四续)—— WASM 产物同步策略 + 权限修正
完成的工作
- 决定了 WASM 构建不放进 CI(与用户确认)✅
- 结论依据:①
deploy.yml没有路径过滤,加 Rust 会让每次改文章都多付一遍 toolchain 成本 + 多一个失败点;② 全新 clone 不装 Rust 就不能完整预览(resources.Get拿 nil);③ 本仓库已有 vendor-in 并提交构建产物的惯例(themes/hugo-paper/) - 替代做法:保留提交产物,把「改过
wasm/**必须重建」写进AGENTS.md的推送前清单(这是 Agent 真正会读的位置,比写在编码规范里有效) - 考虑过但没做:加一个不依赖 Rust 的「源码哈希 sidecar + CI 比对」workflow(能拦住漂移,但要多一个 Deno 脚本 + 测试 + workflow,对单人博客维护面大于收益)。若以后
wasm/有多个 crate 或开始多人协作,再上不迟
- 结论依据:①
- 修掉
particles.wasm的可执行位:之前是100755(因为cp从 cargo target 目录带过来了权限),现改为644;同时在deno.json的build-wasm末尾加&& chmod 644 ...,否则每次重建都会回到 755
下次建议
- 如果哪天开始多人协作,或
wasm/下出现第二个 crate,再考虑上「源码哈希校验」那个方案(比重新编译对比二进制可靠:跨平台/跨 rustc 的 wasm 产物不保证逐字节一致,硬比会假报警)
维护记录(十五续)—— 收尾:警告/动画/颗粒感 + demo 页不公开
完成的工作
- 修
LanguageCode弃用警告 ✅ ——themes/sdttttt-paper/layouts/_default/baseof.html里site.LanguageCode→site.Language.Locale。现在构建零警告- 注:上游
themes/hugo-paper/layouts/_default/baseof.html里同样有一处,但我们的 override 优先,且 vendor 目录不动
- 注:上游
- 修 resize 会重播飞入动画 ✅ ——
particles-wasm.js的 resize 分支原本调wake(),而mod.resize()内部是重建(粒子位置重新随机)→ 每次拖窗口都重演一遍聚合动画。改为新增的drawSettled()(与page-bg.js一致),并把 reduce-motion 分支也改成复用它 - 新增
grain开关(Rust ABI 加floor_size: u32) ✅ —— 让小尺寸渲染能保留颗粒感- 背景:整数像素块 +
round会在尺寸小时把sizeRatio吃掉 —— 徽标上3 × 0.85 = 2.55 → 3,于是边长 == 间距、缝隙为 0,成品退化成无缝拼块 floor_size != 0时用floor,保证size < spacing- 实测:徽标 floor → 缝隙 0.67 CSS px(有颗粒感);大画布仍 round → 缝隙 0.13 CSS px(保持你之前调好的手感,不动);对照(徽标 round)→ 缝隙 0.00
max_particles/floor_size都存在 Engine 里,resize复用
- 背景:整数像素块 +
/particles/改为不外发的 demo ✅ ——content/particles.md加draft: true- 生产构建 267 → 266 页,
public/particles/不存在 - 本地用
hugo server -D仍可预览(AGENTS.md 里本来就写着这条命令) - 排除
private字段作为方案:validate-posts只校验它是布尔值,没有任何地方消费它(遗留字段),用它达不到效果 - 副作用(好的):
particles.js(2D 降级引擎)只在 particles.html 里被引用,而该页不发布 → 线上不会多出这个资源;pt-wasm.js/page-bg.js/particles.wasm仍因bg.html被发布
- 生产构建 267 → 266 页,
- 同步更新
AGENTS.md/README.md,标注 demo 页不公开
下次建议
- 如果以后想同时看两个 demo,可在
hugo server -D下访问/particles/,但注意它会和左下角徽标同时跑两个 WASM 实例(内存 ~19MB、两个 rAF 循环)。真要常看,建议在 particles 页跳过徽标 particles.js(降级引擎)仍用「粒子总数targetCount」而 WASM 已改「间距pitchCss」,现在两者恰好都落 step 4 是巧合。因为该页已不发布,优先级降到最低
维护记录(十六续)—— 显式 WASM 能力检测 + 降级可观测
完成的工作
pt-wasm.js新增supported显式能力检测 ✅ —— 不再只看window.WebAssembly存不存在(有些环境对象在但缺compile/instantiate),而是同时验compile与instantiate两个方法compile()裡包一层Promise.resolve().then(),把同步抛错(如某环境没有fetch)统一变成 rejection,调用方只需.catch,不会冒泡成未捕获错误
page-bg.js在不支持时连 wasm 都不去拉 ✅ —— 先if (!window.ptWasm.supported) { mark('unsupported'); return; }再谈实例化,避免老浏览器白拉一个 23KB 的二进制- 降级路径可观测 ✅ —— 状态写到
html[data-pt-engine]:unsupported/error/wasm/png。之前是「静默失败」,黑盒上看不出为什么没出粒子 particles-wasm.js(demo 页) 同步用supported做早退,直接走纯 Canvas 2D 降级
遇到的问题
- 第一版验证脚本自己写错两次:探针 canvas 放在
right:0,却按x=0..39解码,读到的是页面底色(全 0)→ 差点误判成「能力检测失效」。改成left:0后正常 ⚠️→✅ - 如何同步观察到异步失败:
Promise.reject()的 microtask 会在下一个<script>执行前清空,所以只要 stub 成同步 reject,就能在同一次截图里拿到结果,不用慢资源拖load
验证(真实浏览器,Firefox headless,像素条解码)
| 场景 | 去拉 wasm | pt-bg-ready | data-pt-engine | PNG opacity | canvas opacity |
|---|---|---|---|---|---|
不支持 WASM(WebAssembly = undefined) | 否 ✅ | 否 ✅ | unsupported ✅ | 1 ✅ | 0 ✅ |
| 支持但加载失败(stub 成 reject) | — | 否 ✅ | error ✅ | 1 ✅ | 0 ✅ |
下次建议
- 排查「为什么没出粒子」:DevTools 里看
document.documentElement.dataset.ptEngine,再看 Network 里有没有.wasm请求 - 还有一个未覆盖的场景:
WebAssembly存在但new WebAssembly.Instance()在运行时抛(如 CSP 缺'wasm-unsafe-eval')—— 走的是同一个.catch分支,行为一致;本仓库目前没有设 CSP
维护记录(十七续)—— 修“首次加载左下角闪一下”
完成的工作
- 根因:我把内联脚本换成了
defer外部脚本 ✅- 原实现在
bg.html里紧跟标记放了一段内联<script>,解析时立即执行 → 首屏前就加上了.js - 我改成
pt-wasm.js+page-bg.js(都是defer)后,在它们执行之前html:not(.js)会匹配,而它是“无 JS 回退”的 CSS 开关 → 徽标先以opacity:.85显形,随后脚本加上.js→ 规则失效 → 徽标再淡出。这就是“闪一下” - 修法:在
baseof.html的<body>开头加回一小段同步内联脚本(只加类 + 一个兜底定时器,见下)。放在 body 开头就够:徽标标记在 body 末尾,解析到那里时.js已就位,所以它从未存在“以无 JS 状态被绘制”的时机
- 原实现在
- 顺手消掉第二个闪动源:PNG 不再作为中间态 ✅
- 旧 CSS 是“PNG 默认可见,WASM 就绪后再交叉淡入 canvas” → 短页面(加载时已在底部)会先看到 PNG 再变成粒子
- 现在两层默认都
opacity: 0,由四条规则各自激活一层;并新增html[data-pt-engine="unsupported"|"error"|"png"] .page-bg__png { opacity: 1 }作为降级显示条件
- 补 Fix-2 引入的空洞:脚本根本没跑起来怎么办 ✅
- 两层默认不可见后,如果 JS 开着但
page-bg.js没加载(被拦 / SRI 校验失败),data-pt-engine永远为空 → 徽标彻底消失 - 兜底:baseof 的内联脚本里加一个 2s 定时器,只在完全无标记时置
error→ 显示 PNG - 关键细节:
page-bg.js会先同步标上pending(引擎加载中),所以网络慢时不会被定时器抢答。状态序列:pending → wasm/pending → error/pending → unsupported
- 两层默认不可见后,如果 JS 开着但
遇到的问题
- 验证时自己错两次:
- grep
classList.add('js')找不到内联脚本 —— minify 把'js'改成了"js",换个引号就找到了 ⚠️ - 状态矩阵测试里
getComputedStyle().opacity读到的全是起始值 —— 因为.page-bg__png/.page-bg__canvas有transition: opacity .35s,加完 class 立刻读拿到的是插值。加transition: none !important后才拿到终态 ⚠️
- grep
- 验证时用到的两个技巧(前面几轮积累的):
- 状态矩阵:用真实构建出来的 CSS(
file:///tmp/cb/main.min.<hash>.css)+ 手动切换 class/data 属性,把 2 层 × 6 状态编码成 12 bit 像素条,一次截图全拿到 - 兜底定时器:定时器要走 2s,而截图在
load就触发 → 用本地 http server 上一个/__slow(time.sleep(4))拖住load,定时器才有机会先触发
- 状态矩阵:用真实构建出来的 CSS(
验证
| 项 | 结果 |
|---|---|
| 内联脚本位置 | <body> 开头(1589)早于徽标标记(7577)✅ |
| 状态矩阵(真实 CSS) | A无JS=PNG、B有JS未就绪=两层都不可见、C就绪=canvas、D双击=PNG、E unsupported=PNG、F error=PNG ✅ |
| 兜底定时器 | page-bg.js 不加载 → 2s 后 error + PNG 可见 ✅ |
| 正常流程 | pending → wasm,pt-bg-ready 就位 ✅ |
下次建议
- ⚠️ 教训(值得记住):任何“用 JS 加类来控制 CSS 初始状态”的模式,那个 JS 必须是同步内联的,不能是
defer外部脚本 —— 否则在它执行前 CSS 处于错误状态,首屏就是错的。以后改bg.html/baseof.html时留意这条 - 如果哪天想让
.js更早到位(例如放进<head>),把那段内联脚本挪到partial "head.html"之前即可;目前放 body 开头已经够用(徽标在 body 末尾)
维护记录(十八续)—— 徽标改为淡入淡出,去掉聚合动画
完成的工作
- 徽标不再“从画外飞入聚合”,改为粒子就位后整体淡入淡出 ✅(用户反馈)
page-bg.js重写(299 → ~250 行):删掉了整块 rAF 动画循环(loop/wake/raf/settled)- 现在只做一件事:需要显示时调一次
draw()=mod.settle()+putImageData。settle()会把 build 时随机散布的粒子直接吸附到目标位置,所以没有任何飞入过程 - 显隐完全交给 CSS 的
transition: opacity .8s ease(.page-bg__box)→ 滚到底部淡入、离开淡出 - 接口校验从
build + tick改成build + settle(徽标不再用 tick) - 懒绘制:
drawn标记控制“只在首次揭示 / resize 后画一次”,不用每次显隐都重画
- 副产品:徽标侧的 CPU 开销归零 —— 不再有 rAF 循环,也不再有逐帧光栅化(之前 tick() 约 0.35ms/帧);只在揭示、resize、双击切换时各画一帧
遇到的问题
- 无(改动是纯减法)
- ⚠️ 一个判断:demo 页
/particles/的聚合动画我没有改。理由:那一页是草稿、不发布,而且它的看点恰恰是“展示引擎能逐帧做的动画”(tick()仍被它使用,Rust 侧也保留不动)。如果希望 demo 页也改成淡入淡出,告诉我一声即可
验证
DOM stub 跑真实 page-bg.js:
| 项 | 结果 |
|---|---|
settle() 调用 | 1 次 ✅ 粒子直接就位 |
putImageData | 1 次 ✅ |
tick() 调用 | 0 次 ✅ 无逐帧动画 |
requestAnimationFrame | 0 次 ✅ 完全不需要 |
| 状态 / 类名 | wasm + pt-bg-ready ✅ |
| 产物 CSS | .page-bg__box { opacity: 0; transition: opacity .8s ease } ✅ 淡入淡出 |
另外:构建 266 页零警告、四个 JS 语法、format-markdown-check / test(23/23) / validate-posts / wasm 与源码一致 —— 全过
下次建议
- 淡入淡出速度现在写死在 CSS(
.8s)两个地方:.page-bg__box(盒子显隐)和.page-bg__png/.page-bg__canvas(两层交叉,主要用于双击切换与降级)。想更慢一点或更“柔”可以调这两个值,或把ease换成cubic-bezier - 如果以后想给徽标加“呼吸感”(粒子缓慢明暗/微摆),需重新引入 rAF,但应该做成低频(如 30fps 或只在可见时)以免白烧 CPU
维护记录(十九续)—— 图片体积优化:背景图 / 头像转 AVIF·WebP,关掉重复的 highlight.js
完成的工作
体积审计(hugo --minify 干净构建 + 逐页统计)发现首屏 ~260–350KB 里 95% 是图片,于是做了四件事:
- 背景图 PNG → AVIF ✅
- 源图
static/bg/*.png(共 668KB)移到了assets/src/bg/:Hugo 只把static/整目录复制进public/,assets/里未被 Pipes 引用的文件不发布 → 原图只占仓库、不占部署体积,还能随时换质量重新编码 - 新增
scripts/optimize-images.ts+scripts/lib/image-plan.ts(纯函数,配套__tests__/optimize-images.test.ts),deno task optimize-images/optimize-images-dry - 产物
static/bg/*.avif(640px q45):293→50KB / 199→28KB / 171→43KB,总 668→122KB(-82%) - 为什么是 AVIF:同一张图 640px 下 AVIF q45 = 50KB、WebP q82 = 162KB、PNG = 293KB。用 480 设备像素(显示宽度上限 320 CSS px × dprCap 1.5)对齐后 PSNR 仍有 35.7dB,肉眼无差
- 模板改动:
partials/bg.html与_default/particles.html的readDir白名单从.png改成.avif/.webp(换格式不用再改模板)
- 源图
- 头像 JPG → WebP ✅
static/avatar.jpg(387px / 25.1KB)→static/avatar.webp(192px / 8.4KB,-66%),hugo.toml的avatar同步改为/avatar.webp。头像比徽标显眼得多,所以没跟着用 AVIF,不值得为 4KB 去赌老 Safari - 关掉 highlight.js ✅
hugo.toml加disableHLJS = true。Hugo 早已在构建期用 Chroma 把代码块渲染成内联样式(noClasses = true+style = "dracula"),前端再挂一个 highlight.js 只是把同一份代码重新标一遍 —— 227 个页面各省 48KB(gzip 20KB) - 文档:
AGENTS.md(结构调整 + 新增「碰过assets/src/**必须重跑 optimize-images」)、themes/sdttttt-paper/README.md、custom.css/page-bg.js/bg.html的过时注释(PNG → avif/webp)
遇到的问题
- ⚠️ Hugo 的 AVIF 编码在这个版本是坏的:
.Process "resize 640x avif q45"不报错,但产出 0 字节文件(Homebrew 的 hugo 0.161.1;官方说法是 extended 支持 AVIF 编码,实测不可用)。WebP 正常但只有 -44%,所以最终没走 Hugo Pipes,改用 Deno +npm:sharp - ⚠️ 这条与历史决定相反:
4a03a20 chore(ci): drop optimize-images当时的理由是「static/bg/的 cutout 是手工调过的,不该再走一遍有损压缩」。这次是用户明确要求做体积优化,实测 640px AVIF q45 在徽标显示尺寸下 PSNR 35.7dB(肉眼无差)、体积 -82%,所以推翻了旧结论。原图完整保留在assets/src/bg/,随时可以用--force换质量/尺寸重编 - ⚠️ sharp 只在真正需要编码时动态 import(而且用变量说明符,不让 Deno 类型检查把它拉进依赖图),这样
optimize-images-dry和 CI 里的deno task test都不用装它;配套deno run --no-lock,仓库保持零 npm 依赖(deno.lock仍是一行{"version":"5"}) - 审计中顺手排掉的两个更大头(这次没动,留待后续):
index.xml等 RSS 用.Content全文共 3.1MB(占部署 36%)、所有页面都preload头像但只有首页用得到
验证
| 项 | 结果 |
|---|---|
deno task test | 25 passed / 0 failed(含新增 7 个 planImage/buildPlans 用例)✅ |
deno task optimize-images | 4 张全部转换,二次运行全部「跳过(已最新)」✅ 幂等 |
hugo --minify | 267 页零警告;public/bg/ 只有 3 个 .avif,无 PNG ✅ |
assets/src/ 是否泄漏到 public/ | 没有(find 为空)✅ |
| 文章页引用 | 不再出现 highlight.min.js;preload 变成 avatar.webp ✅ |
/particles/(hugo -D) | 3 张 avif 全部被发现 + preload ✅ |
下次建议
- 留着没做的高性价比项:①
head.html覆盖成只在.IsHome时 preload 头像(现在每页白下 8.4KB);②<img class="page-bg__png">加fetchpriority="low",并把 wasm 拉取 +build()挪到requestIdleCallback/ 首次接近页底(注意保留早期的mark('pending'),否则 baseof 的 2s 兜底会误判成error);③[params] monoDarkIcon = true把 8KB 的theme.png换成 869B 的theme.svg(会改深色切换图标的样子,需确认);④content/search.md指向不存在的layout: "search",产物是空白页,可删 - 如果哪天要换图:把原图丢进
assets/src/bg/→deno task optimize-images-dry预览 →deno task optimize-images→ 提交static/bg/*.avif - 想兼容更老的 Safari 就跑
--format webp(模板白名单已经同时接受.avif/.webp)