程序员和设计师坐在一起对图,最常听到的话是什么?“我这边的颜色跟设计稿不一样”“这个图标在手机上怎么糊了”“为什么PNG这么大,切出来根本没法用”。说实话,这些问题大多数不是谁的操作失误,而是双方对图形图像底层的理解不在同一个频道。作为这个系列的第二篇,我想把那些天天打交道、但经常被一句带过的概念——像素、分辨率、位图与矢量、色彩空间、图像格式——掰开揉碎讲一遍。这篇内容不要求你背数学公式,也不要求你记住各种规范,只希望你读完以后,再跟对方聊图的时候能少几句抱怨、多几个共同语言。不管是刚入门的前端、客户端开发,还是经常跟开发对图的UI/UX设计师,这篇都值得花15分钟看完。
1. 像素与分辨率:先弄清楚“尺寸”到底是谁的尺寸
1.1 物理像素、逻辑像素和DPR:设计师说750,程序员写375
先看一个最常见的场景。设计师在Figma或Sketch里新建画板,选的是iPhone 14的尺寸,比如393x852,或者旧一点的iPhone 8的750x1334。开发拿到设计稿以后,写CSS却写375x667,甚至写375x812。两边数量对不上,看起来像是有一方搞错了。其实两个人都没错,错的是没有把物理像素和逻辑像素分开。
屏幕上的每个发光的点,叫物理像素。我们常说的“分辨率”,比如1920x1080、2340x1080,指的就是物理像素数量。但在Web和App开发里,我们写的px不是物理像素,而是逻辑像素,也叫CSS像素、独立设备像素。逻辑像素和物理像素之间的比例,就是devicePixelRatio,简称DPR。iPhone 8的物理像素是750x1334,DPR等于2,逻辑像素就是375x667。所以在设计软件里用一个750宽度的画板,对应到代码里的视口宽度就是375。DPR为3的手机,逻辑像素可能是393,但物理像素是1179,设计稿常常按物理像素导出,代码里用逻辑像素布局,于是又出现了一倍、二倍、三倍图的概念。
实际操作中,前端可以直接打开浏览器的开发者工具,在设备工具栏里选一个机型,看到里面的视口尺寸基本就是逻辑像素。也可以用一行代码打印当前设备的DPR:
// 查看当前设备的像素比 console.log(window.devicePixelRatio);如果是设计协作,最稳妥的做法是在设计稿的命名里直接标明画板尺寸是1x、2x还是3x,开发拿图的时候心里有数,不至于把切图尺寸理解错。
1.2 PPI与DPI:打印和屏幕的分辨率到底有啥区别
和DPR经常一起被搞混的,还有PPI和DPI这两个缩写。PPI是Pixels Per Inch,指的是每英寸屏幕上能放多少个物理像素,数值越高,肉眼看到的文字和线条越细腻。DPI是Dots Per Inch,最早来自印刷行业,每英寸能打印多少个墨点。现在大家都把“一张图是多少DPI”挂在嘴边,但严格说,图片文件里的DPI只是一个元数据标签,它不改变图片本身的像素数量。
举个具体例子。一张100x100像素的图片,不管它在文件属性里写的是72DPI还是300DPI,放在屏幕上显示的物理大小几乎一样,因为屏幕按像素显示,不认那个DPI标签。只有把它送去打印的时候,DPI才起作用:同样是1000x1000像素的图,按100DPI打印是10英寸宽,按200DPI打印是5英寸宽。很多人以为“把图片的DPI改成300,图片就变清晰了”,这是不可能的。清晰度取决于像素数量,不取决于DPI。想打印出大尺寸且清晰的图,要么提供原始大图,要么用矢量格式,要么就接受马赛克。
这套概念在下一节里还会延伸到切图上,因为很多设计师会在导出时纠结“该填多少DPI”。我的建议是:做网页和App设计,导出时不用纠结DPI,直接按像素导出,给开发的时候标注清楚逻辑尺寸和倍率。如果是做印刷、线下物料,再谈DPI和出血位也不迟。
1.3 常见误区与协作建议:把概念对齐再谈还原度
为了防止团队协作时互相甩锅,我把这几个概念整理成了一张对照表。不需要背,但下次对图的时候建议先看一眼,确认大家聊的是同一个东西。
| 术语 | 含义 | 通常出现在哪 | 典型误区 |
|---|---|---|---|
| 物理像素 | 屏幕实际的发光点数 | 屏幕参数、设计稿大尺寸 | 把它当成代码里的px |
| 逻辑像素 | 开发布局时使用的px | CSS、iOS pt、Android dp | 忽略DPR,直接用物理像素写代码 |
| DPR | 物理像素 / 逻辑像素 | 手机参数、浏览器 | 以为DPR是图片清晰度 |
| PPI | 屏幕每英寸像素密度 | 显示器参数 | 把PPI和文件DPI混为一谈 |
| DPI | 打印每英寸墨点数 | 印刷、打印 | 以为改DPI能提高图片清晰度 |
| 图像分辨率 | 图片的像素宽高 | 图片属性、切图导出 | 把“分辨率高”理解成“尺寸大” |
这几项如果团队里每个人心里都有数,很多争议会消失。比如设计师说“这个图标在2x下要48px”,前端就该知道逻辑像素是24px,图上要有一张96x96的位图,或者一个按24px缩放的SVG。这里给新人的一条实操建议:接到设计稿后,不要只问“尺寸是多少”,先问一句“这是1x还是2x的画板”,这句话能省下后面一半的返工时间。
2. 位图与矢量:什么时候用PNG,什么时候用SVG
2.1 位图的像素矩阵与放大的代价
位图,也叫栅格图,本质是一个二维数组,数组里每个格子存一个颜色值。你看到的JPEG、PNG、WebP基本都是位图。位图的优点是没有上限的细节表现力,适合照片、复杂渐变和自然纹理。缺点也很明显:分辨率固定。一块由500x500个像素组成的图,如果硬要放到2000x2000的容器里,多出来的像素不是凭空生成的,只能靠猜。
计算机不会让画面出现空洞,所以它会在已有像素之间插值。最简单的算法叫最近邻,直接复制旁边像素,出来的效果就是一个个锯齿。稍微聪明一点的双线性插值,会在水平和垂直方向画过渡,图片显得柔和,但也容易出现发虚。更高阶的双三次插值会参考周围16个像素,边缘更平滑,但仍然不是真实细节。这也是为什么很多人用小图放大后总觉得“肉”,因为它本质上是猜测画面里原来没有的内容。
如果真想验证位图放大的效果,你可以用ImageMagick命令行快速做对比:
# 将input.png放大4倍,默认使用高质量插值算法 magick input.png -resize 400% output_high.png # 用最近邻算法放大,效果会呈锯齿状 magick input.png -filter point -resize 400% output_pixelated.png实际工作中,尽量避免把位图从小放大。设计阶段应该按最终展示的最大尺寸去切图,比如手机上要显示120px的图标,至少准备240px的位图,而不是拿48px的图去硬拉。
2.2 矢量的数学曲线:为什么图标可以随便缩放
矢量图和位图完全不是一个物种。它保存的不是像素颜色,而是点、线、曲线、填充颜色这些数学描述。计算机不保存“红色在第100行第200列”,而是保存一条曲线经过哪些控制点、曲线用什么颜色填充。SVG里最基础的图形就是一个圆或者一个矩形,本质上都是坐标计算。
拿一个最简单的SVG圆举例:
<svg viewBox="0 0 24 24" width="24" height="24"> <circle cx="12" cy="12" r="10" fill="#333333" /> </svg>不管渲染成24px还是240px,这个圆的边缘始终保持平滑,因为每个点的坐标都可以按比例实时计算出来。矢量图的另一个好处是体积小,尤其适合图标、logo、字形这类元素。但它是数学描述,对照片这类随机分布颜色的大场景没辙。一个包含几百万个贝塞尔曲线的SVG,反而会比JPEG更臃肿,渲染也更慢。
2.3 协作场景:切图到底该给什么格式
做UI和前端的人最纠结的是切图格式。我自己的经验是有一个决策顺序:先问自己这个图形是“几何形状”还是“真实场景照片”。如果答案是前者,优先选SVG;如果答案是后者,再在有损和无损之间选。下面的表格基本覆盖了我日常遇到的大部分场景。
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 图标、logo、插画 | SVG | 无损缩放、体积小、可用CSS控制颜色 |
| 照片、复杂渐变 | JPEG / WebP | 体积小,有损压缩人眼不易察觉 |
| 需要透明背景的图标 | SVG优先,否则PNG/WebP | 位图透明格式会有锯齿,SVG没有 |
| 简单动画 | SVG/CSS/Lottie | 体积比GIF小,支持透明 |
| 截图、带文字的图片 | PNG / WebP无损 | 避免压缩导致文字边缘发虚 |
| 高保真印刷图 | TIFF / 高分辨率PNG | 支持无损保留细节 |
还有个容易踩坑的点:很多设计师导出PNG图标时,会把透明背景里的灰色脏边一起导出来。这种图放在浅色背景上没事,一放到深色背景上就像蒙了一层灰。解决办法是图标尽量给SVG,或者让设计师检查一下透明边缘有没有半透明的杂色。前端如果只能拿到PNG,也可以试试在CSS里加个混合模式,但治标不治本,最好还是回源头改图。
3. 色彩空间与颜色管理:为什么同一个颜色在两个屏幕上不一样
3.1 RGB与CMYK:发光的屏幕和反光的纸
讨论颜色之前,要先明白一个底层逻辑:颜色不是客观存在的数字,而是人眼对光的感知。屏幕上的红色,是屏幕自己发出红光;纸上的红色,是纸吸收了其他光、反射出红光。这就引出了两套颜色模型:RGB加色模型和CMYK减色模型。
设计师在电脑上做稿时默认用RGB,因为屏幕发光;如果做印刷物料,最后必须转成CMYK,因为油墨是在纸面混合。不理解这一点就会出现很尴尬的事:设计师在屏幕上选了一个特别鲜艳的高饱和蓝,打样出来却变成暗沉发灰的蓝,因为CMYK的色域比常见RGB屏幕小。程序员这边也有一堆色域要认识:sRGB是Web默认标准,Adobe RGB更常用于摄影,DCI-P3是电影和iMac、iPhone常用的广色域。同一个十六进制颜色,在不同色域的设备上显示效果可能完全不同。
前端可以用一行代码检测用户设备是否支持广色域,方便决定要不要提供更鲜艳的图片资源:
// 检测是否支持P3广色域 const supportsP3 = window.matchMedia('(color-gamut: p3)').matches; console.log(supportsP3);不要小看这个判断。如果你的产品用户里有大量老款设备,你却只提供广色域图片,颜色反而会失真。稳妥的做法是把sRGB作为基准色域,P3作为可选增强。
3.2 Gamma与亮度:同样一个#888,为什么看起来不一样
除了色域,还有Gamma这个隐藏捣蛋鬼。人眼对暗部变化比亮部敏感,如果线性存储亮度,暗部细节会显得很少。Gamma校正就是把亮度重新映射,让有限的数值能保存更多暗部信息。显示器和系统默认会用Gamma 2.2左右,照片和视频也沿用了这套套路。
这张映射表会影响我们看到的对比度。所以不止是色域不同,同一个#888的灰色,在不同Gamma设置的屏幕上,观感也完全不同。这也是为什么设计师在MacBook Pro上调出来的灰,放到Windows笔记本上可能显得更深或者更淡。程序员在开发时不要只盯着一组十六进制值,而是要靠取色器和真机上的肉眼对比来验收。
这里有个小技巧:让设计师和开发在同一个环境下看同一块屏幕,最好把显示器亮度调到接近,不要一个开夜间模式一个不开。如果涉及品牌色,可以用色差值变化来判断:两台设备之间色差ΔE小于3,人眼基本分辨不出来;大于5就合格,大于10就需要处理。
3.3 从色板到设计令牌:把颜色管理变成工程问题
颜色管理不能只停留在口头沟通上。我见过很多项目,设计师发来一份色板,前端自己在代码里手写颜色值,时间一长,设计稿里是#4A90E2,线上代码却变成了#4A8FE2,肉眼根本看不出差在哪,但品牌感就慢慢偏了。
更工程化的做法是把颜色抽象成设计令牌。设计令牌就是一套有语义的变量,色板先给一个唯一的令牌名,组件里再引用令牌,而不是直接引用具体色值。前端里最简单的实现就是CSS变量:
:root { --color-primary: #0d6efd; --color-text: #212529; --color-bg: #ffffff; --color-border: #dee2e6; } .button { background-color: var(--color-primary); color: var(--color-bg); }设计师给前端的时候,给出的不是一堆色值,而是一张令牌关系表。比如“主按钮背景用TokenColorPrimary”,这样即使品牌色从蓝色改成绿色,前端也只需要改一个变量,不会出现漏改。
如果要验收颜色,可以用浏览器DevTools的取色器直接吸屏幕上的元素颜色,和设计稿里的色值对比。开发时也可以给颜色变量写自动化测试,比如用Puppeteer截图后计算每个元素的色值,确保回归测试不会破坏品牌色。
4. 图像格式与压缩:体积、画质和透明度的取舍
4.1 常见格式一览:别再只会用JPEG和PNG
图像的格式选择,永远是在体积、画质、透明度和兼容性之间做取舍。JPEG有损压缩,体积控制好,但不支持透明;PNG无损,文字和截图清晰,但体积经常大得离谱;GIF虽然有动画,但只有256色,还容易让边缘出现锯齿。WebP是Google设计的格式,有损无损都支持,也支持透明和动画,兼容性在现代浏览器里已经没问题。AVIF是更激进的压缩算法,体积通常比WebP再小20%-30%,但在老设备的兼容性上还有风险。
下面这张表是我自己选格式时常用的速查表:
| 格式 | 压缩方式 | 透明 | 动画 | 适用场景 | 主要缺点 |
|---|---|---|---|---|---|
| JPEG | 有损 | 不支持 | 不支持 | 照片、复杂渐变 | 压缩过度会出现色块 |
| PNG | 无损 | 支持 | 不支持 | 图标、截图、文字 | 体积大 |
| GIF | 有损/无损 | 支持 | 支持 | 简单动画 | 颜色少、有白边 |
| WebP | 有损/无损 | 支持 | 支持 | Web图片首选 | 老浏览器兼容问题 |
| AVIF | 有损/无损 | 支持 | 支持 | 图片体积敏感场景 | 编码慢、兼容一般 |
| SVG | 矢量 | 支持 | 支持 | 图标、插画 | 不适合复杂照片 |
4.2 压缩实操:如何把图片体积砍掉一半还不翻车
压缩图片这件事,说难不难,但很多人一上来就把质量参数调到最低,结果图片糊成一团,体积也没小到哪去。正确的流程应该是:先选对格式,再调质量参数,最后用肉眼对比关键区域。
以Node.js环境为例,用sharp库可以快速把图片转成WebP并调整尺寸:
const sharp = require('sharp'); async function compressImage(inputPath, outputPath) { await sharp(inputPath) .resize(800, 800, { fit: 'inside' }) // 等比缩小到不超过800px .webp({ quality: 80 }) // 80质量是体积和画质的平衡点 .toFile(outputPath); } compressImage('design.png', 'output.webp');体验上很像“无损压缩指南”,但WebP本身就是有损算法,quality参数越高画质保留越好,体积也越大。通常80-85之间是最合适的档位,再往上涨肉眼基本看不出区别,体积却可能多出30%。如果想再极限一点,可以试试AVIF编码,sharp同样支持。实际项目里经常一次性处理几十张图,可以用一个循环脚本把整个目录扫一遍,然后把压缩前后的体积对比打印出来,这样你心里就有数,哪种图片收益最大。
我自己的习惯是:原图永远留一份,压缩产物单独放一个目录。压缩图只是给线上用的,不覆盖原始文件。否则哪天想重新用大图,就找不回来了。
4.3 响应式图片与Retina屏:srcset不是玄学
移动端和Retina屏普及后,“一张图打天下”已经行不通了。同一张照片,在手机上只需要480px宽,在Retina屏上的高清版就需要960px甚至1440px宽。如果统一加载1920px的大图,用户会白白浪费好几MB流量,页面加载也会明显变慢。
响应式图片的标准做法是给img标签加上srcset和sizes。srcset告诉浏览器不同尺寸的图片文件,sizes告诉浏览器图片在页面里实际占用的逻辑宽度。浏览器会根据设备DPR、视口宽度、网络状况,自动挑一个合适的图片加载:
<img srcset="photo-480.jpg 480w, photo-960.jpg 960w, photo-1600.jpg 1600w" sizes="(max-width: 600px) 480px, 960px" src="photo-960.jpg" alt="示例照片" />sizes里的单位是逻辑像素,所以如果页面里图片宽度是50vw,在手机375px下实际就是187px,浏览器会自动选择最接近且不小于该值的一张。对于设计师来说,这也意味着在给前端交付图片时,不能只给一个尺寸。最好按“1倍、2倍、3倍”输出,或者直接给一个超过最大展示尺寸的原图,让前端来处理响应式,避免小图放大、大图浪费体积的问题。
5. 图形性能与渲染:动画卡顿可能不是电脑的问题
5.1 重排、重绘与合成:为什么动画要尽量用transform
图形图像延伸到UI动画之后,程序员和设计师的协作又进入另一个雷区。设计师经常在原型里做出丝滑的过渡,但前端实现出来却一顿一顿的。问题往往不是电脑性能差,而是动画触发的方式不对。
浏览器渲染一帧的流程大概是这样:先根据DOM和CSS算布局,然后绘制颜色和纹理,最后把绘制好的图层合并到屏幕上。如果动画改变了元素的width、height、left、top这些属性,浏览器每一帧都重新计算布局,这一整条流水线都要跑,压力非常大。如果改变的是transform和opacity,浏览器可以跳过前面的步骤,直接在合成阶段把图层做位移和透明度变化,压力小很多。
所以写动画时,第一原则是不要用left/top做位移动画:
.card { transform: translateX(0); transition: transform 0.3s ease; } .card.active { transform: translateX(100px); }同样一个位移动画,用transform比用left能明显减少掉帧。这里的代价是,transform只能做整块图层的位移,没法做元素内部文字随宽度的重排。如果确实需要宽度变化,那就要接受更多重排成本,或者考虑用Canvas/SVG来替代DOM动画。
5.2 图片内存与加载:一张4K图为什么能吃掉64MB
网页卡顿还有一种隐藏原因:图片内存。很多人只看图片文件体积,却忘了它在内存里占的空间。位图加载到内存后,是按像素数和通道数计算的,和文件压缩体积没有直接关系。一张4096x4096、RGBA格式的图片,文件可能只有2MB,但内存占用是:
4096 × 4096 × 4 = 67,108,864字节,约64MB。
如果一个页面里同时放了20张这样的图,内存直接超过1GB,在移动端上就会卡到几乎不能用。所以缩略图、懒加载、按需加载都是很有必要的优化手段。懒加载很常见,一个IntersectionObserver就能实现:
const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; observer.unobserve(img); } }); }); document.querySelectorAll('img[data-src]').forEach((img) => { observer.observe(img); });另外,给图片设置解码方式也能让首屏更快一点点。HTML里的img标签可以设置loading属性和decoding属性:
<img src="photo.webp" loading="lazy" decoding="async" alt="懒加载示例" />loading=“lazy”让图片在进入视口附近才加载,decoding=“async”让图片解码不阻塞页面渲染。这两行代码成本极低,收益却很实在,建议成为团队的基础规范。
这些优化做下来,页面加载速度不一定只取决于网速,很多时候反而是内存和渲染链路在拖后腿。设计师在做动效和配图时,如果也能稍微理解这些成本,就会主动避免一些又大又复杂的特效,最后上线效果反而更稳。
我自己做了这么多年开发,也和很多设计师配合过,最后发现所有的争执都离不开这一篇里的几个概念。像素和分辨率、位图和矢量、色彩空间、图像格式,再加上渲染性能,这五块就是程序员和设计师之间的通用语言。如果你也遇到过对图时说不清道不明的时刻,建议把这几个概念存成笔记,下次直接翻出来对照。不要急着争论谁对谁错,先确认双方的坐标系,图形图像的问题,大多都有答案。