去年年底我接手了一个后台管理系统的性能优化最直接的问题不是接口慢而是首页一次性拉了40多张小图标。每张图标几KB到十几KB不等F12打开Network面板几十个请求排着队往下跑。当时我第一个念头就是把它们合并成一张精灵图CSS Sprites把几十次请求压缩成一次。这一试不要紧算是把所有能踩的坑都踩了一遍。精灵图这个名字很形象就是把一堆小图标像贴瓷砖一样贴到同一张大图上然后通过CSS的background-position把需要的那个“瓷砖”露出来。底层逻辑不复杂难的是在真实项目里怎么制作、怎么接入、怎么适配高清屏以及遇到各种定位错乱时怎么排查。这篇文章就从我这次案例讲起把完整流程和踩过的坑都摊开说打算做性能优化、或者刚接触精灵图的前端同学可以当一份实操笔记来参考。1. 精灵图的真实价值藏在HTTP请求的账本里1.1 一次请求不只是传一次文件很多新人会觉得图片有40多张每张都很小加起来也才一百多KB为什么加载慢问题不在体积而在请求数量。一次HTTP请求的过程远不只是传文件本身要先查DNS把域名解析成IP然后建立TCP连接如果用的是HTTPS还要再做TLS握手接着发送请求头、等待服务器响应最后才把图片内容传输回来。这一整套往返在网络环境好的情况下也要几十毫秒到一两百毫秒弱网环境下可能到秒级。更要命的是浏览器对同一域名的并发连接数有限制。HTTP/1.1时代同一个域名下面Chrome大约只允许6个并发请求剩下的大量请求只能在队列里等着。40多张小图标意味着这6条连接要来回跑七八轮排队时间会被明显放大。即便现在很多站点已经上了HTTP/2多路复用能缓解一部分排队问题但每个小文件仍然有各自的头部开销、缓存校验、服务端处理成本。把50个文件合并成1个文件等于把这50次“快递”打包成一次物流省掉的是重复的打包、配送、签收环节。1.2 模拟项目X的改造账本请求数从47降到1我用一个模拟项目X的数据来说明这次改造的影响。改造前的页面图标分布大概是顶部导航icon 14个侧边栏菜单icon 12个状态标签icon 9个按钮内嵌icon 7个还有一些提示类小图5个合计47个图标文件。合并前的情况量了一下总大小156KB但首屏阶段光这些图标的请求就产生了47次HTTP请求页面加载完成后Network面板里这部分资源的总耗时在2.3秒左右。合并成精灵图之后请求变成了1个合并后文件大小反而到了189KB但加载耗时间降到了1.5秒左右。这组数据挺有意思体积不仅没减小还变大了原因后面会说但整体加载时间明显下降。在性能优化里请求数量往往比体积更影响真实弱网体验这也是精灵图最核心的收益来源。指标合并前合并后图标文件数量471文件总体积156KB189KB平均单请求耗时约48ms约1500ms首屏可用时间2.3s1.5s1.3 精灵图适合哪些场景不适合哪些场景做完这次优化我对精灵图的适用边界有了更清楚的判断。它最适合的场景是图标数量多、文件体积小、颜色相对简单、在页面里被高频复用。比如后台管理系统的菜单图标、状态图标、社交媒体的分享按钮这些场景下合并收益最明显。反过来以下情况就不太适合硬上精灵图一是大尺寸图片比如Banner、产品展示图拼到一起后文件会变得很大加载反而更慢二是需要频繁换肤、换图标的场景因为每次改动一张整个精灵图都要重新生成缓存也跟着失效三是颜色非常丰富、带大量渐变和半透明的复杂图标这类图用PNG格式体积很难压缩合成之后不仅体积大配合压缩管线也不好优化。所以精灵图不是一个“万能药”而是一个要看账本的工程决策。2. 制作精灵图不复杂难在规范和坐标2.1 手工PS拼图小批量场景的正确姿势一开始我没有上任何自动化工具因为项目里的图标只有47个UI同学给的是PNG源文件我直接在Photoshop里手工拼。操作很简单新建一个透明画布尺寸稍微留足比如1000×800把所有图标按文件夹里的顺序拖进去每个图标单独一个图层图标和图标之间至少留4px间隙。为什么一定要留间隙因为PNG边缘带半透明像素如果两个图标贴得太近CSS定位的时候可能会把相邻图标的边缘一起裁进来导致显示多出一条线或者颜色串位。间隙越大越安全但也不能无限大否则整张图被放大、体积跟着涨。一般4px到8px比较合适。拼完之后需要记录每个图标的坐标。我当时的做法是打开“信息”面板把每个图标的左上角像素位置记下来。这个活在图标少的时候还行50个图标记录起来不算太轻松而且要反复确认有没有记错挺考验耐心。如果你只是临时做个三五张图手工拼完全够用但如果图标会持续增加手工维护就是一场灾难。2.2 自动化生成工具把拼图和坐标输出交给脚本手工拼图最大的问题不是拼图本身而是每次新增一个图标你都可能要重新调整整张图然后重新记录坐标这太不工程化了。所以后续我转向了某个开源的精灵图生成插件这类插件的工作流程基本一致扫描你指定的图标目录自动计算每个图标的位置生成一张合并后的大图同时输出一份CSS映射文件。我用这种方式重建了模拟项目X的图标资源。目录结构大概是这样assets/ icons/ cart.png search.png setting.png ...构建命令跑完后插件会生成assets/ sprite.png sprite.csssprites.css里会自动写好每个图标的类名和坐标。这个时候我才意识到精灵图制作的效率问题其实早就被工具解决得差不多了真正决定体验的是你对截图规范的遵守程度。用自动化工具时要特别注意图标文件命名建议全部用小写字母加连字符比如search-icon.png、setting-icon.png。如果命名里有空格或中文生成出来的类名很可能带转义字符后面写样式的时候特别别扭。另外图标尺寸尽量保持一致的良好习惯比如统一按24×24、48×48来切图能让自动排列更紧凑减少拼图里的浪费空间。2.3 坐标体系用变量管理而不是在代码里写魔法数字拿到生成的CSS后如果没有统一管理页面里会出现大量background-position: -123px -456px这样的魔法数字。看着是能用但后期一旦重新拼图、坐标变了就要全局搜索替换很头疼。我的做法是在预处理器里把坐标抽成变量或者说是构建工具直接生成一份变量文件。比如生成一段像这样的内容$icon-search-bg: -10px -20px; $icon-cart-bg: -50px -20px; $icon-setting-bg: -90px -20px;需要用到的时候直接像下面这样引用.icon { background-image: url(sprite.png); background-repeat: no-repeat; } .icon-search { width: 24px; height: 24px; background-position: $icon-search-bg; }这套体系的好处是重新生成精灵图之后只需要更新变量文件所有用到变量的地方自动生效。如果你用自动生成工具大部分工具都支持输出这种变量文件一定要用起来别让坐标裸奔在业务代码里。3. 工程化接入从一张大图到日常可维护3.1 基础用法background-position 是精灵图的核心语法理解了精灵图原理之后接入本身只是一层薄薄的CSS语法。一个图标的显示区域由元素的width和height决定然后通过background-image指定精灵图再用background-position把精灵图移动到你想要的坐标位置。举个例子。假设精灵图名叫sprite.png搜索图标在整张图的左上角坐标是(0, 0)。那这个图标就写成.icon-search { width: 24px; height: 24px; background-image: url(sprite.png); background-repeat: no-repeat; background-position: 0 0; }如果搜索图标的坐标是(-50px, -20px)意思就是“把精灵图往左移动50px、往上移动20px让目标图标刚好落进元素可视区域”。background-position的坐标是相对元素左上角计算的这是精灵图最容易绕晕的地方但其实只要记住“坐标通常填负数、把背景图拉到位”就可以了。3.2 自动化生成CSS让坐标代码从构建流程里直接产出来手工写坐标不仅容易错还违背了“一次构建处处使用”的原则。所以在模拟项目X里我把精灵图生成动作直接接到了构建流程中每次图标目录有变化时跑一条命令自动生成新的精灵图和对应的CSS文件业务组件里只要引用生成出来的类名或变量即可。这个流程看起来有点像这样npm run spritenpm run sprite背后实际执行的是某个开源拼图插件的命令行接口。它会读取assets/icons文件夹里的所有图标拼接图片、计算坐标最后生成sprite.png和sprite.scss。我在项目根目录的构建脚本里加了一条前置命令确保打包之前先同步一次scripts: { build: npm run sprite npm run build:app, sprite: sprite-generator --input assets/icons --output assets/sprite.png --style assets/sprite.scss }这样团队成员只需要把新的图标扔进assets/icons重新跑一次构建页面里新增图标类名就能直接用。这比手动编辑坐标可靠得多避免了“记错坐标导致图标显示错位”的低级错误。3.3 高清屏适配DPR下不能只改background-size有经验的开发都知道Retina屏上普通的精灵图会发虚原因是物理像素是CSS像素的两倍甚至三倍而我们的背景图没有提供对应倍数的清晰图。解决办法是准备2x甚至3x的精灵图然后用background-size做整体缩放。这里最容易踩坑的地方是background-size设的并不是“图标显示尺寸”而是“整张精灵图的显示尺寸”。假设原始精灵图是600×400其中某个图标占据48×48但我们希望它在页面上显示为24×24也就是2倍图场景下缩放为一半。此时background-size应该设置成300px 200px等于把整张图缩小到一半原图标自然也从48×48变成24×24。如果background-size写成24px 24px整张图会被强行缩成一个很小的方块定位铁定错乱。再看坐标问题因为整张图被缩放了一半background-position的计算也要跟着变。原图搜索图标坐标如果是(-100px, -80px)缩放一半之后实际定位应该写(-50px, -40px)。这个换算很容易忘我见过不少项目在2x精灵图上把坐标原样搬上去结果图标显示错位、漏边一检查才发现是没算缩放系数。.icon { background-image: url(sprite2x.png); background-size: 300px 200px; /* 原始精灵图宽高的一半 */ } .icon-search { width: 24px; height: 24px; background-position: -50px -40px; /* 原坐标(0,0)对应显示尺寸换算后的位置 */ }更稳妥的做法是让自动化工具直接根据2x命名规则生成一份单独的缩放样式或者把换算公式写进SCSS函数而不是靠人肉换算。4. 实际项目里的四个坑我踩完挨个说4.1 背景定位偏移1px我从代码一路查到截图第一次在模拟项目X里接入精灵图时页面刚上线就有人反馈“这个搜索图标在Retina屏上边缘被切掉了一点。”我第一反应是background-position写错了翻来覆去检查代码发现坐标和生成的CSS完全一致。接着怀疑background-size换算出了问题改了缩放比例后其他图标显示正常只有部分图标还是差1px左右。排查到最后发现是切图环节的锅某个图标原始宽度是25px不是偶数。开发时我们按1.5倍缩放处理坐标计算经过四舍五入导致显示时右侧有一条相邻图标的边缘混进来。解决方法是把这类图标统一调整为偶数尺寸后重新切图并且给所有图标留8px间隙让一点误差也不容易串色。这次排查给我的经验是精灵图的问题不总在CSS源头可能在PSD切图阶段。如果定位偏移呈“周期性”优先检查图标本身尺寸和缩放系数如果偏移不规律大多数时候是坐标记录错误。4.2 忘记加 background-repeat: no-repeat整块“花屏”这个坑非常基础但也很容易在合并多组精灵图时被复制粘贴漏掉。元素的宽度和高度如果大于背景图的目标图标区域默认情况下背景图会平铺那种“一整张拼接图平铺成马赛克”的画面我只能用“花屏”来形容。问题发生时开发者工具的Styles面板里能看到background-repeat这一项默认是repeat。只要把精灵图样式基类里的background-repeat: no-repeat写清楚就基本能避免。我一般会把下面这段作为每个精灵图基类的标配.icon { background-repeat: no-repeat; background-position: 0 0; }然后在每个具体图标类里只覆盖width、height和background-position。这样即使某个图标忘了单独设置也不会出现平铺异常。4.3 懒加载场景下一张大图反而破坏了加载节奏精灵图最大的优势是一次请求但如果你把全站的图标都合成一张大图也会带来一个麻烦首屏可能只需要3个图标但是浏览器必须把整张精灵图下载下来才能显示尤其当精灵图已经到了几百KB甚至上兆时懒加载就形同虚设了。我在某个移动端页面就吃过这个亏。当时把整个H5页面的所有图标合成了一张300多KB的精灵图首屏Visible部分其实只有五六个小图标但整张大图的下载时间拖慢了首屏呈现。后来我把图标按模块拆分首屏相关的图标单独合成一张非首屏模块再合另一张甚至对极少使用的全屏弹窗图标用Base64单独内联。这个“拆分雪碧图”的做法建议做成规范每个页面只合并当前页面用到的图标全局公共的、少量高频图标才可以考虑全站合并。4.4 精灵图体积没减反增透明空白是罪魁祸首前面说过模拟项目X合并后的体积从156KB涨到了189KB这里面很大一部分是透明区域造成的。PNG对同色连续区域压得还不错但图标之间存在大量透明位置这些透明像素也要占用存储所以一张排列松散的精灵图体积很可能比各个小图加总还大。应对方法有三个。第一拼图时尽量紧凑工具一般会自动紧凑排列但手工拼图时要习惯开参考线对齐。第二用专门的压缩工具处理合并后的成图比如转成经过有损压缩的WebP格式体积能再降不少前提是兼容性允许。第三也是最容易被忽略的减少精灵图的“一图全家桶”倾向别把所有图标毫无关系地堆在一起按功能分组每组单独合成体积和缓存控制都会更灵活。5. 精灵图还能活多久和更现代方案的取舍5.1 iconfont、SVG Sprite、Base64各有各的活法做了这个案例之后我一直在重新思考精灵图的定位。技术圈里涌现了不少替代方案但它们之间并不是简单的谁取代谁更像各管一摊。方案请求数清晰度维护成本适用场景PNG精灵图1靠多倍图适配略繁琐中等需重新生成老浏览器兼容要求高的项目iconfont1字体文件单色高清放大无压力高新增图标要转字体纯色图形、需要换色场景SVG Sprite1SVG合并文件矢量无限清晰高但更现代支持SVG的现代浏览器/移动端Base64内联0随CSS加载取决于原图低极少数固定图标、动态拼接5.2 我现在的选择原则经历了这次精灵图改造我对选型有了更务实的判断。如果项目明确支持现代浏览器我会优先用SVG Sprite它不是位图、缩放不糊、还能CSS控制颜色。但对一些需要兼容老内核的管理系统、嵌入场景PNG精灵图依然是一套非常稳妥的方案因为它不依赖额外字体或SVG特性兼容性极佳。iconfont更适合纯色图标但字体图标在抗锯齿和渲染上偶尔有小瑕疵。Base64则用来处理“绝对不会变”的三五个小图标省一次请求。我的选择原则可以概括成几句话追求性能和现代性用SVG Sprite追求稳定和兼容用PNG精灵图纯色小图标多且需要变颜色考虑iconfont极少用的图标才用Base64。5.3 项目是艘老船先加固船底再换发动机把模拟项目X的精灵图改造完整复盘一遍我最大的体会是性能优化不能只看单点指标要盯住请求数、体积、缓存失效、团队维护成本这组账一起算。老项目就像一艘老船直接换发动机风险太大但把几十张小图合并成一张精灵图等于把容易漏水的地方先堵上是相对稳妥又收益明显的动作。最后分享一个我现在还在用的习惯每次做页面优化时先打开Network面板按“请求数”排序把小于50KB、数量多、重复率高的资源挑出来问问自己“能不能合并、能不能内联、能不能缓存”。这个习惯其实比认识多少工具都重要。如果你手头正好也有一个图标满天飞的项目不妨从合并第一次精灵图开始慢慢把性能账算明白。
阅读完成 · 觉得有帮助?