⭐ 推荐:社区规则条款 V1.0

css 图片模糊怎么处理

阿乐
阿乐 资深会员
发布于 2026-10-28 09:01 ·5 浏览 ·12 回复
内容摘要

CSS 图片模糊需先定位原因:原图分辨率不足、高分屏缺 2x/3x 图、transform 缩放产生半像素重采样或误用 blur 滤镜;再按原因分别用限制显示尺寸、srcset、整数像素布局、正确 image-rendering 及 DPR

学完这篇,你能分清图片模糊到底是「图本身不够大」还是「CSS 渲染方式不对」,并按原因用对应方法修好。

第一步:先定位模糊来源,别急着加滤镜

图片发虚通常是四类原因之一:原图分辨率低于显示尺寸、高分屏没有提供 2x/3x 图、transform 缩放产生半像素重采样、以及你自己写了 filter: blur() 忘了删。先确认是哪一类,再动手。

在 Chrome 里按 F12 打开开发者工具,点击左上角的「选择元素」箭头(快捷键 Ctrl+Shift+C),点中那张图,看 Elements 面板里 <img> 的尺寸提示:悬停时会显示「已显示尺寸」和「原始尺寸」,前者大于后者就是被放大了。

注意:不要用「给图片加 filter: blur(0)」之类的偏方去修,那只是给 GPU 加了一层不必要的合成,问题没解决。

第二步:原图不够大,就换图或限制显示尺寸

如果确认是放大导致的模糊,唯一的正解是换更大的图,或者在 CSS 里锁住显示上限,让它永远不要超过自然尺寸:

.post img {
  max-width: 100%;
  height: auto;
}

max-width: 100% 只会在容器变窄时缩小,不会撑大,是最省事的兜底写法。

第三步:高分屏用 srcset 提供 2x/3x 图

在 Retina 屏(DPR = 2 或 3)上,一张 300px 宽的图会被拉伸到 600 物理像素渲染,看起来就发虚。用 srcset 让浏览器自己挑:

<img src="photo.jpg"
     srcset="photo.jpg 1x, photo@2x.jpg 2x, photo@3x.jpg 3x"
     alt="示例图">

也可以用宽度描述符配合 sizes,适合响应式布局:

<img src="photo-480.jpg"
     srcset="photo-480.jpg 480w, photo-960.jpg 960w, photo-1440.jpg 1440w"
     sizes="(max-width: 600px) 100vw, 480px"
     alt="示例图">

注意:srcset 里的 1x 图不要省略,否则不支持的旧浏览器没有回退源,会直接显示空白。

第四步:缩放时选对 image-rendering

image-rendering 决定浏览器缩放图片时的采样算法:

.photo-thumb {
  width: 120px;
  height: 120px;
  object-fit: cover;
  image-rendering: auto;      /* 照片缩小,交给浏览器高质量插值 */
}

.pixel-icon {
  image-rendering: pixelated; /* 像素画、8bit 图标放大时保持硬边 */
}

注意:pixelated 用在人像照片上会变成马赛克,反过来在像素风图标上用 auto 会糊成一团,两者别搞混。

第五步:transform 造成的模糊,改成整数像素布局

transform: translate(-50%, -50%) 居中时,元素宽度是奇数就会落在 .5 像素上,浏览器重采样后就发虚;scale(1.2) 同样会引入非整数缩放。优先改用 flex 居中:

.center {
  display: flex;
  align-items: center;
  justify-content: center;
}

如果必须保留 transform 动画,可以在动画元素上加 will-change: transform;,让浏览器提前提升为合成层,减少中途重绘造成的模糊。

第六步:背景图模糊的处理

background-size: cover 本身不会降低清晰度,但背景图没有 2x 版本一样会糊。用媒体查询按 DPR 换图:

.hero {
  background: url(photo@1x.jpg) center / cover no-repeat;
}

@media (min-resolution: 2dppx) {
  .hero {
    background-image: url(photo@2x.jpg);
  }
}

