8.3 KiB
个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告
日期: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)
头像 <img src=头像?v=2>(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.goPutObjectInput仅设置ContentType,从未设置CacheControl—— 已确认。
3.3 easy.qrcode 头像 logo 逻辑(反编译 52KB 源)
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 内出现可扫 → 用户体感"加载中"消失。