先讲个我真实踩过的坑。
前两年做移动端改版,设计师交付了一套 banner 图,Mac 上打开,放大 200% 边缘照样锐利。我没多想就把原图丢进了前端页面。结果真机测试的时候,用户反馈“首页的图怎么是糊的”,尤其是标题文字的边缘,像隔了一层磨砂玻璃。当时我先怀疑 CDN 转码,又怀疑是手机型号太老,排查到最后发现原因特别基础——图片物理分辨率不够手机屏幕的 DPR 去填充。那次之后,我把 DPR、压缩、格式选择这三件事彻底捋了一遍,今天这篇就是当时的全部心得。
这篇文章适合所有被“图片模糊”困扰的人:设计师要理解为什么自己导出的资源会在手机上“变样”,前端要知道怎么适配不同屏幕密度,做性能优化的人也需要弄明白压缩和质量之间的平衡点到底在哪。放心,不涉及晦涩算法,我会把原理掰开揉碎,再给你一套可以直接抄作业的流程。
1. 模糊的第一层原因:设计稿坐标系和手机坐标系是两套逻辑
1.1 设计稿里的“清晰”只存在于设计软件里
你在 Sketch、Figma 或 PS 里打开一张图片,100% 缩放显示时,它之所以清晰,是因为此时画布上的 1 个像素,恰好对应当前电脑屏幕上的 1 个物理像素(如果你的电脑屏幕 DPR=1)。换句话说,设计稿的“清晰”是一种 1:1 的静态预览假象。它没有经过网络传输、没有经过压缩算法、不需要响应不同屏幕的像素密度,它只是“这张图在自己的原始分辨率下被完整展示”而已。
但一旦图片进入网页或 App,情况就变了。前端的世界以 CSS 像素(也叫逻辑像素)为基准进行布局,而手机的屏幕是由物理像素组成的。在绝大多数非入门级手机上,一个逻辑像素背后是 2 个或 3 个物理像素。这就意味着,一张 100px 宽的图片,在普通屏(DPR=1)上需要 100 个物理像素来显示,在 Retina 屏上却需要 200 个甚至 300 个物理像素来填充。如果你的源图只有 100 个像素点,多出来的 100 或 200 个点从哪来?只能靠浏览器用插值算法硬算出来。算出来的点没有真实图像信息,人眼感知到的就是边缘发虚、细节丢失——也就是我们常说的“糊”。
这里有个很贴切的生活类比:你把一张 100×100 的照片贴在墙上,然后拿一个 200×200 的相框去展示它,中间多出来的区域只能靠后期“脑补”填充,脑补出来的画面自然不可能有真实的纹理和边缘信息。手机屏幕干的其实就是这件事。
1.2 Retina 屏给设计师制造的“错觉”
还有一层隐藏因素:现在的设计师多半用 Retina MacBook 工作,屏幕本身 DPR=2。当你在这种屏幕上以 100% 查看设计稿时,系统已经帮你做了一次“缩放渲染”——也就是说,你在屏幕上看到的清晰,很可能是系统把图放大后做了平滑处理的效果。这种情况下,如果你凭视觉判断“这图够清晰”,十有八九会误判,因为你看的不是原图的实际信息量。
所以我在项目里的经验是:不要用肉眼在电脑上判断图片清晰度。要判断一张图够不够清晰,应该看它的物理分辨率是否达到“显示尺寸 × 目标设备 DPR”这个值。比如一个在手机端占据 320 CSS 像素宽的 banner,如果目标机型是 DPR=3,那么源图宽度至少要为 960 物理像素;如果只有 640px,在 DPR=2 的机型上能看,在 DPR=3 的机型上就会糊。这是所有后续操作的地基,也是很多人最容易忽略的第一步。
2. DPR:决定“需要多大图”的底层计算逻辑
2.1 DPR 到底怎么算出来的
DPR(Device Pixel Ratio,设备像素比)的定义很简单:设备的物理像素数除以 CSS 逻辑像素数。浏览器里直接访问window.devicePixelRatio就能拿到当前设备的值。举几个真实例子:
- iPhone 6/7/8:逻辑分辨率 375×667,物理分辨率 750×1334,DPR=2
- iPhone 14 Pro:逻辑分辨率 393×852,物理分辨率 2556×1179,DPR=3
- 主流 Android 旗舰:DPR 一般为 2.5~3.0
- 部分折叠屏或 2K 屏安卓机:DPR 可到 3.5 甚至 4.0
这就意味着“一套设计到处跑”这件事在图片资源上是行不通的。同样是 375 宽的逻辑视口,DPR=2 的手机需要 750 物理像素宽的图片,DPR=3 的手机就需要 1125 物理像素宽的图片。如果你只准备了一张 750px 的图,放到 DPR=2 的机型上正好,放到 DPR=3 的机型上就要被拉大、被插值,糊是必然结果。
很多人会问:为什么电脑上没感觉、手机上一看就糊?答案也很简单:大部分桌面显示器的 DPR 还停留在 1 或 2,手机则普遍是 2.5 到 3 起步,高端机型甚至更高。DPR 越高,对图片物理分辨率的要求就越苛刻,问题自然在移动端暴露得更明显。
2.2 图片资源该导多大的一个速算公式
我在做前端适配时,会用下面这个公式来核验所有位图资源是否合格:
图片物理像素 ≥ 图片在页面中的 CSS 尺寸 × 目标设备 DPR
举例:页面里一个 80×80 的 App 图标,目标设备 DPR=3,那么源图至少要 240×240 物理像素。一个全屏背景图,CSS 尺寸是 390×844(iPhone 14 Pro 的逻辑尺寸),DPR=3,那源图需要 1170×2532 物理像素,才能保证整屏不糊。
注意这个公式要同时考虑宽和高,不要只看宽度。很多人只记得高度和宽度成正比,却忘了 Retina 屏幕的物理像素是“宽和高各乘 DPR”,总面积是乘 DPR² 的。宽度达标但高度不达标,上下边缘一样会虚。
2.3 设计软件里的 @1x、@2x、@3x 导出是从这来的
很多设计师很好奇:为什么开发提资源时动不动要 @2x、@3x?其实就是因为上面这个计算。设计稿里一个 24×24 的图标,对应的是它在 375 逻辑宽画布上的 CSS 尺寸。要在 DPR=2 和 DPR=3 的机器上都清晰,就得分别导出 48×48、72×72 的位图。这也是为什么现在的设计工具导出资源时都会让你选 @1x、@2x、@3x,而不是一股脑导一个大图——因为不同屏幕密度需要不同规格的资源,合理分级才能兼顾清晰度和体积。
有一个细节值得多说一句:iPhone 6 Plus 这一代机型,系统内部按 DPR=3 渲染,但最终输出到 1080P 物理屏后会再做一次降采样。它的实际渲染 DPR 是 3,但物理 DPR 接近 2.6。大多数情况下我们仍然按 @3x 出资源,因为系统渲染阶段就是按 3x 的。这里不用过度纠结,只要记住主流 iOS 高端机型按 @3x 准备资源即可。
3. 压缩是在丢弃信息,关键是要知道丢了什么
3.1 有损压缩到底对图片做了什么
图片压缩这件事,很多人的理解是“把图片变小一点”,模糊点说确实没错,但它更准确的本质是:通过算法丢弃人眼不太敏感的信息,换取更小的体积。以 JPEG 为例,它会把图像切成一个个 8×8 的像素块,做离散余弦变换后,人眼对高频细节(也就是锐利边缘、纹理细节)不敏感,于是算法就把高频分量的数值往零的方向“量化”,量得越狠,文件越小,高频信息损失也越多。结果就是:文字边缘开始出现毛刺、虚影,纹理细节被磨平,渐变区域出现色带。
WebP 和 AVIF 的编码思路比 JPEG 更先进,它们用了更复杂的预测编码和更高级的变换,能在同样体积下把高频信息保留得更好。但不管什么有损格式,都绕不开一个原则:压缩率越高,细节丢失越多。没有任何算法能在无限压缩的同时保持无限清晰,“又要体积小又要高画质”只能在某个质量拐点附近取得平衡。
3.2 什么内容的图最容易被压缩压糊
我在项目里踩出来的经验是:不同类型的图,对压缩的容忍度差别巨大。
- 渐变和光滑表面:最容易出现色带(banding)。比如天空、背景光晕,压到质量 50 以下,肉眼可见一道一道的颜色断层。
- 文字和 UI 边缘:容易出振铃(边缘一圈一圈的伪影)和模糊。比如带文字的 banner、包含尖锐边角的图标。
- 纹理密集的图片(如树叶、布料、头发):压缩会把这些纹理“擦”成一片,细节全丢。
- 人像皮肤:大面积肤色本来就是低频信息,压到 60~70 肉眼还能接受,但压到 40 以下皮肤会显得“塑料感”十足。
所以如果你要压的是一张带大标题文字的营销 banner,我建议质量参数给高一点(JPEG 或 WebP 85 左右),因为文字区域对清晰度的感知极其敏感;如果是纯风景摄影背景,质量压到 70 左右,肉眼几乎分辨不出来。
3.3 质量参数 70/80/90 的真实观感差异
我在多个项目里做过盲测,结论可以参考:JPEG 质量参数从 90 降到 80,文件体积通常能减少 30%~50%,视觉差异在大部分内容上都微乎其微;从 80 降到 70,体积还能再缩 20%~30%,但在文字边缘和渐变区域会开始露出马脚;从 70 往下,各种伪影就会变得明显。所以我的默认建议是:普通内容 JPEG 用 75~85,有文字或 UI 元素的图用 85~90,仅在确实需要极限压缩时再用 60~70。
WebP 的整体表现比 JPEG 好一档。同样视觉质量,WebP 的体积常常是 JPEG 的 60%~75%。我自己用 cwebp 压图时,质量参数一般给 75~80;AVIF 更夸张,质量 45~60 就能达到接近 WebP 80 的视觉效果,体积能再省 20%~30%。但 AVIF 编码慢、解码开销高,低端安卓机上要小心使用,不能只管体积不管性能。
4. 格式选错,清晰度还没开始比就已经输了
4.1 JPEG、PNG、WebP、AVIF 的真实应用边界
格式选择和“糊不糊”的关系,很多人没意识到:格式本身不直接决定清晰度,但它决定了你在同样的体积预算下能保留多少细节。同一个画面,用 JPEG 压到 100KB 可能已经开始发糊,用 WebP 压到 100KB 可能还很锐利,这就是格式选择的差距。
我整理了一张非常适合放在项目 Wiki 里的对比表:
| 格式 | 透明通道 | 有损/无损 | 同视觉质量体积 | 兼容性 | 最适合的内容 |
|---|---|---|---|---|---|
| JPEG | 不支持 | 有损 | 基准 | 所有环境 | 照片、复杂的渐变背景 |
| PNG | 支持 | 无损 | 最大,通常比 JPEG 大数倍 | 所有环境 | 图标、UI 元素、需要透明的图形 |
| WebP | 支持 | 两种都支持 | 比 JPEG 小 25%~35% | 现代浏览器全支持 | 通用替代品,日常首推 |
| AVIF | 支持 | 两种都支持 | 比 WebP 再小 20%~30% | Safari 16+、Chrome 85+、Android 12+ | 大图、高质量内容,能用就用 |
需要特别提醒的是 PNG 的坑:它虽然无损,但和“清晰”强绑定,维护成本也高。一个透明 PNG 图标可能几百 KB,换成无损 WebP 能缩小一半以上。现在浏览器对 WebP 支持已经很完备,没必要死守 PNG。除非你是在做兼容远古浏览器的企业项目,否则新项目里 PNG 应该退出日常图片格式的名单。
4.2 根据图片内容决定格式,而不是凭习惯
同一个页面里,不同区域的图片应该用不同格式,这是我在多次优化后总结出的比较高效的策略:
- 全屏 banner 或大图:优先 WebP/AVIF。图片面积大,体积敏感,这两种格式能帮你省下大量带宽,同时保持足够锐利。
- 图标和 UI 元素:优先 SVG(矢量)或 WebP 无损。SVG 不参与像素运算,任意 DPR 下都不糊,这是位图永远比不了的。如果只能用位图,至少用 2x/3x 的 WebP 无损,而不是 1x 的 PNG。
- 表情包、聊天图片这类高频小图:WebP 有损即可,质量 75~80,兼容性和体积都合适。
- 需要保留透明通道的复杂图形:WebP 无损或 AVIF 无损。
这里我想强调一个观点:很多人把“格式选择”当成后端或前端的事,但决定图片格式的应该是内容本身。一张照片塞进 PNG 是体积灾难,一个 UI 图标导出成 JPEG 是质量灾难。搞不清楚内容类型就动手压图,事倍功半。
4.3 用 picture 标签做渐进增强,全都要
很多人担心 AVIF/WebP 兼容性,办法其实很简单:用<picture>标签做多源回退,浏览器会优先选它认识的格式,不认识的自动降到 JPEG/PNG。我在移动端项目里的标准写法是这样:
<picture> <source srcset="banner-2x.avif 2x, banner-3x.avif 3x" type="image/avif"> <source srcset="banner-2x.webp 2x, banner-3x.webp 3x" type="image/webp"> <img srcset="banner-2x.jpg 2x, banner-3x.jpg 3x" src="banner-2x.jpg" alt="活动主视觉"> </picture>这样既享受了现代格式的体积和清晰度优势,又不会在老旧浏览器上碎图。需要说明的是,上面这个写法把 DPR 选择直接写进了srcset的 2x/3x 描述符里,浏览器会根据设备密度自动挑最合适的那张。如果图片的展示宽度还会随视口变化,那就应该把w描述符和sizes结合起来,我下面会细讲。
5. 一套能直接落地的图片处理流程:从设计稿到手机
5.1 设计稿导出阶段的三个关键动作
第一个动作:确认设计稿基准宽度。如果你的设计稿是 375 宽(1x 逻辑尺寸),那所有位图资源按 @2x、@3x 导出即可;如果你的设计稿是 750 宽(很多团队习惯 @2x 起画),那导出 @1x 就相当于 @2x,不要被“1x”这个名字带偏。我建议团队里统一一个约定,否则每次都要解释一遍。
第二个动作:能用矢量的用矢量。图标、按钮、装饰线条全部用 SVG 导出,不要转成位图。SVG 的好处是整个页面的 DPR 再高都不会变糊,文件还特别小。只有照片和复杂位图才需要走下面的位图流程。
第三个动作:位图按公式计算最小分辨率。先把图片在页面里的实际 CSS 尺寸列出来,再乘以目标 DPR(一般取最大需要 3 或者 4),得到源图物理尺寸。举个实际场景:设计稿里有一个 88×88 的逻辑尺寸商品图标,目标设备包括 DPR=3 的 iPhone,那么源图至少导出 264×264 物理像素。很多设计工具可以一键导出 @1x/@2x/@3x,这时你导出 @3x 即可用于所有高密度屏,@2x 用于中低密度屏。如果设计稿原始资源达不到这个值,要标注出来让设计师重新导出,而不是硬着头皮用。这一步做完,基本能杜绝大部分“手机上图片糊”的问题。
5.2 压缩工具选型与一套可复用的参数
压缩环节我给自己的工具箱和参数如下,都是亲测过稳定可用的:
- Squoosh:免费在线工具,直接在浏览器里对比原图和压缩后的细节,支持 JPEG/WebP/AVIF。适合一张一张精修。
- TinyPNG:在线批量压缩,适合对 PNG/WebP 做无脑批量处理,但不能细调质量。
- ImageOptim:macOS 本地批量压缩,适合团队统一处理素材,配置一次后拖拽即可。
- 命令行工具:集成到 CI 流水线时用。cwebp、avifenc、cjpeg 分别对应 WebP、AVIF、JPEG。示例:
# 导出 WebP,质量 80 cwebp -q 80 banner.png -o banner.webp # 导出 AVIF,质量范围 40~50 avifenc --min 40 --max 50 banner.png -o banner.avif # 导出 JPEG,质量 82 cjpeg -quality 82 banner.png -o banner.jpg参数方面,我的通用基准是:JPEG 75~85,WebP 有损 75~80,AVIF 45~60。如果图片里有大段文字,质量往上调 5~10。如果只是背景纹理,适当往下降。压缩这件事没有唯一正确答案,最终判断标准是“在同一台 Retina 屏幕上,原图和质量图并排放大到 200% 对比,肉眼无明显差异”。
另外,如果你是在做 H5 上传图片的交互,前端用 canvas 压缩后再上传也是完全可行的方案。核心是生成 canvas 时先按目标显示宽度 × DPR设置画布尺寸,再调canvas.toBlob()指定质量参数,最后把 blob 塞进 FormData 上传。但要注意,前端压缩不能替代服务端转码,它更适合解决“用户手机原图 10MB,直接上传太慢”这类场景。
5.3 前端适配:srcset、sizes 和 CSS 的一整套配合
图片资源准备好之后,前端要用正确的方式加载。很多模糊问题其实出在加载方式上——浏览器把图硬拉到了不该放的尺寸。
对于需要响应屏幕宽度的图,推荐srcset+sizes的写法,让浏览器自己根据视口宽度和设备 DPR 选图:
<img srcset="banner-400.jpg 400w, banner-800.jpg 800w, banner-1200.jpg 1200w" sizes="(max-width: 480px) 100vw, 800px" src="banner-800.jpg" alt="活动主视觉" />这里的400w表示图片资源宽 400 物理像素,sizes告诉浏览器这个图片在页面里实际占多宽。浏览器会结合当前视口宽度和 DPR 计算出真正需要的图片宽度,然后从候选里挑最合适的那张。如果我们只给一个src="banner-800.jpg",那么在 DPR=3 的大屏手机上,800px 的图可能不够,糊的局面又会出现。
对于作为 CSS 背景图的场景,可以用 image-set() 按 DPR 提供不同资源,或者直接把多倍图信息写进 CSS:
.banner { background-image: image-set( "banner-1x.jpg" 1x, "banner-2x.jpg" 2x, "banner-3x.jpg" 3x ); }注意:即便是背景图,源图本身的分辨率依然要先满足“CSS 尺寸 × DPR”这个条件,image-set 只是负责把选图逻辑自动化,不负责无中生有。
5.4 验收:从模拟器到真机,把“糊不糊”变成可判断的标准
最后的验收环节,我把它分成三步。第一步,在 Chrome DevTools 的 Device Mode 里把 DPR 手动设成 2 和 3,分别看页面图片资源是否加载了对应的 2x/3x 版本。第二步,再用 Network 面板确认实际加载的图片 URL 的分辨率,和显示尺寸做换算。第三步,真机看一遍,尤其要看首页 banner、轮播图、商品图这类大图区域。模拟器能模拟逻辑像素和 DPR,但模拟不了真实屏幕的观感,所以真机这步省不得。
我常用的一个技巧是:在页面里临时给图片加outline: 1px solid red,如果图片被 CSS 拉得明显超出原分辨率,红色框内会出现模糊;接着用document.querySelector('img').naturalWidth查看图片原始物理宽度,和clientWidth * devicePixelRatio对比,就能快速定位是资源问题还是加载方式问题。这个方法可以分享给所有做移动端的朋友。
6. 排查清单:遇到“图糊了”按这个顺序找原因
6.1 最常见的几种“糊”和它们的真凶
我在帮团队排查图片模糊问题时,总结出一个比较高效的排查顺序,按照出现频率从高到低排:
| 现象 | 最可能的原因 | 解决方向 |
|---|---|---|
| 全屏背景图边缘发虚 | 源图分辨率不够 显示尺寸×DPR | 重新导出 2x/3x 资源或提高源图宽度 |
| 图标/小图糊 | 用了 1x 位图或没有 @3x | 换 SVG,或导出 @3x 位图 |
| 文字边缘马赛克 | 压缩质量压太低 | JPEG/WebP 质量提到 85+ |
| 图片在某种机型糊,另一种不糊 | DPR 差异导致,高 DPR 机型不够吃 | 按高 DPR 机型准备资源 |
| 图片被人为拉伸 | CSS 设置了固定宽高比例不匹配 | 检查 CSS width/height、aspect-ratio |
| 加载时先模糊后清晰(或一直模糊) | 懒加载占位图/低质量预览图被当最终图 | 检查懒加载的真实图片 URL 是否正确 |
| 原图清晰,上线后部分设备变糊 | CDN 在分发时自动转码成低质量版本 | 关闭 CDN 图片压缩或调高转码质量 |
6.2 一条排查链路:从一个 banner 糊的案例说起
有一次线上反馈首页 banner 在 iPhone 14 Pro 上发糊,我先看 Network 里实际加载的图:URL 末尾是banner_640.jpg,640px 宽。再看页面里这个 banner 的 CSS 尺寸是width: 100%,在 390 逻辑宽的手机上,DPR=3,浏览器期望的物理宽度是 390×3=1170px。而实际只有 640px,等于少了将近一半的像素信息。问题定位就清晰了:不是压缩太狠,也不是格式问题,是源图分辨率没匹配上 DPR。后来把资源换成了 1170px 宽的 WebP,文件体积反而比原来的 640px JPEG 还小,画质却明显上一个台阶。
这个案例里最值得学习的一点是:分辨率不足导致的糊,靠压缩优化是救不回来的。你只能提升源图物理像素。压缩能做的事情是在“分辨率足够”的前提下控制体积,两者的关系一定要分清。
写到这里,差不多把 DPR、压缩、格式选择这条链路讲透了。如果只记住一条经验,我希望是这句话:先在源头上保证图片物理分辨率达标,再去考虑压缩和格式;反过来,再好的格式也补不了像素缺失的坑。以后你再看设计稿,别轻易用肉眼说“这张图很清晰”,先算算它的物理分辨率能不能喂饱目标手机的 DPR,再决定压不压、用什么格式压。这才是真正不再被“糊”困扰的开始。