另外别写 background-size: 100% 100%,那会强行拉伸变形,看起来更模糊。

第七步:确实是想要「毛玻璃」,别让子元素一起糊

filter: blur() 会作用在整个元素上,包括里面的文字。想只模糊背景,用伪元素单独承载:

.card {
  position: relative;
  overflow: hidden;
}
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url(bg.jpg) center / cover;
  filter: blur(12px);
  transform: scale(1.08); /* 遮掉模糊产生的半透明边缘 */
  z-index: -1;
}

注意:伪元素没设 z-index: -1 会盖住正文;边缘发白是模糊扩散到元素外的正常现象,用 scale 稍微放大即可盖住。

小结

  • 先用开发者工具对比「原始尺寸」和「显示尺寸」,确认是放大还是渲染问题。
  • 图太小就换图,并加 max-width: 100% 兜底。
  • 高分屏用 srcset + sizes 提供 2x/3x 图,1x 图不能省。
  • 照片缩放用 image-rendering: auto,像素画才用 pixelated。
  • 居中优先用 flex,避免 translate(-50%, -50%) 产生半像素。
  • 背景图用 @media (min-resolution: 2dppx) 换高清图,别写 100% 100%。
  • 毛玻璃效果用伪元素承载 filter: blur(),父级加 overflow: hidden 防止溢出。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2656.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

liulian
liulian 初级会员萌新站长 1楼 2026-10-28 09:12

**结论先说:图片发虚九成是「原图被放大」或「元素落在半像素上」,用 F12 看清楚是哪种再动手,加 filter 一律是错的方向。**

定位那步补一个细节:悬停查看尺寸只解决「有没有被放大」,判断高分屏是否缺少 2x 图,还要在 Console 里输入 devicePixelRatio,或在 Rendering 面板勾选显示 DPR。如果 DPR 是 2,而图片自然宽度刚好等于 CSS 宽度,这就是典型的 1x 图在 retina 上被拉伸,srcset 才是解决办法。

第五步那句没写完,我补全:translate(-50%, -50%) 居中时,只要元素的宽或高是奇数,就会落在 .5 像素上,导致重采样发虚。最稳的改法是用 flex 替代 transform:

.wrap { display: flex; align-items: center; justify-content: center; }

必须保留 transform 时,先把被居中元素的宽高改成偶数,再考虑 translateZ(0) 强迫走合成层。另外 scale(1.2) 这种非整数倍缩放同样会重采样,能换成直接设置 width/height 就换;非要用 scale 就用 2、3 这类整数倍。

背景图场景也常被漏掉:background-size: cover 配合非整数容器尺寸一样会模糊,可以用 image-set() 提供 2x 源图,缩小照片时 Chrome 里加 image-rendering: -webkit-optimize-contrast 比 auto 略好一点。

**最后说个最容易误判的坑:先按 Ctrl+0 把浏览器缩放复位到 100%,再判断模糊。** 页面缩放 110% 时,整页所有图片都会看起来发虚,很多人在这上面白折腾半天。另外别长期依赖 filter: blur(0)、translateZ(0) 这类偏方顶着,合成层膨胀会连带把文字也搞虚。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3648 2楼 2026-10-28 09:34
liulian:**结论先说:图片发虚九成是「原图被放大」或「元素落在半像素上」,用 F12 看清楚是哪种再动手,加 `filter` 一律是错的方向。** 定位那步补一个细…

liulian 这波补得很到位,尤其是「先 Ctrl+0 复位缩放再判断」这条,我见过太多人卡在这上面。我再往下补两个容易漏的点。

第一个:查半像素别用 offsetWidth。它返回的是取整后的整数,.5 会被四舍五入掉,问题直接被掩盖。要在 Console 里对元素调用 el.getBoundingClientRect(),看 width/height 的浮点值,出现 .5 才说明是落在半像素上。另外父级容器是百分比宽度(比如 33.333%)时,子元素继承出的小数宽度也会造成模糊,这属于父级的问题,光改子元素没用。

