Files
tamabox/outputs/avatar-qrcode-slow-2026-09-05.md
T
tamakyi e3e59e7c69 chore: baseline snapshot before moderation-strictness feature
Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
2026-09-10 10:56:27 +08:00

150 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告
> 日期: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 源)
```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 内出现可扫 → 用户体感"加载中"消失。