跳到主要内容

文章图片CLS问题

最后更新于:

image-20260801081712.avif

文章图片 CLS(布局反复重排)问题

问题

 1目前,我文章页面加载哪里是不是有Bug呀?
 2
 3有的小伙伴打开我网站,会看到页面抖动,抽搐
 4
 5但有的小伙伴打开我网站,都正常,我打开自己网站也都正常
 6
 7只是有个懂技术的大佬发现如下问题:
 8你要给图片加占位图
 9我反正没见到抽搐
10但那个抽搐,是因为图片太多,加载导致的
11悬停描边,这个有问题
12悬停移除阴影,然后从黑描边,变成蓝描边
13这不对劲
14
15其它懂代码的大佬给的建议:
16给你优化建议:让所有图片都跟占位图一样的尺寸,自适应缩放固定占位图尺寸,避免占位图加载成功,把你的长图、短图,直接影响文档篇幅,或加大或减短,就会出现这种现象
17
18请帮我深入排查并分析下。

回答

结论:是有问题,而且大佬说得基本对。 页面「抽搐」主因是图片占位尺寸和真实宽高比不一致,导致布局反复重排(CLS);悬停描边是次要的视觉问题,不是主因。


为什么有人抖、有人不抖

这很正常,取决于网络和缓存:

情况表现
图片已在浏览器缓存 / 网速很快img.complete 为 true,几乎不走占位,看起来正常(你自己常是这种)
冷启动、慢网、手机 4G先出 16:9 占位 → 再换成真实长图/短图 → 整页高度跳变
文章图多、比例差异大(如 OneImg 这种)累计 CLS 更明显,像「抽搐」

所以不是「随机 Bug」,而是条件触发的布局偏移。


主因:占位固定 16:9,真实图片不是

1. Markdown 图片写死了假尺寸

render-image.htmlLines 7-7

1<span class="img-error-wrapper"><img src="{{ $src }}" ... width="800" height="450" loading="lazy" ... style="max-width: 100%; height: auto; display: block;" /></span>

每张图都当成 800×450(16:9)。浏览器先按这个比例占位;真正解码后若是竖长截图或扁图,高度会突然变,正文就上下抽一下。

2. 加载动画占位也是 16:9

_img-error-placeholder.scssLines 5-8

1.img-error-placeholder {
2  ...
3  aspect-ratio: var(--img-error-aspect-ratio);  /* 实际是 16 / 9 */

加载流程大致是:

先占一块 16:9 高度load 完成去掉占位,按真实比例排版图片 absolute,不占流式高度长图变高 / 短图变矮 → 整页抖动页面16:9 占位真实图片

大佬说的「长图、短图直接影响文档篇幅」——就是这个。

你们首页卡片封面其实已经做对了(660×380 + 对应 aspect-ratio),文章正文这套还没跟上。


次因:悬停「黑描边 → 蓝描边」

当前实现并不是改 border 宽度,而是:

  • 默认:四向黑色 box-shadow(看起来像黑边)
  • 悬停:box-shadow: none + 蓝色 outline: 2px

outline 是刻意避免边框挤布局的,所以悬停一般不会造成正文高度 CLS。但还有问题:

  1. 阴影突然消失 + 蓝描边出现,观感上像「跳一下」
  2. transition: all 会把很多属性一起动画
  3. 别处还有冲突样式:scale(1.02)translateY(-2px)_docs-layout.scss / _vitepress-enhancements.scss),和「去阴影 + 蓝描边」抢效果

所以大佬觉得「悬停描边不对劲」——对,体验别扭;但页面抽搐的主因仍是图片加载重排。


和专家建议的对应关系

建议现状判断
给图片加占位已有动画占位有,但比例错了
占位与真实图同尺寸全站假 16:9这才是 Bug 核心
悬停描边/阴影有问题shadow ↔ outline + 冲突 transform次要,建议一并收拾

修复方向(按收益排序)

  1. 按真实宽高比占位(最关键)
    • 本地图:Hugo 读 resources 的 Width/Height 写进 <img>,占位 aspect-ratio 跟图走
    • 外链图:front matter / 约定尺寸,或构建时探测;至少别统一写死 800×450
  2. 加载态与最终态同一比例
    • is-loading 时 wrapper 也用该图的 aspect-ratio,加载完高度不变
  3. 悬停样式收干净
    • 去掉冲突的 scale / translateY
    • transition 只过渡 box-shadowoutline-color
    • 继续用 outline(或透明边框占位),避免真 border 挤布局

如果你愿意,我可以直接改主题:先修正文图片的真实比例占位 + 统一悬停样式,这是消除「有人抖、有人不抖」最有效的一步。

解决

按计划全部优化下吧。

已按计划改完,核心是:占位跟真实比例走,悬停不再硬切阴影/缩放。

已做改动

  1. 去掉假的 800×450 本地图读真实宽高;支持 ![alt](url "1200x800")?w=&h=;可选构建期拉取远程尺寸。
  2. 占位与成图同比例 已知比例:wrapper 先按真实 aspect-ratio 占位,加载前后高度不变。 未知比例:图片留在文档流(不再用假 16:9),浏览器按真实尺寸撑开一次,避免「先错后对」的二次跳动。
  3. 悬停样式统一 去掉 scale / translateY 和「卸阴影 → 蓝描边」硬切;改为透明 outline 预留 + 轻阴影变色。
  4. 封面图 post-cover 固定 16:9 + object-fit: cover,避免封面加载顶版面。