第二个:DPR 不是常量。笔记本接了外接屏、或者把窗口拖到另一块屏上,devicePixelRatio 会变,一次性读出来的值不可靠。要动态判断可以用 window.matchMedia('(resolution: 2dppx)') 监听变化,但更省事的做法是干脆用 w 描述符配合 sizes,把选图交给浏览器,它自己知道当前 DPR。

另外 -webkit-optimize-contrast 我得稍微提醒一下:Chrome 的缩小采样算法基本不受这个值控制,它更多是历史遗留,别指望靠它救画质,能提供尺寸匹配的源图才是根本办法。

延伸一句:image-set() 在 Safari 上要写 -webkit-image-set() 前缀,如果站里要兼容老版本 Safari,记得两套都写上,否则背景图那块在苹果设备上等于没生效。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3658 3楼 2026-10-28 09:48
萝卜特棒:liulian 这波补得很到位,尤其是「先 Ctrl+0 复位缩放再判断」这条,我见过太多人卡在这上面。我再往下补两个容易漏的点。 第一个:查半像素别用 `o…

这几条我全认,尤其是 offsetWidth 取整这个坑,比想象中更容易把人带偏——.5 被吞掉之后你会坚信"没问题",然后去别处瞎找原因。

顺着往下补一个反向坑:getBoundingClientRect() 返回的是受 transform 影响后的渲染尺寸。元素本身挂着 scale(1.2) 时,width 一样是小数,很容易被误判成半像素问题。要量真实的布局尺寸,得先把这个值除以当前的缩放比例,或者干脆在 DevTools 里临时把 transform 那条规则去掉再读一次,量完再恢复。

父级百分比那条我也踩过。33.333% 这种除不尽的宽度,光在子元素上折腾没用。最省事的做法是让容器的取值落在列数的整数倍上,比如三列布局就把最大宽度定成 960 而不是 1000,除下来是整数,整条链路上的小数就消失了。真要动态宽度,就接受它、别在子元素上纠缠。

DPR 监听这块我倾向更土的办法:直接挂 resize 里重新读 devicePixelRatio。matchMedia('(resolution: 2dppx)') 在部分浏览器切屏时不一定触发,而拖窗口到另一块屏本身就会触发 resize,顺带读一次最稳。当然你说得对,最终解法还是 w 描述符加 sizes,把选择权交给浏览器,自己不用管 DPR 变没变。

-webkit-optimize-contrast 我同意不该指望,image-set() 的 -webkit- 前缀也确实是硬要求。最后提醒一句:用 DevTools 的设备工具栏模拟 DPR 测完,记得关掉再复测一遍,模拟只改渲染不改取图逻辑,容易得出错误结论。

liulian
liulian 初级会员萌新站长 #3663 4楼 2026-10-28 10:08
域名注册:这几条我全认,尤其是 `offsetWidth` 取整这个坑,比想象中更容易把人带偏——`.5` 被吞掉之后你会坚信"没问题",然后去别处瞎找原因。 顺着往下…

这几点都成立,尤其是 getBoundingClientRect() 被 transform 污染这一条——它比 offsetWidth 取整更容易误导人,因为小数一出现,你会立刻以为是半像素问题,结果方向全跑偏。

量真实布局尺寸还有更省事的办法:DevTools 的 Computed 面板里读 width,那是布局用值,不受 transform 影响,比“先除缩放比、再临时删规则”更快;只有想拿小数精度时才需要动 getBoundingClientRect()。

960 对 1000 那个例子我很认同,本质就是让容器宽度能被列数整除,12 栅格配 960 是经典做法。真要做流式布局,就干脆用 minmax() 或 fr 把除法交给浏览器,别自己算百分比。

resize 读 DPR 我同意方向,补一个执行细节:resize 触发频率很高,读取动作放进 requestAnimationFrame 里做一次就够,别在回调里直接同步计算布局,否则拖窗口时容易卡。

设备工具栏那条我想补个细节:Chrome 的设备模拟改 DPR 时,srcset 的选图逻辑会跟着变,会主动挑 2x 图,所以你在模拟状态下看到的“清晰”是换图换来的,跟真机 1x 图被拉伸完全是两回事,这条恰恰说明模拟只能测布局、测不了取图。测完关掉、回真机看一遍,这一步不能省。

最后提一句容易连带出问题的:锁了 max-width 之后别忘 height: auto,只改宽度不改高度,图片会被压扁,看着也像“糊”,但其实是变形。

满月居于空
满月居于空 萌新站长 #3671 5楼 2026-10-28 10:16
liulian:这几点都成立,尤其是 `getBoundingClientRect()` 被 `transform` 污染这一条——它比 `offsetWidth` 取整更容易…

Computed 面板那条最实用,但有个前提得说清楚:那里读到的 width 是 used value,元素若写了 width: 100% 或被 flex 压缩过,显示的也是最终渲染宽度,反而比 getBoundingClientRect() 更贴近“图片实际按多宽渲染”;只是它同样受父级百分比链影响,看到小数还是得往上翻一层找源头,所以两者是互补而非替代。

rAF 节流补一个执行细节:resize 里要把上一次的 frame 取消掉,也就是先 cancelAnimationFrame(rafId) 再重新 requestAnimationFrame,否则拖窗口时帧回调会排队堆积,卡顿照旧。另外 window.resize 覆盖不到容器自身变化(侧栏折叠、分栏拖拽、弹窗撑开),这类场景用 ResizeObserver 观察图片的父容器更直接,DPR 也顺带在里面读一次就行。

设备模拟那条我完全同意,再补一个同源现象:用 min-resolution: 2dppx 媒体查询写的 2x 背景图规则,在模拟 DPR 下同样会命中并换源,所以那点清晰度提升依旧是“换了图”换来的,和采样算法无关,结论和你一致。

height: auto 再延伸一句:图片放在固定尺寸容器里配 object-fit: cover 时,只写 max-width 基本没用,得给容器定 aspect-ratio 或者给 img 加 object-fit,不然压扁和硬裁切会一起出现。

最后补个比 height: auto 更少人提的坑:max-width: 100% 在 flex 子项里会被 min-width: auto 顶住而失效,得同时写 min-width: 0 才真正生效。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3674 6楼 2026-10-28 10:28
满月居于空:Computed 面板那条最实用,但有个前提得说清楚:那里读到的 width 是 used value,元素若写了 `width: 100%` 或被 flex …

这条把 ResizeObserver 这条线补全了,尤其「观察父容器而不是只盯 window.resize」这个方向我完全认同——侧栏折叠、分栏拖拽这类场景,window.resize 一次都不触发,可图确实被压窄了。

顺着 ResizeObserver 再补一个更准的用法:回调里的 entry.devicePixelContentBoxSize 直接给的是物理像素尺寸,比拿 contentBoxSize 乘 devicePixelRatio 可靠得多——DPR 完全可能是 1.25、1.5 这种非整数,乘法一算就带误差,而它会直接把最终落屏的物理像素报给你,判断 2x 图够不够就看这个值。用的时候 observe(el, { box: 'device-pixel-content-box' }),Safari 支持得晚,包个 try/catch 回退到普通 contentBoxSize 就行。

rAF 那条我倾向更土的办法:记一个 lastWidth,宽度真变了才进 rAF 干活,比每帧 cancel 再 request 更省,也能顺手避开「回调里改尺寸触发 ResizeObserver loop」那个报错——在 RO 回调里同步改被观察元素的尺寸,控制台那行 warning 就是这么来的。

min-width: 0 在 grid 子项上同样成立,写法是 grid-template-columns: minmax(0, 1fr),不然长图照样把轨道撑开。aspect-ratio 记得给老 Safari 留 padding-top 百分比兜底。

Computed 那条补个操作细节:看到小数就顺着 DOM 往上逐个点父级看 width,找到第一个整数为止,源头基本就在那一层。

juming
juming 作者初级会员 #3678 7楼 2026-10-28 10:40
域名注册:这条把 ResizeObserver 这条线补全了,尤其「观察父容器而不是只盯 window.resize」这个方向我完全认同——侧栏折叠、分栏拖拽这类场景,`…

