CSS精灵图全解析:从原理到高清屏适配与替代方案
2026/9/13 7:06:47 网站建设 项目流程

做前端的,只要项目周期稍微长一点,基本都绕不开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的“对齐”功能把它们水平居中对齐、垂直居中对齐。注意,对齐的时候要盯着“信息”面板看坐标,确保每个图标都落在整数像素上,别出现像x=40.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 URI,CSS里面每个图标一个类名,比如.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) = 120px
  • y坐标 = (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="#" class="btn-mail">发消息</a>

CSS代码:

.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方向左移60px,y方向不变。

很多时候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缩小了一半,图标在整个背景图里的位置也等比例缩小了一半。原来二倍图里x=120px的位置,缩放后实际是60px,所以background-position应该写-60px。

写起来大概是这样的:

.icon-mail { width: 40px; height: 40px; background-image: url("sprite@2x.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("sprite@2x.png"); } }

这里有个比较隐蔽的坑:两套精灵图必须保证图标排列完全一致,否则同一个类名在不同设备上会显示成不同图标,排查起来非常折腾。

4.3 方案二:把精灵图换成SVG,一劳永逸

如果你已经受够了计算二倍图坐标、维护多套图,那可以考虑直接用SVG。

SVG是矢量格式,不管屏幕dpr是2还是3,它都能无损缩放,不会出现任何模糊问题。做法上不需要去拼“位图精灵”,而是用SVG的symbol功能做“矢量雪碧图”。

基本用法是这样的:

<svg style="display: none;"> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M12 2L2 12h3v8h6v-6h2v6h6v-8h3L12 2z"></path> </symbol> <symbol id="icon-user" viewBox="0 0 24 24"> <path d="M12 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 class="icon"><use href="#icon-home"></use></svg> <svg class="icon"><use href="#icon-user"></use></svg>

CSS里直接控制颜色和大小:

.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也变,缓存问题就从根上解决了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询