你可进一步加强(远程图很多时)

user-configuration/themes/hugo-teek/params.toml 里已加:

1[images]
2
3fetchRemoteDimensions = false

改成 true 后,构建时会拉取远程图真实尺寸(首构较慢,结果会缓存),CLS 能再降一档。或在 Markdown 里写:

![说明](https://oneimg.onedayxyy.cn/images/xxx.avif "1920x1080")

重新构建/刷新后再看 OneImg 这类多图长文,慢网下抖动应明显减轻。需要的话我可以再帮你开 fetchRemoteDimensions 并跑一轮构建验证。


卡主原因(avif格式问题)

已续完,构建验证通过。断点原因和结果如下。

卡住的原因

resources.GetRemote 拉 AVIF 时被 Hugo 安全策略拦截:failed to resolve media type。已在配置中放行:

[security.http]

mediaTypes = [’^image/avif$']

验证结果

场景结果
WebP + fetchRemoteDimensions写入 1228×723has-aspect-ratio
AVIF 无尺寸不再写假的 800×450,走客户端占位
AVIF + title "920x518"正确锁定 920 / 518
OneImg 正文(31 张图)全部 img-error-wrapperfake800=0,构建成功

重要限制(正文多为 AVIF)

当前 Hugo Extended 能拉到 AVIF,但解不出 Width/Height。因此:

  • 已开启 fetchRemoteDimensions = true
  • 只对 .jpg/.jpeg/.png/.gif/.webp/.tif/.tiff 做 GetRemote
  • AVIF 不白下载;靠之前的客户端真实比例占位,或手写:

![说明](https://oneimg.onedayxyy.cn/images/xxx.avif "1920x1080")

已生效配置

  • fetchRemoteDimensions = true(template / user-configuration / _default
  • security.http.mediaTypesimage/avif
  • render-image.html:安全 try 读宽高 + 跳过无效 AVIF GetRemote

临时验证目录已清理。若要给大量历史 AVIF 也在构建期锁比例,可以说一下,我可以再加批量补 title "WxH" 或独立探测脚本。

编写独立探测脚本

我想给大量历史 AVIF 也在构建期锁比例,帮我再加批量补 title “WxH” 和独立探测脚本吧。

已加好脚本,并跑完全量补全。

新增工具

  • scripts/avif-dimensions.mjs:独立探测(解析 AVIF ispe)+ 批量写 title "WxH"
  • scripts/avif-dimensions.bat:Windows 快捷入口
  • 缓存:.cache/avif-dimensions.json(已加入 .gitignore
1node scripts/avif-dimensions.mjs run        # 探测 + 写入 Markdown
2
3node scripts/avif-dimensions.mjs reprobe    # 重试失败项后再 apply
4
5node scripts/avif-dimensions.mjs apply      # 仅按缓存补 title
6
7node scripts/avif-dimensions.mjs probe --dry-run

全量结果

数量
唯一 AVIF URL14995
已探测到尺寸14919(约 99.5%)
仍失败76(多为外链 403 / 本站 404 失效图)
已写入 Markdown约 1.6 万+ 引用(分两轮 apply)

OneImg 示例已变成:

![](https://oneimg.onedayxyy.cn/images/image-20260720065042.avif "1169x438")

构建时 render-image.html 会读这个 title,直接锁 --img-aspect-ratio

你这边还需要做一步

改的是 hugo-teek-site/content/。正式构建前请重新跑 VP 转换 / make build(或你的 hugo-teek-tools convert),让 .content-vp-converted 跟上。

剩余 76 张失败图基本是失效外链,客户端占位仍会兜底;若要清掉死链可以再说。


清掉死链吧。

已清完。先复核再删除,76 条全部仍是死链,无误删。

结果

  • 删除死链 URL:76
  • 删除图片引用:81
  • 修改文件:26
  • 缓存剩余:14919 条可用尺寸,0 条失败

报告在 .cache/avif-purged.json。以后可再跑:

node scripts/avif-dimensions.mjs purge-dead

 1效果:
 2Administrator@DESKTOP-7GA8OTS MINGW64 /d/hugo-teek-private (master)
 3$ node scripts/avif-dimensions.mjs purge-dead
 4content: D:\hugo-teek-private\hugo-teek-site\content
 5cache:   D:\hugo-teek-private\.cache\avif-dimensions.json
 6mode:    purge-dead
 7缓存中待确认死链: 0
 8确认删除: 0,复活可探测: 0
 9报告: D:\hugo-teek-private\.cache\avif-purged.json
10清理文件 0,删除图片引用 0,死链 URL 0

构建前记得重新 convert,让 .content-vp-converted 同步。

推送

已全部推送到 origin/master

推送提交

  1. ca5220f7fix(images): 消除文章图片 CLS,并批量补全 AVIF 尺寸(916 个文件)
  2. ce72a376docs: 同步迭代记录(图片 CLS 修复)

当前分支与远程一致,工作区干净。

最新文章