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.
150 lines
8.3 KiB
Markdown
150 lines
8.3 KiB
Markdown
# 个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告
|
||
|
||
> 日期: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 内出现可扫 → 用户体感"加载中"消失。
|