# 个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告 > 日期:2026-09-05 | 对象:`https://box.tama.guru/_/tamakyi`(个人主页 list.html) > 测速视角:南宁电信(真实大陆出口,与本机直连一致) --- ## 一、结论速览 | 严重度 | 问题 | 一句话 | |---|---|---| | 🔴 P0 | **R2 图片全部无 `Cache-Control`** | 头像 Last-Modified 是当天 → 启发式缓存寿命 ≈ 0 → **每次**页面加载都重新走国际链路下载头像,这是"每次都很久"的直接原因 | | 🔴 P0 | R2 是纯 CF 国际链路 | 南宁电信实测每个 R2 资源 **0.78–1.4s**,是 snrat 大陆镜像(0.3s)的 **3–5 倍**;304 条件请求也要 **1.05s**(省不掉往返 RTT) | | 🟠 P1 | 二维码绘制依赖头像 `onload` | easy.qrcode 加载 logo 头像失败仅 `console.error`、**无任何降级** → 头像加载失败/超时 = 二维码永久空白 | | 🟠 P1 | 头像缓存键分裂 | 同一头像被拆成 `?og-image=1`(header icon/meta)与 `?v=2`(页面头像 + QR logo)两个缓存条目 → 一页下载 2 份 | | 🟠 P1 | 背景图 303KB 未压缩 PNG | `box_background.png` = 297KB,换设备/清缓存时首访要拉 297KB 的国际链路下载 | | 🟡 P2 | defer 统计脚本拖慢 DOMContentLoaded | vercount 实测中位 1.24s(波动至 2.7s)、mami 波动至 3.7s → 二维码初始化挂在 DOMContentLoaded,被一起拖慢 | --- ## 二、加载链路拆解(页面发生了什么) 个人主页资源依赖(实测 61KB HTML): ``` EO 动态页 HTML ........................ ~1.2s (源站新加坡,已知架构上限) head 渲染阻塞 CSS:仅 uikit.min.css (snrat) . ~0.5s (waline CSS 已用 media=print 非阻塞,✅ 无问题) defer 脚本 ×10(下载完才触发 DOMContentLoaded): snrat: uikit/icons/dayjs/alpine/waline/jQuery 0.2–0.5s/个 R2: time.box.js(1.6KB) + easy.qrcode(52KB) ~0.9–1.0s/个 第三方: cn.vercount.one ~1.2s(波动 2.7s)、mami.tama.guru ~0.3s(波动 3.7s) 头像 (body 解析即并发) ...... ~1s(R2) 背景 uk-img(UIkit 可见后加载) ............... ~1.4s(R2,297KB) favicon = 头像 URL ............................ ~1s(R2) 二维码 boxqrimg: DOMContentLoaded 后 easy.qrcode 初始化 → 先加载 logo=头像?v=2(onload 后才绘制)→ +0~1s ``` **首访体感:头像 ~2.2s、二维码 ~2.5–3.5s(R2 链路抖动时 5s+)** **二次访问:由于无 Cache-Control,头像/favicon 每次仍要 ~1s 重新下载 → 这就是"每次都慢、一直加载中"的体感来源。** 浏览器"加载中"迟迟不退的构成:`load` 事件要等非懒加载图片(头像)完成;`DOMContentLoaded` 要等全部 10 个 defer 完成(含 R2 与第三方统计的最慢者)。两者都被 R2 + 统计脚本拖着。 --- ## 三、证据链(均为 2026-09-05 实测) ### 3.1 各资源耗时(南宁电信,3 次中位) | 资源 | Host | 中位耗时 | 备注 | |---|---|---|---| | jQuery / UIkit | cdnjs.snrat.com | **0.29s / 0.52s** | 大陆镜像,基准线 | | 头像 webp 27KB | r2.337845818.best | **0.91–1.05s** | | | easy.qrcode.min.js 52KB | r2.337845818.best | 0.96s | | | time.box.js 1.6KB | r2.337845818.best | 0.90s | 小文件也 0.9s = 纯 RTT 主导 | | 背景 box_background.png 297KB | r2.337845818.best | 1.39s | | | waline CSS | r2.337845818.best | 0.99s | | | vercount 统计 | cn.vercount.one | 1.24s(波动至 2.69s) | | | mami 统计 | mami.tama.guru | 0.31s(波动至 3.7s) | | ### 3.2 决定性验证:R2 缓存头 ``` 头像/背景/图标/CSS 响应头: Content-Type / ETag / Last-Modified 有 Cache-Control ❌ 无 Expires ❌ 无 cf-cache-status: DYNAMIC(CF 边缘不缓存,每次都回 R2 源) ``` - 头像 `Last-Modified: Sat, 05 Sep 2026 06:49:29 GMT` = **当天上传** - 按 RFC 7234 启发式缓存:freshness ≈ (当前时间 − Last-Modified) × 10% ≈ **0** → 浏览器每次**重新验证/下载** - 实测:带 `If-Modified-Since` 的 304 请求仍需 **1.05s**(RTT 无法通过 304 消除) - 对比:easy.qrcode.js / time.box.js 有 `max-age=2678400`(31 天强缓存)→ 它们二次访问是秒出的,唯独**图片类对象从未设置缓存头** > 上传代码 `internal/storage/s3.go` `PutObjectInput` 仅设置 `ContentType`,**从未设置 `CacheControl`** —— 已确认。 ### 3.3 easy.qrcode 头像 logo 逻辑(反编译 52KB 源) ```js if (d.logo) { var r = new Image; r.onload = function(){ 绘制(带头像logo) }, // ✅ 头像到了才画 r.onerror = function(a){ console.error(a) }, // ❌ 仅打日志,什么都不画 r.crossOrigin = d.crossOrigin; r.src = d.logo; // logo = 头像 URL } ``` **结论:二维码 = 同步等待 R2 头像图下载;头像失败/超时 → 二维码区域永久空白,页面没有任何降级提示。** ### 3.4 头像 URL 在同一页面出现 3 个变体(缓存键分裂) | 出处 | URL | 浏览器行为 | |---|---|---| | header.html 646–648(icon/favicon) | `头像?og-image=1` | Chrome 请求 favicon | | header.html 642/644/649(og/twitter meta) | `头像?og-image=1` | 爬虫专用,浏览器不下载 | | list.html 4–6 + 页面头像 + QR logo | `头像?v=2` | 页面必下载 | `?og-image=1` 与 `?v=2` 是两个缓存键 → favicon 与页面头像各下载一次(同一天上传的头像,各自都 miss)。 --- ## 四、优化建议 ### P0(改小、见效最大:解决"每次都慢") **① R2 上传图片设置强缓存头(根因修复)** - 改 `internal/storage/s3.go` 上传处加 `CacheControl: aws.String("public, max-age=31536000, immutable")` - 图片 key = 日期 + 15 位随机串,**天然不可变** → `immutable` 完全安全 - 存量对象(历史头像/背景)用 `wrangler r2 object` 或 S3 API 重打 metadata;或重传一次 - **预期收益**:二次访问头像/favicon/QR logo 从 ~1s → **0ms(本地缓存直读)**,这是本次优化中体感变化最大的一项 **② easy.qrcode 失败降级 + 两阶段绘制(防二维码空白)** - 改 remake 库 logo 分支:`onerror` 时改为**先画无 logo 二维码**(立即可扫),并 `onload` 时自动重绘带 logo 版 - 或加超时(如 3s)兜底,避免头像悬挂时二维码永久空白 - 参考实现方向:库内 `r.onerror = function(){ 降级绘制() }`,其中降级绘制即"无 logo 路径 `b()` + makeImage" ### P1(进一步缩短首访) **③ 统一头像缓存键** - 确认 `?og-image=1` 无服务端语义(Go 代码中无处理,疑似历史遗留)后:header 的 icon/favicon 与 list.html 统一用同一 URL 形态(②落地强缓存后,`?v=2` 击穿历史污染缓存的目的已由 immutable+随机 key 取代,可逐步去掉 query) - 收益:每页少 1 次头像下载(favicon 与页面头像共享缓存条目) **④ 背景图压缩** - `box_background.png`(297KB)→ WebP(预计 ≤60KB),首访下载量降 ~5 倍;该图 2024-08 上传未再变,压缩一次长期受益 **⑤ 统计脚本 async 化** - `cn.vercount.one/js`、`mami.tama.guru/tamami.js` 均非页面功能依赖 → 改 `async`(或 DOMContentLoaded 后再注入),不再拖慢 DOMContentLoaded → 二维码初始化提前 ### P2(结构性,可选) **⑥ 二维码区域骨架占位**:`#boxqrimg` 给 min-height + 轻提示(如"二维码生成中…"),避免空白感 **⑦ waline 评论懒加载**:滚动到评论区再初始化(若评论非首屏核心) **⑧ 图片域加速(治本路线)**:R2 域名无法直接挂 EO,但可在 R2 域名前接 EO/大陆 CDN 回源 `r2.337845818.best`,把头像等图片链路从"CF 国际 ~1s"降到"EO 近大陆 ~0.2s"——与主站 EO 方案同理,属另一工程 --- ## 五、建议落地批次 | 批次 | 内容 | 风险 | |---|---|---| | 批 1(P0) | ① + ② | 低:加缓存头仅影响新上传;库改动影响 3 个模板的二维码渲染,需测试 profile/list/not-found | | 批 2(P1) | ③④⑤ | 低–中:③需先确认 og-image 语义 | | 批 3(P2) | ⑥⑦⑧ | 视需要 | > 参考实测:①落地后二次访问头像 0ms;②落地后即使头像失败,二维码 0.2s 内出现可扫 → 用户体感"加载中"消失。