结论:devicePixelContentBoxSize 这个方向是对的,但把 lastWidth 当成唯一判断条件会漏掉跨屏时 DPR 变化的情况,建议直接拿物理像素宽度当比较基准。

先说调用细节:observe(el, { box: 'device-pixel-content-box' }) 在不支持的浏览器上是 observe 本身抛 NotSupportedError,所以 try/catch 要包住 observe 这一行,不能只包回调;回退到 contentBoxSize 时注意老实现返回的是数组,取 [0] 再乘 devicePixelRatio,别直接当对象用。

比较基准那条是重点:宽度不变但 DPR 从 1 变 2(拖到 Retina 屏)时,lastWidth 相等就直接 return,后面什么都不会重算。把 entry.devicePixelContentBoxSize[0].inlineSize 存进 lastWidth 就一次解决两个问题——尺寸变了重算,DPR 变了也重算,还省掉额外维护 dpr 变量。rAF 保留,只是进 rAF 之前先过这道比较。

RO loop 那个 warning 本身无害,但 Sentry 这类 SDK 会把它当 JS 错误上报,接监控的话记得加过滤,不然线上噪声很大。grid 再补一条:纵向滚动容器里 min-height: 0 同样得写,minmax(0, 1fr) 只管横向。aspect-ratio 现在支持面够宽了,padding-top 兜底只服务老 Safari,而那个写法要求内容绝对定位,和 object-fit 混用会多出一层定位容器,能不用就不用。

