CSS精灵图全解析:从原理到高清屏适配与替代方案

发布时间:2026/9/13 7:06:50
CSS精灵图全解析:从原理到高清屏适配与替代方案 做前端的只要项目周期稍微长一点基本都绕不开CSS精灵图sprite。简单说就是把一堆小图标拼在一张大图上页面里每个图标都去这张大图里“切”一块出来显示。这招听起来有点原始但在HTTP/1.1时代每多一个小图标就是多一次网络请求页面卡不卡、慢不慢很多时候就差在这些细节上。就算放到今天维护老项目、处理兼容性场景时精灵图依然是经常要用到的保底技能。这篇文章我把从做图、拼图、定位到高清屏适配的整个流程都过一遍包括PS里的具体操作、background-position的计算逻辑、常见的坑以及现在还值不值得用。不管你是刚接触CSS的新人还是被老项目折腾到头皮发麻的维护者应该都能从中找到直接拿走就能用的东西。1. CSS精灵图到底是什么为什么老前端都对它又爱又恨1.1 每个小图标都是一次HTTP请求页面就是这么变慢的先说一个很多人已经快忘了的背景。在HTTP/1.1时代浏览器对同一个域名的并发连接数有限制以前一般是6个左右。什么意思呢就是同一个域名下浏览器同时最多只能发起6个请求剩下的要排队等着。这时候问题就来了。一个稍微像样点的页面不说图片、样式、脚本光是顶部导航的小图标、按钮图标、列表图标随随便便就十几个。如果每个图标都是独立的图片文件浏览器就得先排队请求这个、再请求那个。每张图片都是一次完整的TCP握手加HTTP往返几毫秒到几十毫秒不等。图标越多等待被拉得越长首屏就会明显变慢。我见过一个很典型的项目首页有二十几个小图标打开以后能肉眼看到图标一个一个蹦出来那种体验真的谈不上好。后来把图标全部合成一张精灵图首屏请求少了一大截加载速度提升非常明显。打个比方吧。你去超市买东西如果每拿一瓶酱油就去结一次账结二十几次账时间全浪费在排队上了。聪明人都是拿一个购物车把所有东西装一起最后结一次账。精灵图干的就是这件事。1.2 sprite的原理把一堆小图拼成一张大图再用定位“切”出来精灵图的原理不复杂。CSS里一个元素默认只能设置一张背景图background-image于是大家就想到我可以把多个小图标合并到一张大图里然后用background-position把需要显示的那块“挪”到元素的可见区域。具体来说背景图默认是从元素的左上角开始放置的也就是background-position: 0 0 的情况。如果整张精灵图里某个图标在第2列第3行你希望元素窗口里只显示这个图标就要把背景图往左推、往上推推到合适位置。这个“推”的动作在CSS里就是设置负值的background-position。这里有一个很多新手会搞混的点负值到底代表什么方向。记住一句话——元素不动背景图动。背景图往左移动是负x背景图往上移动是负y。比如background-position: -60px -40px意思就是背景图整体向左移动60像素、向上移动40像素。另外精灵图使用的时候一定要记得设置background-repeat: no-repeat。不然背景图会在水平和垂直方向不断重复铺满整个元素你看到的就不是一个图标而是满屏的“马赛克墙”极其酸爽。这个问题我后面在排查章节还会再提一次因为它实在是出现频率太高了。2. 手把手制作一张精灵图PS手工合成与自动化工具实测2.1 PS手工合成切片、对齐、导出一步步来在没有自动化构建工具的时候我都是在Photoshop里手工拼图。流程不复杂但有几个细节做不好后面定位一定会翻车。第一步先统一图标尺寸。比如我要做一套40x40的小图标那所有图标的视觉范围都按40x40来设计。这个尺寸必须是固定的不能这个图标35px、那个45px不然拼出来的精灵图边缘参差不齐计算坐标的时候想哭都来不及。第二步新建画布。假设有4个图标横向排列图标尺寸40x40我习惯在图标之间留20px的间距顶部和底部各留20px。那么画布宽度就是4个40加上3个20等于220px高度是40加上下各20等于80px。这里留间距很重要一是防止图标之间互相干扰二是给以后新增图标留一点空间。第三步把图标拖进画布用PS的“对齐”功能把它们水平居中对齐、垂直居中对齐。注意对齐的时候要盯着“信息”面板看坐标确保每个图标都落在整数像素上别出现像x40.5这样的半像素。半像素是后期定位差1px的头号元凶。第四步隐藏背景图层。精灵图一般要透明背景所以只保留图标图层背景图层直接隐藏。然后执行“文件—导出—存储为Web所用格式”格式选PNG-24勾选透明度。这里有个经验之谈导出的PNG我会再用TinyPNG压一遍。很多时候一套精灵图几十KB压完之后能降到原来的一半甚至更少加载速度差异肉眼可见。2.2 偷懒方案在线工具与构建插件自动生成手工拼图在图标少的时候还行一旦图标数量上了十个手算坐标、手动对齐就变成了纯粹的内耗。这时候我有两个偷懒方案。第一个是在线雪碧图生成器。比如csssprites.com、toptal的CSS sprite generator直接把一堆小图标拖进去它会自动完成拼图并且把每个图标对应的background-position都生成好。对于临时性、一次性需求这类工具非常香。但它们的拼图策略不一定最优有时候产出的CSS类名比较随意得自己做映射。第二个是前端工程化的自动生成方案。如果你用webpack可以试一下webpack-spritesmith用gulp的话有gulp.spritesmith。这类插件的用法大同小异你把所有需要合并的图标丢进一个指定文件夹构建时自动生成一张精灵图和对应的CSS文件或者Data URICSS里面每个图标一个类名比如.icon-home、.icon-user直接用就行。自动生成的好处不只是省时间更重要的是“再也不用算坐标了”。你新增一个图标丢进文件夹重新构建一下坐标、间距、命名全部帮你处理好彻底告别手算。2.3 拼接和命名时容易被忽略的细节不管是手工拼还是工具拼下面这几个细节都值得留意。间距别太小。我见过有人把间距设为1px、2px理由是“这样可以减小图片体积”。但一旦后续要新增图标、重新拼图或者遇到高清屏双倍图间距太小会导致坐标计算极其困难稍微偏差一点就会串图。我个人的习惯是至少10px以上如果图标本身比较大间距可以更大。命名要有规律。图标文件在拼接前叫什么名字工具生成的类名往往就基于这个名字。所以从一开始就把图标命名为icon-home.png、icon-user.png这种规范形式别用aaa.png、111.png。后期维护的时候你就知道规范命名有多省心了。注意阴影和出血。图标的投影阴影如果出了画布边界导出后边缘会被硬生生切掉看起来非常假。拼图的时候要给阴影预留空间或者把画布拉大一点确保阴影完整。3. 背景定位的核心算法background-position的坐标到底怎么算3.1 坐标系的起点和方向很多人一开始就搞反了到了整个精灵图技术里最容易出事的环节——算坐标。先建立一个基本认知background-position: 0 0表示背景图左上角和元素左上角对齐。此时元素的可见区域显示的是背景图的左上角那一块。如果我想显示背景图中坐标为(x, y)位置处的图标就要让背景图向左移动x像素、向上移动y像素写成background-position: -xpx -ypx。x和y就是目标图标左上角在整张精灵图里的位置。这里的x和y怎么算假设一张精灵图宽度240px、高度120px图标按3列2行排列每个图标40x40图标之间的间距和上下边距都是20px。那么这个图标在第2行第3列x坐标 (3-1) * (40 20) 120pxy坐标 (2-1) * (40 20) 60px因此background-position就是 -120px -60px。一个最容易犯的错误就是把方向搞反写成正的120px 60px。正数意味着背景图向右、向下移动元素的可见区域会跑到背景图的左上角左边去结果就是一片空白或者显示错位。还有一个新手常犯的错算间距时漏掉最后一项。注意第1个图标的x是0往右每多一个图标偏移量就增加一次“图标宽度间距”。很多人算第3个图标时直接用了120px但没意识到120是从0开始算的结果最后一个图标的位置差了一个间距的距离。3.2 实战一个带hover态图标按钮的完整实现光讲原理有点干来个实际例子。假设我要做一个“发消息”按钮默认状态显示一个信封图标鼠标悬停时显示另一个颜色的信封图标。两个图标做在同一张精灵图里水平排列每个图标40x40间距20px。图片总宽度 40 20 40 100px高度40px。HTML结构a href# classbtn-mail发消息/aCSS代码.btn-mail { display: inline-block; width: 40px; height: 40px; background: url(sprite.png) no-repeat 0 0; text-indent: -9999px; overflow: hidden; }这里把链接文字用text-indent挤到屏幕外视觉上只显示图标。如果不希望隐藏文字也可以给元素加padding把文字放在图标右侧。hover时的关键代码.btn-mail:hover { background-position: -60px 0; }第一个信封在坐标(0, 0)第二个信封在x 40 20 60px处所以hover时背景图x方向左移60pxy方向不变。很多时候hover背景切换会突然“跳一下”因为图片第一次加载还没完成背景一闪而过又恢复原样。解决的办法是提前把精灵图加载好比如在页面底部加一个控制不住的容器把精灵图设成它的背景图人为触发浏览器加载。当然如果整张图本来就在页面上使用这个现象基本遇不到。3.3 当精灵图需要缩放时定位计算就全变了上面讲的是精灵图以1:1尺寸显示的情况。如果因为某些原因你在CSS里写了background-size把精灵图缩小了那之前算好的坐标就不能直接用了。比如一张精灵图设计尺寸是200x100你把它设置成background-size: 100px 50px相当于整图缩小了一半。那么原来坐标在(40, 30)的图标在缩放后的背景图里实际位置变成了(20, 15)。background-position的负值也要跟着减半。这个点在高清屏适配时特别重要下面专门开一节讲。4. 高清屏下精灵图发虚三种解法一次说清4.1 先搞懂dpr是怎么把图片“放大”的高清屏设备Retina屏上的问题是精灵图几乎所有应用场景都躲不掉的坎。先理解一个概念dpr即devicePixelRatio设备像素比。通俗说一个CSS像素在普通屏幕上是1个物理像素在2倍屏上是2x2共4个物理像素在3倍屏上是3x3共9个物理像素。所以当你给一个40x40的容器放一张40x40的图片时普通屏幕显示是清晰的但2倍屏会用80x80个物理像素去展示这40x40的内容相当于把图片强制拉大了一倍。图片本身没有那么多细节被拉大之后就变得模糊、发虚。如果只是普通页面图片倒还好但图标这种东西边缘清晰度要求很高一模糊档次全无。要解决这个问题就得给高清屏准备更高分辨率的图。4.2 方案一双倍图加background-size缩放最经典的做法就是做一张二倍尺寸的精灵图然后用background-size把它缩回设计尺寸。举个例子。原本图标设计显示尺寸是40x40二倍图就要把图标实际画成80x80间距也从20px变成40px。整张二倍精灵图如果是400x200那CSS里就写background-size: 200px 100px把整图缩回设计尺寸。注意这个时候background-position就不能写二倍图里的原始坐标了。因为整张图被background-size缩小了一半图标在整个背景图里的位置也等比例缩小了一半。原来二倍图里x120px的位置缩放后实际是60px所以background-position应该写-60px。写起来大概是这样的.icon-mail { width: 40px; height: 40px; background-image: url(sprite2x.png); background-size: 200px 100px; background-repeat: no-repeat; background-position: -60px 0; }只提供二倍图其实还不够完美。有些高分屏是3倍屏有些普通屏幕是1倍屏理论上各准备一套对应倍数的图最合适。实际项目中大多数团队会做一个折中用media query或者image-set给不同dpr设备提供不同图。.icon-mail { background-image: url(sprite.png); background-size: 200px 100px; } media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .icon-mail { background-image: url(sprite2x.png); } }这里有个比较隐蔽的坑两套精灵图必须保证图标排列完全一致否则同一个类名在不同设备上会显示成不同图标排查起来非常折腾。4.3 方案二把精灵图换成SVG一劳永逸如果你已经受够了计算二倍图坐标、维护多套图那可以考虑直接用SVG。SVG是矢量格式不管屏幕dpr是2还是3它都能无损缩放不会出现任何模糊问题。做法上不需要去拼“位图精灵”而是用SVG的symbol功能做“矢量雪碧图”。基本用法是这样的svg styledisplay: none; symbol idicon-home viewBox0 0 24 24 path dM12 2L2 12h3v8h6v-6h2v6h6v-8h3L12 2z/path /symbol symbol idicon-user viewBox0 0 24 24 path dM12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z/path /symbol /svg svg classiconuse href#icon-home/use/svg svg classiconuse href#icon-user/use/svgCSS里直接控制颜色和大小.icon { width: 24px; height: 24px; fill: #333; } .icon:hover { fill: #f00; }矢量雪碧图不需要计算background-position不需要考虑高清屏颜色还能直接用CSS改配合:hover、动画都很方便。要注意的是如果SVG放在外部文件里用的方式引用就会涉及跨域问题本地file://协议直接双击打开页面时是加载不了的必须起一个本地服务或者把SVG内联到HTML页面中。5. 踩坑复盘精灵图最常见的5个问题与排查方法5.1 图标总是差1~2像素对不齐这是我被问过最多的问题。图标位置大概对了但就是差那么一两像素怎么调都不太对。经验告诉我先不用怀疑CSS大概率是制作环节出了问题。第一检查图标有没有精确落在像素网格上。PS里如果图标坐标是40.5px这种半像素导出后边缘就会发虚定位也容易跑偏。把PS的“视图—对齐—像素网格”打开重新调整图标位置。第二检查间距计算。如果说好图标间距20px结果拼图时一个图标和另一个之间变成了21px坐标就算不对了。第三检查容器宽高是否和图标设计尺寸完全一致。如果容器设了border背景图定位是从border内侧开始的一像素偏差就来了。排查顺序建议是先看容器尺寸再看背景图本身最后才怀疑background-position的计算。5.2 精灵图加载后一片空白或者乱套空白或者乱套多半不是图片本身的问题而是基本的CSS属性没写对。最常见的就是漏了background-repeat: no-repeat。一旦漏掉整张精灵图就会在元素里疯狂平铺视觉上完全不是图标而是一张让人眼晕的拼贴画。这个错我已经不知道见过多少次了。其次是容器没有宽高。很多新手拿一个 或者放图标但忘了给宽高元素尺寸为0背景图自然就看不到。给容器设置display: inline-block再把宽高补上问题立刻消失。还有可能是路径确实错了图片没加载出来。这时候打开浏览器开发者工具切到Network面板看那个背景图片请求是不是404。如果是404检查CSS文件里的相对路径是相对于CSS文件位置解析的而不是相对于HTML页面很多人都在这里栽跟头。5.3 合成后体积变大加载变慢不划算精灵图并不是把所有图合在一起就一定最好。如果图片本身很大比如那种带渐变、带纹理的banner图硬塞进精灵图里一张图就好几十KB甚至上百KB那还不如拆开按需加载。我的经验是只把体积小、数量多、复用率高的图标合进精灵图。像那种超过10KB的图先考虑压缩压完还是很大就单独作为独立图片引入。另外合并前记得用TinyPNG或pngquant压缩一遍。一套图标压完之后体积通常能下降50%以上。还有一种情况是颜色数量很少的简单图标可以尝试导出PNG-8格式。PNG-8虽然颜色数少但支持alpha透明对纯色图标来说画质几乎无损体积能比PNG-24小很多。5.4 高清屏下图标发虚、颜色奇怪、背景发黑这三个问题往往一起出现根源各不相同。发虚的原因大概率是只有1倍的精灵图没准备二倍图或者写了background-size但背景图本身分辨率不够。换个思路理解CSS把40px的图显示成40px但2倍屏幕要用80个物理像素去呈现等于拿一辆能跑40km/h的车非让它跑出80km/h的速度力不从心当然虚。颜色奇怪、边缘有杂色一般是导出格式不对。图标有小色块、透明背景尽量用PNG-24如果用了PNG-8且颜色数太少渐变色就会变成一条条色带。背景发黑常见于把透明PNG硬转成了JPG或者某些工具导出时自动填充黑色底。检查一下导出设置确认格式是PNG而不是JPG。5.5 精灵图问题排查速查表问题现象可能原因解决办法图标差1~2px 对不齐未对齐像素网格、间距计算错误、容器与图标尺寸不一致检查PS中对齐、重新计算间距、统一容器尺寸背景图平铺、画面混乱漏写background-repeat: no-repeat补充no-repeat图标不显示容器无宽高、图片路径404设置显示模式与宽高、检查相对路径图片加载闪烁精灵图首次加载未完成预加载精灵图或使用Data URI高清屏图标模糊只有1倍图或background-size使用不当使用二倍图并正确设置background-size颜色带、杂色PNG-8颜色数不够改用PNG-24导出透明背景变黑误导出为JPG使用PNG格式6. 精灵图过时了吗现代替代方案与我的取舍建议6.1 字体图标iconfont最省心的纯色小图标方案现在做新项目如果是纯色、单色的图标我第一个想到的往往不是精灵图而是字体图标iconfont。代表产品就是阿里的iconfont字体平台和Font Awesome。字体图标的本质是“字体”只不过每个字符被替换成了一个矢量图形。它的好处特别明显矢量清晰放大缩小不模糊颜色直接用CSS的color改大小直接用font-size调整个项目通常只要引入一个字体文件请求数极少。字体图标也有自己的短板。首先它天生适合单色图标想实现多色渐变就非常吃力。其次字体文件本身如果很大首次加载会有短暂的白屏或者闪烁需要做字体预加载优化。第三某些字体的渲染在不同浏览器上可能会有细微差异这个在做像素级还原时会很头疼。6.2 SVG symbol雪碧图矢量、可控、不易出错如果你需要多色图标、动态效果、或者希望支持更灵活的自定义样式SVG symbol是目前我比较推荐的方式。前面介绍过它的基本用法。相比传统位图精灵图SVG雪碧图不需要计算坐标不存在高清屏模糊问题颜色控制自由还能做CSS动画。因为SVG本质上是一段XML文本有时还能被CSS的fill、stroke等属性精细控制。需要留意的几个限制是内联SVG会让HTML体积变大外部SVG文件有跨域问题而且老浏览器对的支持不完整。如果兼容目标包含IE11以下建议引入svg4everybody这样的shim或者干脆降级使用精灵图。6.3 什么时候精灵图仍然是首选说了这么多替代方案是不是意味着精灵图该被扫进历史垃圾堆了我的看法是它依然有价值。第一种场景就是老项目。很多维护多年的项目已经有一整套基于精灵图的图标方案CSS类名、图标资源、开发习惯全都定好了。这时候贸然迁移到iconfont或SVG成本很高、收益却不大反而不如保持原样继续沿用精灵图方案。第二种场景是需要位图质感的图标。比如图标上有细腻的纹理、光影、渐变效果或者干脆是一张真实物体的照片。这类图形用SVG画起来复杂度太高、体积也不一定小用位图精灵图反而合理。第三种场景是目标浏览器环境比较老旧比如企业内部系统跑的还是老浏览器对SVG支持不完整。这时候精灵图是兼容性最稳的方案。从我个人的实践来看新项目中我的默认组合是单色小图标用iconfont复杂多色图标用SVG symbol纯位图、照片类素材直接用独立图片而精灵图则作为兼容性兜底方案留着备用。这套组合既能保证性能又能照顾到开发体验。最后再分享一个经验不管用哪种方案静态资源都建议加上版本号或者内容hash。精灵图尤其要注意缓存问题改完图片、文件还是老名字的话用户那边没准儿还加载着旧图排查起来特别浪费时间。把文件名后面加上一段hash比如sprite-a1b2c3.png每次内容变化hash也变缓存问题就从根上解决了。