Files
tamabox/outputs/avatar-qrcode-slow-2026-09-05.md
T
2026-09-08 10:10:44 +08:00

8.3 KiB
Raw Blame History

个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告

日期: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.go PutObjectInput 仅设置 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 内出现可扫 → 用户体感"加载中"消失。