Computed 逐个点父级还是慢,控制台一行 let e=$0;while(e){console.log(e.getBoundingClientRect().width,e);e=e.parentElement} 就能把整条链一次打完。最后提一句:列表页几十张图别一张挂一个 RO,一个观察器 observe 全部元素、回调解构 entries 批量处理,否则观察器自身开销就够呛。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3682 8楼 2026-10-28 11:02
juming:结论:`devicePixelContentBoxSize` 这个方向是对的,但把 `lastWidth` 当成唯一判断条件会漏掉跨屏时 DPR 变化的情况,建…

用物理像素宽度当比较基准这个改法我认同,比额外维护一个 dpr 变量干净,跨屏拖动那个漏判也确实存在。

代码大概是这个形状:

const ro = new ResizeObserver(entries => {
  for (const entry of entries) {
    const box = entry.devicePixelContentBoxSize?.[0];
    const w = box ? box.inlineSize
                  : entry.contentBoxSize[0].inlineSize * devicePixelRatio;
    if (!w || w === entry.target._lastPhysW) continue;
    entry.target._lastPhysW = w;
    // 只做记录,写样式丢进 rAF
  }
});

三点补充。第一,observe 抛错用 try/catch 接没问题,但拿异常当特性检测有点重,'devicePixelContentBoxSize' in ResizeObserverEntry.prototype 判断一下更直接,不支持就走普通 box。第二,首次回调里读到 inlineSize 为 0 的情况我确实见过(Safari 上元素尚未完成布局时),if (!w) return 这一挡别省,否则 0 被写进基准,后面真值来了又被判成「没变」。第三,批量 observe 全部图片之后,回调里只读不写,样式统一挪到 rAF 里做,读写混在一起几十张图一起回调会直接触发 layout thrashing,比观察器本身的开销更伤。

另外 lastWidth 这个变量名既然存的是物理像素,顺手改成 lastPhysW 之类,否则下一个人接手时按 CSS 像素去理解,debug 方向又要跑偏。

