打开浏览器的Network面板,随便刷新一个带图标较多的页面,你大概率会看到一排排排队请求的小图:搜索图标、购物车图标、用户头像、星星评分……每个图标都单独发一次HTTP请求,整个页面加载时间就被这些请求数拉长了。这时候就会有人提到雪碧图(精灵图)。DaZeng这篇笔记就围绕雪碧图的使用展开,把我做过项目里真正用到的合并思路、坐标计算、工具链路和踩坑过程整理出来,给正在做前端优化或者维护老项目的朋友一个参考,尤其是刚接触CSS Sprite的读者。
雪碧图并不是什么高深算法,原理非常简单:把一堆零散的小图拼成一张大图,再用CSSbackground-position把要显示的局部“挖”出来。它解决的问题也很朴素——减少HTTP请求数量。整个方案从游戏开发领域借鉴过来,游戏里把多个角色动画帧放进一张贴图,网页里把多个小图标放进一张位图,思路一脉相承。后面我会把手工拼图、坐标计算、Retina适配、自动构建、踩坑排查,以及现在和新技术的取舍一次说清楚。
1. 一个HTTP请求引发的思考:雪碧图在解决什么问题
1.1 浏览器并发连接限制与资源排队
在HTTP/1.1时代,浏览器对同一个域名的并发连接数普遍限制在6个左右,超过这个数量的请求会进入队列等待。一个电商页面哪怕只有20个图标,理论上也会产生20个请求,这些请求和页面里真正的首屏图片、接口数据挤在一起排队。用户看到的结果就是:明明单张图片都很小,整页加载却像“堵车”一样慢。
我在维护过一个营销落地页,顶部导购、分类入口、底部工具栏加起来差不多50个PNG小图标。第一次做优化时,我用雪碧图把这些图标合并成一张整图,Network面板里的请求数肉眼可见降了一大截。你可以直观感受一下区别:原来50个图标请求变成1个,附带的好处是合并后的文件总大小往往更小,因为单张PNG能复用调色板和压缩算法,比50个独立文件更节省体积。
1.2 从游戏精灵到网页CSS精灵
“精灵(Sprite)”这个词来自游戏图形学。早期游戏机内存有限,开发者不会把每个动画角色都存成独立图片文件,而是把所有角色、动画帧、特效素材画在一张大图上,运行时用坐标把需要的部分截取出来显示。大图在内存中只需要加载一次,显示哪个角色完全由坐标决定,这就是Sprite在游戏里的含义。
网页端沿用了这套逻辑。雪碧图的叫法其实是一个音译梗,英文Sprite传到中文社区,有人干脆把它叫成“雪碧”,时间久了“雪碧图”反而成了CSS精灵图最常见的中文名。它和饮料没有任何关系,只是这个名字足够好记,大家在团队协作里也默认约定叫雪碧图。记住这一点,再看项目中各种sprite.png、icon.png文件,你就知道它们是同一个东西了。
2. 动手之前,先判断哪些图适合进雪碧图
2.1 适合合并的图片画像
不是所有图片都能无脑往里塞。我一般会先做一轮图片体检,满足下面几个条件的才值得合并:
- 体积小、外观规整的图标类图片,比如导航图标、按钮图标、状态小图。
- 带有透明背景的位图,格式通常为PNG-8或PNG-24。
- 使用频率高、在多个页面反复出现的公共图标。
- 尺寸比较固定,不会频繁变化的图片。
这类图片合并后,收益最大,维护起来也最简单。说白了就是长宽在几十像素以内、颜色不多、边缘清晰的“规矩图”,拼在一起之后坐标非常容易记忆。
2.2 不适合用雪碧图的几个场景
大照片、中等尺寸的头像、需要单独懒加载的大氛围图,不适合做雪碧图。原因很简单,把这些大图拼到一起,单张雪碧图可能达到几MB,用户为了看一个头部横幅,必须把整张大图全下载完,前期的加载速度反而更差。
频繁变动的图标也不太适合手工雪碧图。如果产品近期在改版,图标三天两头换,每次替换都要重新拼图、重新计算坐标,协调成本很高。我曾经在一家创业公司遇到这种困境:设计稿一周能改三次,我把图标手工合成到一张雪碧图里,结果每次新版本上线前都在重新导出、更新坐标,最后彻底放弃手工流程,切到了自动构建方案。
业务敏感图片更不建议传到在线雪碧图生成工具里。很多在线工具虽然方便,但上传的图片不在自己服务器上,数据风险不可控。特别是登录按钮、用户头像这类设计稿,哪怕只是轻微位移,流传出去也可能被别人拿去分析产品设计细节,得不偿失。
3. 在没有构建工具时,用PhotoShop手工拼一张雪碧图
3.1 统一基准网格和间距
手工拼图时最容易犯的错是凭感觉摆放图标,拼完以后坐标乱成一团。我自己的习惯是先统一基准网格:把图标按8px的倍数设计,比如24x24、32x32、48x48,然后用网格线对齐。这样做有两个好处,一是坐标算起来快,二是后续写CSS时,所有偏移量都是整齐的数字,不容易出错。
图标与图标之间还要留出足够间距。我通常留12px到16px,这个间距是给“相邻裁切”用的缓冲带。假设一个图标周围没有透明间隔,另一个图标紧挨着它,只要background-position算错哪怕1px,就会看到相邻图标的边缘残影。间距留大了虽然会稍微增加文件尺寸,但能大幅度降低误触概率,性价比很高。
PhotoShop操作步骤并不复杂,按照下面几步走就能得到一张干净的雪碧图:
- 新建一个透明画布,设置好圆整的总宽总高。
- 打开图标文件夹,把每个图标图层拖入画布。
- 拉参考线,让每个图标都对齐到同一条横向/纵向参考线上。
- 在“图层”面板里选中所有图标图层,用“顶对齐”和“水平分布居中”统一间距。
- 关闭背景层,确认没有隐藏的多余图层。
- 执行“文件 - 导出 - 存储为Web所用格式”,格式选PNG-24,勾选透明。
导出前一定要重点核对两点。第一点,图标的透明边缘有没有被裁干净。很多设计工具在导出时会把透明区域自动全部裁掉,导致你记录的坐标彻底失效。第二点,图层顺序是否正确,覆盖在最上层的图标不能压住其他图标的内容,尤其是有阴影或描边的图标,阴影很容易把相邻图标“咬”掉一块。
3.2 导出前必须核对的两件事
第一件事是检查画布边缘。有些图标有投影或发光效果,画布边缘如果正好切到投影的一半,会导致图标边缘看起来发虚,而且合并后很难修复。最好在图标周围统一留一圈安全边距,然后按照安全区来记录坐标。
第二件事是确认导出选项里的“透明度”没有误关。我见过同事导出的雪碧图背景是白色的,插到深色页面里,整个图标区出现白块,排查了半小时才发现在“存储为Web所用格式”里把透明通道弄丢了。PNG-24带透明通道,PNG-8也可以带透明,但两者在少数浏览器上的抗锯齿表现略有差异,普通图标用PNG-24就好。
3.3 用Gulp/webpack自动生成雪碧图
项目大了以后,手工拼图不够现实,我强烈建议把雪碧图生成纳入构建流程。Gulp生态有gulp.spritesmith,webpack生态有webpack-spritesmith插件,原理都是读入指定目录里的所有图标文件,自动拼图,自动生成CSS变量和背景定位代码。
以gulp.spritesmith为例,典型配置能实现的效果是:把src/icons目录下的所有PNG合并成sprite.png,同时生成一个_sprite.scss文件,里面定义了每个图标的类名、宽高、background-position偏移量。之后在样式里直接引用对应类名即可,再也不用手算坐标。
这个方案的维护体验比手工好太多。设计师只要把新图标扔进指定目录,执行一次构建命令,所有样式自动更新,不怕漏坐标、不怕间距不统一。你只需要在初始化时统一好图标命名规范,比如icon-home.png、icon-user.png,后续就等着打包工具自动干活。
当然,自动构建也有学习成本。团队里如果没人熟悉构建配置,第一次搭建会踩很多环境坑。对于几十个图标以下的小项目,手工拼图反而更快;对于长期迭代、图标数量持续增加的项目,直接上自动构建是正确路线。
4. CSS定位计算与核心写法
4.1 background-position的本质是坐标偏移
雪碧图的CSS写法核心是background-position。背景图片的默认位置是元素的左上角对齐,如果你把背景图整体往左或往上挪动,视觉上就能“切出”图片的某一块区域。
理解background-position有一个简单的类比:你拿着一个放大镜在看一张大地图,地图本身不动,但你移动放大镜的位置,看到的内容就不一样。CSS里的放大镜就是设置了宽高的元素,背景图相当于地图,background-position就是决定放大镜放在地图的什么位置。
坐标永远是负值,因为你要把背景图向左移、向上移,让目标图标的左上角刚好落入元素的可视区域。假如某图标在雪碧图中的位置是 x=40px,y=20px,那么CSS里写的就是:
.icon-demo { width: 24px; height: 24px; background: url('sprite.png') no-repeat -40px -20px; }很多刚学雪碧图的读者会漏掉no-repeat,导致背景图在元素内重复平铺,图标看起来一片混乱。background简写里的no-repeat必须写,并且建议写在background后,避免被其他样式覆盖。
4.2 常见场景的坐标表
假设我在项目里做一个三图标的菜单栏,导航图标分别叫home、user、cart,经过PhotoShop拼图后,得到六个坐标对应的信息。这类坐标数据最适合整理成表格,方便团队查阅:
| 图标名 | 图标宽高 | 在雪碧图中的X坐标 | 在雪碧图中的Y坐标 | CSS background-position |
|---|---|---|---|---|
| icon-home | 24x24 | 0 | 0 | 0 0 |
| icon-user | 24x24 | 36 | 0 | -36px 0 |
| icon-cart | 24x24 | 72 | 0 | -72px 0 |
| icon-home-hover | 24x24 | 0 | 36 | 0 -36px |
| icon-user-hover | 24x24 | 36 | 36 | -36px -36px |
| icon-cart-hover | 24x24 | 72 | 36 | -72px -36px |
这张表里的规律非常明显:图标横排间距是36px,第一行是普通状态,第二行是hover状态。这样的排布方式在处理悬停切换时特别好用,因为只需要改background-position的Y坐标,就能切换普通态和高亮态。坐标表的维护可以和设计稿同步更新,设计如果调整了图标,只需要重新导出图片并更新表格,CSS改动非常小。
4.3 Retina高清屏适配公式
高清屏适配是雪碧图使用里比较容易翻车的地方。普通屏幕用1倍图,Retina屏幕如果用同一张图,图标边缘会发糊。解决办法通常是准备一张2倍图,通过background-size把图片缩小到逻辑大小。
先说结论公式:假设导出的2倍雪碧图总宽度是W2,总高度是H2,CSS逻辑尺寸是W和H,那么background-size设成W2/2和H2/2。相应地,每个目标图标的background-position也要除以2,因为整张背景图被缩小了一半。
举个例子,我在PhotoShop里导出了一张2倍雪碧图,总尺寸是 480x360,目标图标在2倍图中的坐标是 x=96px,y=48px。CSS逻辑尺寸下,背景图整体缩小为 240x180,目标图标的偏移量也要跟着减半,写成 -48px -24px,对外展示的图标大小则保持不变。
.icon-profile { width: 40px; height: 40px; background: url('sprite@2x.png') no-repeat; background-size: 240px 180px; background-position: -48px -24px; }有一点必须提醒:background-size和background-position的配合关系很容易混淆。很多人以为只要设置background-size就不需要改坐标,结果图标总是偏到一边。实际上background-size改变了整个背景图的尺寸,背景图坐标系也相应缩放,所以坐标必须同步缩放。更稳妥的办法是让自动构建工具直接输出2倍图对应的CSS,避免人工算错。
5. 三个实战场景:图标菜单、hover切换、精灵动画
5.1 图标菜单与坐标映射
实际页面里最常见的是图标菜单。如果没有雪碧图,你可能会写三个img标签;用了雪碧图之后,改成三个带类名的空元素,通过CSS背景展示图标。这种方式对屏幕阅读器不一定友好,但对视觉呈现非常高效。
假设菜单结构是首页、个人中心、购物车三个图标,雪碧图按前面的表格生成,HTML结构可以这样写:
<div class="menu-bar"> <span class="menu-item icon-home"></span> <span class="menu-item icon-user"></span> <span class="menu-item icon-cart"></span> </div>CSS里先把公共背景图挂在.menu-item上,再分别给每个图标设置background-position:
.menu-item { display: inline-block; width: 24px; height: 24px; background: url('sprite.png') no-repeat; } .icon-home { background-position: 0 0; } .icon-user { background-position: -36px 0; } .icon-cart { background-position: -72px 0; }这种方式比三个独立img的好处在于:图片加载次数少,维护时可以只改CSS,不用改HTML结构。最重要的是所有图标尺寸统一,只要改.menu-item的宽高,背景定位不会错乱。
5.2 状态切换(hover)该怎么办
图标菜单必然涉及hover切换。最省事的做法是:把普通状态图标排在第一行,把hover状态图标排在第二行,两行之间Y坐标相差一个固定值。这样hover时只需要把background-position的Y坐标换成下一行对应的值,其他都不用动。
我实际写过的一种写法是:
.icon-home { background-position: 0 0; } .menu-item:hover .icon-home { background-position: 0 -36px; }如果你把hover图标统一放到第二行,这个规律可以继续推广。比如正常态的Y坐标是0,hover态的Y坐标是-36px,那么所有图标hover时都统一把Y改成-36px,X坐标保持不变,就能完成整组高亮切换。
这种排布方法还会影响雪碧图的制作方式。在PhotoShop里布置图层时,你要预留两行或三行空间给不同状态:第一行是普通状态,第二行是hover状态,第三行可能是禁用状态。按状态分区,会让后面的CSS维护轻松不少。
5.3 用CSS动画播放一列连续帧
雪碧图还能实现小动画,原理和游戏精灵动画一模一样。如果有一个角色走路动画,包含6帧,每一帧是40x40,把它们纵向排列在一张雪碧图里,然后通过@keyframes不断修改background-position,就能看到连续播放的动画效果。
核心代码大致是这样:
.walk-anim { width: 40px; height: 40px; background: url('walk-sprite.png') no-repeat; animation: walk 0.6s steps(6) infinite; } @keyframes walk { 0% { background-position: 0 0; } 100% { background-position: 0 -240px; } }代码里的steps(6)非常关键。它告诉浏览器,在0到-240px这6个坐标点之间不要做平滑插值,而是直接跳到每一帧的坐标,否则动画会变成背景图平滑滑动,而不是逐帧播放。infinite表示无限循环,适合走路、呼吸、待机循环这类效果。
这种逐帧动画的好处是避免了引入视频文件或GIF图,体积通常比GIF小不少,而且可以精确控制每一帧的时长和循环次数。缺点是动画帧多了以后,场景变化复杂,坐标编写容易出错。我建议先把帧号、帧高写进注释里,方便后续调整。
6. 踩坑实录:从图标错位到图片不显示
6.1 错位的根源排查过程
有段时间我在做一个后台系统,某个按钮图标突然向左偏移了一个半图标的距离,看起来像被“推”出去。我一开始以为是CSS写错,反复检查background-position数值,发现计算没问题。后来打开PhotoShop对着雪碧图检查,才发现是拼图时某个图标多占了一个身位,后面的图标全部跟着向右位移,整个坐标系统全部乱掉。
那次以后,我总结出图标错位的排查链路:
- 先确认图标在雪碧图里的真实位置,用PhotoShop或代码量图工具量出目标图标左上角的精确坐标。
- 再看CSS里
background-position是不是这个坐标的负值。 - 如果CSS数值没错,回到源文件检查拼图时是否有隐藏图层、多余间距、画布边缘自动裁切。
- 最后确认是不是Retina屏幕命中了媒体查询,导致
background-size被覆盖,坐标缩放没有同步。
只要按这个顺序排查,大部分错位问题都能定位到根因,而不是无头苍蝇一样四处改样式。
6.2 背景图不显示的常见原因排查顺序
背景图完全不显示也是新手高频问题。我会按下面这几个原因依次排查,顺序越靠前越容易检查:
- 元素宽高为0。很多空元素没有内容,如果不设置宽度和高度,背景图根本撑不起来。
- background简写被其他样式覆盖。比如先写了
background-position,后来又写了一条background简写,把前面的定位覆盖掉了。 - 图片路径错误。CSS文件和图片文件相对路径算错了,浏览器找不到图片资源。
no-repeat没写,图标被重复平铺成马赛克效果。
这里有个小技巧:遇到不显示的情况,先打开浏览器开发者工具,选中元素看“样式”面板里background-image到底有没有生效。如果生效了,再看“网络”面板里图片请求有没有404。这两个面板能直接锁定大半问题。
6.3 更隐蔽的坑:缓存、间距和透明通道
还有一类坑和代码无关。雪碧图更新后,浏览器可能还带着旧缓存,用户看到的永远是上一个版本。尤其对文件名不变的同名雪碧图,这个问题特别典型。我处理时通常会给雪碧图文件名加上版本号或内容哈希,比如sprite-20250112.png,这样更新文件后不会触发缓存冲突。
间距问题还会在白色背景图标上翻车。有些图标本身是不透明的白色区域,拼图时如果间距留得太小,旁边图标的白色部分会透过半透明边缘“渗”过来,视觉上像多了一圈白边。遇到这种情况,建议在导出前检查每个图层的混合模式,并确认周围有足够的透明padding。
透明通道丢失的坑前面已经提过,这里再补充一个更隐蔽的情况:PNG-8虽然支持透明,但在某些浏览器组合下,半透明像素会变成锯齿状边缘。如果你做出的图标边缘发毛,尝试改用PNG-24;如果文件大小过于敏感,再退而求其次调整PNG-8的减色算法。
7. 2025年还要不要用雪碧图:和SVG、IconFont、HTTP/2怎么取舍
7.1 四类图标方案横向对比
随着HTTP/2、HTTP/3普及,单域名并发请求限制被大幅放宽,雪碧图“减少请求数”的优势没有以前那么不可替代。但这不代表雪碧图就该被扫进垃圾桶。我通常会在项目初期把四类方案放在一张表里对比:
| 方案 | 请求数 | 多色支持 | 清晰度 | 维护成本 | 适用场景 |
|---|---|---|---|---|---|
| CSS雪碧图 | 少 | 好 | 依赖倍图 | 中 | 位图图标、旧项目、逐帧动画 |
| IconFont | 少 | 弱 | 好 | 中 | 纯色图标、按钮图标 |
| SVG Sprite | 少 | 好 | 无限清晰 | 低 | 矢量图标、需要动态改色的场景 |
| Data URI | 最多一个文件内嵌 | 好 | 依赖源图 | 高 | 极少图标、首屏关键资源 |
从表里能看出来,雪碧图在现代项目里仍然有价值,尤其面对位图、多色彩、复杂渐变图标时,SVG和IconFont都无法完全替代。IconFont只能做纯色图标,渐变和多色就必须依赖SVG或位图;SVG对复杂位图场景无能为力,因为照片、皮肤纹理、环境光效根本没法矢量化。
7.2 现在仍然推荐雪碧图的场景
我个人目前会在两种场景下优先推荐雪碧图。第一种是位图图标居多且数量庞大的老业务系统,全量切换到SVG成本较高,保留雪碧图并接入自动构建是性价比最高的过渡方案。第二种是需要逐帧动画的运营场景,比如活动页里的角色走路、火焰闪烁、礼盒打开,这类动画本质是连续帧序列,用雪碧图加CSS动画是最稳妥的做法。
HTTP/2的好处也有限制。很多老服务器、部分CDN节点还是HTTP/1.1,内网系统更常见。即使上了HTTP/2,大量小图片还是会带来TCP连接管理和缓存命中方面的额外开销。所以在新项目里,我会优先用SVG Symbol组件库,但绝不会把雪碧图方案一票否决。实际工作里通常是混合使用:公共图标走SVG,运营位图走自动构建的雪碧图,两套体系并存并不冲突。
8. 最后又想起来的一个小习惯
在我维护过的项目里,雪碧图真正好用的阶段,永远是有清晰流程的阶段。无论是手工拼图还是自动构建,一定要保证三样东西齐全:源文件、导出文件、坐标表。很多团队都死在“设计稿更新了,但坐标表没更新”,最后CSS就是错乱的。我建议把坐标表直接写在样式文件头部注释里,或者生成到独立的scss文件,这样任何人接手都能快速知道怎么改。
如果你想从这个方案开始尝试,我的建议是:先从一个超过15个图标的活动页做起,手工拼一张图,算一组坐标,体会一下雪碧图的优缺点;如果项目进入长期迭代,再切换成自动构建。直接在最开始就上完整工具链,容易在配置上花太多时间,反而看不到雪碧图本身带来的收益。这种古老的优化手段,只要用在合适的场景里,还会继续发光。