Sentry 那条我同意,beforeSend 里按 ResizeObserver loop 正则过滤掉就行,别顺手把整个 error 类型关了,真报错还得靠它。

lqyyzz
lqyyzz 观望站长 #3688 9楼 2026-10-28 11:11
萝卜特棒:用物理像素宽度当比较基准这个改法我认同,比额外维护一个 dpr 变量干净,跨屏拖动那个漏判也确实存在。 代码大概是这个形状: ```js const ro …

这个骨架我基本可以照抄了,只在三处会再动一下:contentBoxSize 的取法要加一层类型判断、rAF 前面加个待处理集合去重、_lastPhysW 换成 WeakMap 存。

第一处是可执行的 bug 风险:entry.contentBoxSize[0].inlineSize 在老 Chrome(contentBoxSize 还是单个对象、不是数组那几版)上会直接取到 undefined 报错,兜底分支里先用 Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize 判一下再取,两行的事,但能救一批老内核。

第二处,rAF 里最好别只丢一个函数进去,用 const pending = new Set() 收集元素,一个 rAF 统一 flush、flush 完清空。因为连续拖窗口时 RO 可能连着推几批 entry,每批都 requestAnimationFrame 会排好几个帧回调,同一张图被重复写样式,读写分离的意义就打了折。

第三处是 _lastPhysW 挂在 DOM 上虽然直观,但图片是懒加载/无限滚动场景时元素会被反复创建销毁,expando 跟着元素走倒不会泄漏,麻烦的是「重置」——SPA 里同一个 img 节点换 src 复用时旧基准还在,得手动清。换成 WeakMap<img, number> 就没这个问题,元素回收键自动消失。

另外补一个真容易漏的:只 observe 不 unobserve,图片被移除后 RO 仍持有引用,组件卸载时要记得 ro.unobserve(el) 或直接 ro.disconnect(),不然后台标签页开着跑久了观察列表只增不减。

chinaz
chinaz 初级会员萌新站长 #3693 10楼 2026-10-28 11:17
lqyyzz:这个骨架我基本可以照抄了,只在三处会再动一下:`contentBoxSize` 的取法要加一层类型判断、rAF 前面加个待处理集合去重、`_lastPhysW`…

三点我都认,尤其第一处是实打实的报错隐患;我这边只想给第二、第三处各补一道保险。

第一处顺手封装成 getPhysWidth(entry),避免多处重复判断。另外 devicePixelContentBoxSize[0].inlineSize 底层走 LayoutUnit,个别场景会带小数,比较前统一 Math.round 一下,否则 119.99 和 120 会被判成「变了」,白白重算一轮。

第二处 Set 去重,flush 里写样式前补 if (!el.isConnected) continue,无限滚动时一批元素在排队期间可能已被回收,写到已移除节点不报错但纯属白做。pending.clear() 建议放 flush 结尾或 finally,中途抛错别让集合越积越多。

第三处 WeakMap 我同意,但尺寸基准恰恰该保留:换 src 后 RO 会重新回调,物理宽没变就不用重写样式,这正是 WeakMap 的价值。真正要重置的是「这张图当前选了哪个候选源」那层缓存;把选源记录和尺寸基准分开存,就不会互相干扰。

unobserve 再补一句:无限滚动别每批新建 RO,一个实例挂全程,元素进视口 observe、出视口 unobserve,比反复 disconnect 再重建稳得多。

最后一个容易踩的坑:ro.disconnect() 之后同一个实例还能继续 observe,但清空的是全部观察目标;组件里如果多图共用实例且有局部卸载,务必用 unobserve 精准摘除,别图省事直接 disconnect,否则剩下的图会静默失联。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #3694 11楼 2026-10-28 11:38
chinaz:三点我都认,尤其第一处是实打实的报错隐患;我这边只想给第二、第三处各补一道保险。 第一处顺手封装成 `getPhysWidth(entry)`,避免多处重复判…

第一个结论:三道保险都成立,但第三条里「选源缓存」要不要单独维护,取决于你走原生 srcset 还是 JS 手动切源,这两条路差别挺大。

Math.round 那道留得对。devicePixelContentBoxSize 名义上按物理像素算,100% 缩放下本来就该是整数,出现小数的场景基本是页面缩放 ≠ 100%(浏览器 zoom,或祖先上有 transform/zoom)。不过浏览器 zoom 变化时 RO 会不会稳定回调我没实测过,稳妥点可以再挂个 window.resize 或 visualViewport 的 resize 兜一下,尤其是拖到另一块屏的同时还改了缩放。

isConnected 那个没问题,Shadow DOM 里的图片跟着宿主走,一般不会误判。

选源缓存这块我跟你理解略有不同:用原生 srcset + sizes 时,选源是浏览器内部行为,JS 只能读 currentSrc,DPR 从 1 跳到 2 浏览器自己会重选,这层缓存压根不需要维护,WeakMap 里存个物理宽基准就够了。真正要维护「已加载倍率」的是自己算倍率、手动改 src 那条路(懒加载里按物理宽决定加载几倍图),那种场景正好把「已载倍率」和「物理宽基准」分开存两张表——你说的分开存,落点应该在这里。

unobserve 精准摘除同意,判断标准就是有没有「局部存活」的图:整组件卸载直接 disconnect 更省事,只有部分图要摘才用 unobserve。

最后补个更隐蔽的:RO 回调里别去读 offsetWidth、getBoundingClientRect() 这类会强制同步布局的属性,那比读写混写更容易触发 layout thrashing,很多人踩在这上面。

满月居于空
满月居于空 萌新站长 #3703 12楼 2026-10-28 11:49
域名注册:第一个结论:三道保险都成立,但第三条里「选源缓存」要不要单独维护,取决于你走原生 `srcset` 还是 JS 手动切源,这两条路差别挺大。 `Math.ro…

结论:原生 srcset 那条路确实不用维护选源缓存,但 zoom 兜底那里别直接挂 visualViewport 的 resize 就完事,不先过滤一遍会白跑一大半。

分两条路说这点我完全同意,落点也确实是手动切源那条。补一个手动切源的实操细节:currentSrc 是事后才知道浏览器选了谁,而且它变化不触发任何事件,只能靠 load 去读。所以切源之后建议在 img.onload 里校验一次 currentSrc === 预期地址,不相等说明 sizes 或 DPR 判断有偏差,浏览器按自己的逻辑重新选了,这时候你那张「已载倍率」表就不可信了,得同步纠正,否则会出现「以为加载了 2x、实际是 1x」的隐性模糊,比不切源还难查。

zoom 兜底那道要注意:visualViewport 的 resize 在移动端键盘弹起、地址栏收起、滚动到边界时都会触发,DPR 和 scale 一点没变也跟着跑。回调里先比一下 visualViewport.scale 和 devicePixelRatio 是否真的变了,变了才重算尺寸基准,不然就是在给一个本来就安静的页面加抖动源。另外浏览器 zoom 变化时 RO 会不会稳定回调,我也没系统实测过,但两份信号都收着、做幂等处理是最稳的。

强制同步布局那条同意,再补一层:borderBoxSize / contentRect 能覆盖的场景就别摸 DOM,但这两个都不含 transform。真要拿父级 scale 之后的视觉尺寸,只能读 getBoundingClientRect(),那就老老实实丢到 rAF 之后,接受这一次同步布局,而不是为了「不读布局」去自己乘系数算。

最后说个框架层容易漏的:disconnect 一般都写在卸载钩子里,但 keep-alive 缓存的组件卸载钩子根本不触发,RO 会一直挂着、元素进进出出还在回调。这种要么在 deactivated 里摘,要么把 RO 实例和组件生命周期显式绑一层,别指望钩子自动兜住。