Day07 X Image Sprites

经过昨天的一番折腾,我想读者们都对基本的图片优化稍有概念了,今天要介绍的优化技巧其实严格来说也算是图片的优化范畴,但是跟优化图片本身的大小无关,因此我选择独立拉出一篇来介绍。

What is Image Sprites ?

Sprites ? 难道是那个...「雪碧,透心凉!」吗...?

喂!当然不是啦!

我们先来看看这个网站关於图片的网路请求状况。(对,又是这个网站。)

你会发现页面发出了 6 个 network requests,聪明的你应该可以发现问题所在:「这只是一个简单的网站,就需要一张图发一个 request 了,那如果图片超级多的网站不就需要发出超级多 requests?(当然可以做快取,但 Caching 不在今天的讨论范围内)」

是的,这样的确需要发出很多网路请求。聪明的你立刻举一反三:「那有没有可能...将很多张小图合并为一张大图,这样只需要一次请求,浏览器载入大图後再到前端做切割呢?」

Wow,你太神了!这种将许多图片合并为一张大图,载入後再进行切割以节省 banswidth 的方法就是今天要介绍的 Image Sprites,也被一些人称作「雪碧图」。

今天会介绍点阵图的 Sprites (又被称作 CSS Sprites) 与 SVG Sprites 两种方式,CSS Sprites 主要会介绍 gif, jpeg 与 png 格式的使用方式,SVG 图片因为使用方式与其他格式的图片稍有不同,所以通常被独立出来并被称为 SVG Sprites,不过其实底层的概念都是相同的,都是透过合并图片的方式来减少网路请求。

点阵图的 Sprites (CSS Sprites)

合并成大图後,要怎麽使用?

前面提到 Sprites 是在前端切割大图以得到要使用的小图的技术,但其实它并不需要真的将图片去做切割,而是透过 background-image 与 background position 这些 CSS properties 来找到小图在大图中的位置。

举例来说,今天你拿到一张世界地图

你可以透过台湾在整个世界地图上的位置(经纬度)来找到它

让我们看看 W3C 的范例:

<!DOCTYPE html>
<html>
<head>
<style>
    #home {
      width: 46px;
      height: 44px;
      background: url(img_navsprites.gif) 0 0;
    }

    #next {
      width: 43px;
      height: 44px;
      background: url(img_navsprites.gif) -91px 0;
    }
</style>
</head>
<body>
    <img id="home" src="img_trans.gif" width="1" height="1">
    <img id="next" src="img_trans.gif" width="1" height="1">
</body>
</html>

可以从两个 img tag 的 src attribute 看到它们使用的是同一张图片,再看看 CSS 的 style 可以发现它们透过 background position 这个 property(范例是使用 background 的缩写方式)来从大图中定位出要使用的图片的位置。

所以,我得自己去组图,再自己去计算小图在大图中的位置吗?

不用那麽麻烦的,我们可以透过第三方的工具如 Toptal 的 CSS Sprites Generator 或是 CSS Sprite Generator 来帮我们建立组好的图,另外这些工具也会直接提供包含 background position 的 CSS code 给我们,我们直接拿来使用就可以了。

CSS Sprite Generator 这个工具来简单 demo 一下


它提供了一个包含各大工程师常用社群应用 icon 的 demo sprites image,点选 download 後可以接着下载它组好的 sprites 大图与 CSS code(也可以点选图中的 CSS tab 将 code 复制贴到自己的专案上)

接着我们在 HTML 中加入

<body>
    <i class="sprite sprite-github"></i>
    <i class="sprite sprite-gmail"></i>
    <i class="sprite sprite-linkedin"></i>
    <i class="sprite sprite-stackoverflow"></i>
    <i class="sprite sprite-tumblr"></i>
    <div class="container">
      <i class="sprite sprite-twitter"></i>
    </div>
  </body>

这边我故意加了一个 div 在最後一个 icon 上面,方便证明从 Sprites 拿到的小图也可以个别排版。

再来将 CSS 也加入

.sprite {
  background-image: url(spritesheet.png);
  background-repeat: no-repeat;
  display: block;
}

.sprite-github {
  width: 30px;
  height: 30px;
  background-position: -5px -5px;
}

.sprite-gmail {
  width: 30px;
  height: 30px;
  background-position: -45px -5px;
}

.sprite-linkedin {
  width: 30px;
  height: 30px;
  background-position: -5px -45px;
}

.sprite-stackoverflow {
  width: 30px;
  height: 30px;
  background-position: -45px -45px;
}

.sprite-tumblr {
  width: 30px;
  height: 30px;
  background-position: -85px -5px;
}

.sprite-twitter {
  width: 30px;
  height: 30px;
  background-position: -85px -45px;
}

.container {
  background: yellow;
  margin-top: 40px;
}


可以看到各个小 icon 被显示在网页上的正确位置,从 Devtool 也可以看到用了 Sprites 後只发起一个网路请求去抓取大图而已,而这个大图当然也可以被浏览器 Cache,让载入速度更快。


SVG Sprites

SVG 图也可以使用 Sprites 技术吗?当然可以,虽然它的使用方式与点阵图的 Sprites 不太一样,但背後的原理是差不多的。

为了快速 Demo,我选择透过 svgsprit 这个第三方服务来帮我们产生 SVG 的 Sprites 图片。

首先可以先到 flaticon 或类似的网站下载一些免费的 SVG 图片,再把下载好的多张 SVG 图档一起拖曳到 svgsprit 里,svgsprit 会自动生成一堆 svg 的 tag,如下图:

接着点选右下角的 Copy Sprites,并在自己练习的 repo 里创建一个叫 sprite.svg 的档案,把复制的内容贴上。

<svg width="0" height="0" class="hidden">
  <symbol xmlns="http://www.w3.org/2000/svg" viewBox="-2 0 511 512" id="svg-1">
    <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="253.5" x2="253.5" y1="0" y2="512">
      <stop offset="0" stop-color="#2af598"></stop>
      <stop offset="1" stop-color="#009efd"></stop>
    </linearGradient>
    <path d="M85.5 60c0-11.027 8.973-20 20-20h174.89v71.11c0 33.085 26.915 60 60 60h71V290h40V142.855L309.259 0H105.5c-33.086 0-60 26.914-60 60v230h40zm234.89 51.11V67.901l62.887 63.207h-42.886c-11.032 0-20-8.968-20-20zM506.5 401v20c0 50.055-40.707 90.8-90.816 91h-.368c-50.109-.2-90.816-40.945-90.816-91 0-50.18 40.8-91 90.945-91 12.868 0 25.32 2.645 37.008 7.86l-16.297 36.527c-6.527-2.91-13.496-4.387-20.71-4.387-28.09 0-50.946 22.879-50.946 51 0 28.059 22.863 50.898 51 51 21.023-.074 39.102-12.848 46.898-31H419.5v-40zm-361 55.5c0 30.602-24.898 55.5-55.5 55.5H.5v-40H90c8.547 0 15.5-6.953 15.5-15.5S98.547 441 90 441H56C25.398 441 .5 416.102.5 385.5S25.398 330 56 330h68.5v40H56c-8.547 0-15.5 6.953-15.5 15.5S47.453 401 56 401h34c30.602 0 55.5 24.898 55.5 55.5zM271.121 330h42.316l-62.792 182h-30.446L156.5 330h42.379l36.383 103.95zm0 0" fill="url(#a)"></path>
  </symbol>
  <symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" id="svg-3">
    <path d="M36.985 0H7.963C7.155 0 6.5.655 6.5 1.926V55c0 .345.655 1 1.463 1h40.074c.808 0 1.463-.655 1.463-1V12.978c0-.696-.093-.92-.257-1.085L37.607.257A.884.884 0 0036.985 0z" fill="#e9e9e0"></path>
    <path fill="#d9d7ca" d="M37.5.151V12h11.849z"></path>
    <path d="M48.037 56H7.963A1.463 1.463 0 016.5 54.537V39h43v15.537c0 .808-.655 1.463-1.463 1.463z" fill="#e57e25"></path>
    <path d="M21.459 50.238c0 .364-.075.718-.226 1.06s-.362.643-.636.902-.61.467-1.012.622-.856.232-1.367.232c-.219 0-.444-.012-.677-.034s-.467-.062-.704-.116-.463-.13-.677-.226-.398-.212-.554-.349l.287-1.176c.128.073.289.144.485.212s.398.132.608.191.419.107.629.144.405.055.588.055c.556 0 .982-.13 1.278-.39.296-.26.444-.645.444-1.155 0-.31-.104-.574-.314-.793s-.472-.417-.786-.595-.654-.355-1.019-.533-.706-.388-1.025-.629-.583-.526-.793-.854-.314-.738-.314-1.23c0-.446.082-.843.246-1.189s.385-.641.663-.882.602-.426.971-.554.759-.191 1.169-.191c.419 0 .843.039 1.271.116s.774.203 1.039.376c-.055.118-.118.248-.191.39l-.205.396c-.063.123-.118.226-.164.308s-.073.128-.082.137c-.055-.027-.116-.063-.185-.109s-.166-.091-.294-.137-.296-.077-.506-.096-.479-.014-.807.014c-.183.019-.355.07-.52.157s-.31.193-.438.321-.228.271-.301.431-.109.313-.109.458c0 .364.104.658.314.882s.47.419.779.588.647.333 1.012.492.704.354 1.019.581.576.513.786.854.318.781.318 1.319zm4.402 2.817L22.73 42.924h1.873l2.338 8.695 2.475-8.695h1.859l-3.281 10.131h-2.133zm14.807-5.25v3.896c-.21.265-.444.48-.704.649s-.533.308-.82.417-.583.187-.889.233-.608.068-.909.068c-.602 0-1.155-.109-1.661-.328s-.948-.542-1.326-.971-.675-.966-.889-1.613-.321-1.395-.321-2.242.107-1.593.321-2.235.511-1.178.889-1.606.822-.754 1.333-.978 1.062-.335 1.654-.335c.547 0 1.058.091 1.531.273s.897.456 1.271.82l-1.135 1.012c-.219-.265-.47-.456-.752-.574s-.574-.178-.875-.178c-.337 0-.658.063-.964.191s-.579.344-.82.649-.431.699-.567 1.183-.21 1.075-.219 1.777c.009.684.08 1.276.212 1.777s.314.911.547 1.23.497.556.793.711.608.232.937.232c.101 0 .234-.007.403-.021s.337-.036.506-.068.33-.075.485-.13.269-.132.342-.232v-2.488h-1.709v-1.121h3.336z" fill="#fff"></path>
    <path d="M45.5 22v-6h-6v2h-6v-4h-10v4h-6v-2h-6v6h6v-2h3.548c-4.566 2.636-7.548 7.588-7.548 13a1 1 0 102 0c0-5.246 3.229-9.999 8-11.995V24h10v-2.995c4.771 1.997 8 6.75 8 11.995a1 1 0 102 0c0-5.412-2.982-10.364-7.548-13H39.5v2h6zm-30-2h-2v-2h2v2zm16 2h-6v-6h6v6zm10-4h2v2h-2v-2z" fill="#c8bdb8"></path>
  </symbol>
</svg>

上面这段标签语法其实就是 SVG 的 sprites 大图,有注意到 svg tag 旁的 width 与 height 都被设为 0 吗?所以说直接把这段标签放到 HTML 是不会有任何东西被显示的。

那我们要怎麽使用小图呢?

接着在 HTML 中写入:

<svg class="icon">
  <use xlink:href="sprite.svg#svg-2"></use>
</svg>
<svg class="icon">
  <use xlink:href="sprite.svg#svg-3"></use>
</svg>

这就是 SVG Sprites 的使用方式,在 xlink:href attribute 中写入 sprite 的档案路径(刚刚我们把 sprite 存成了 sprite.svg),井字号後则是指定小图的 id。回过头看 sprite 的标签可以看到有两个 symbol tag,每个 symbol tag 都有 id,这些 id 其实就是我们刚刚上传到 svgsprit 的图档名称,也是帮助定位出小图的关键。

最後再加上 CSS class

.icon {
  width: 50px;
  height: 50px;
  margin: 0.5em;
}

底下两个 SVG icon 就成功显示出来了,从 Devtool 也可以看到只载入了一个 SVG 的图档。

SVG Sprites 跟点阵图的 Sprites 差在哪?

相信聪明的你一定发现了,SVG Sprites 不用算位置,透过 id 就能找到想要的小图。SVG 还有一个很大的特性,就是可以透过 CSS 属性例如 fill 修改图档颜色,整体弹性比点阵图高很多。

刚刚我们把 Sprites 的标签语法放在独立的档案 Sprite.svg 里,虽然说也可以直接放在 HTML 里,不过刚刚切分成独立档案的作法我认为是比较容易维护的写法。另外,独立的 Sprite 图档也可以被浏览器快取。

如果使用框架进行元件化的开发,还可以实作出 Component 来包装 SVG Sprites,以 React 为例:

只需要传入必要的资讯,其他比较丑且不易阅读的细节就可以隐藏起来。

今天 Demo 的 Github Repo: https://github.com/kylemocode/it-ironman-2021/tree/master/image-sprites-demo

关於 SVG Sprites,还可以做什麽优化?

产生 Sprites 图档之前,先针对每张 SVG 做最佳化

一个 SVG 图档有很多的属性,通常 SVG 的优化服务会利用拔除一些不必要的属性减少图档大小来做最佳化。另外如果 SVG 图档的 fill 属性已经被设定,之後将会没办法客制化的透过 CSS 改成自己要的颜色,所以有些优化工具会把 fill 属性也一并移除。

我们可以把图片丢到如 SVGOMG 这样子的线上服务来最佳化,不过这样似乎有点麻烦,想要更方便一点,可以参考如 svgo 的 command line tool,写好 config 档後就可以批次大量的最佳化 SVG 图档。

利用如 gulp 等自动化工具做 SVG 最佳化与产生 SVG Sprites

基本上各种自动化工具都可以找到其他人已经写好的 plugin,我们可以把 SVG 相关的 workflow(最佳化 -> 产生 Sprites)都整合到开发流程里。例如使用 gulp-svgo 做 SVG 最佳化搭配 gulp-svg-sprite 拿优化後的图片产生 Sprites 图。

(当然,CSS Sprites 也可以与自动化工具做整合喔!)


Sprites 的优缺点与适用场景

昨天的内容有提到图片在网页的资源中占了非常大的比例,那是不是可以只要是图片就都用 Sprites 来减少网路请求数量啊?

当然不是的,减少网路请求的 trade off 就是单次图片请求的体积会变大。那到底多少图片合并成大图是比较有效益的呢?之前看过一个研究,以浏览器与目前网路的效能来说,图片大小在 200KB 以内的载入速度是不会差太多的,所以可以建议以这个大小作为一个分水岭。

所以,只要合并起来小於 200KB 的图我们都应该使用 Sprites 技术吗?答案依然是否定的。

Sprites 最大的优点当然是藉由节省网路请求的数量来提升效能,那缺点与限制呢?以 CSS Sprites 来说,建议宽度或高度要一致,不然每张小图很容易影响到彼此的背景,这个条件就限制了许多 use case。

再来是导致专案不好维护的问题,想想今天你要新增或是删除小图,不管是 CSS Sprites 还是 SVG Sprites 都得重新制作一次 Sprites 大图,CSS Sprites 小图的 position 甚至有可能会变动,如果还得做昨天介绍的响应式图片,依照不同萤幕尺寸显示不同的照片,嗯...光用想的我就快要疯掉了。

针对 Sprites 技术的适用场景,下个小结论:
Sprites 是把双面刃,虽然可以减少网路请求,不过要小心合并的大图不能变得太肥大而反而成为效能瓶颈,举例来说:

  • menu 上的各个 icon
  • 赞助商的企业 Logo
  • 保留动画和状态,例如:若 icon 有 hover 效果想要换一张图片就不需要再另存图片(因为对於浏览器来说它们实际上是同一张图)

都有机会是 Sprites 可以发挥效用的场景。不过在使用 Sprites 技术前最重要的是思考到底有没有必要为了节省这些网路请求而需要用到 Sprites ,也可以想想是不是有更适合的解决方案。更进一步还要考虑到程序的维护性,如果是比较常变动的部分,使用 Sprites 可能就是给自己找麻烦罗!


Sprites 技术过时了吗?

有人说,随着技术的演变,例如 HTTP2 的到来(未来篇章会介绍)、网路速度与浏览器的升级,或是发展出其他的图片技术,雪碧图已经没有存在价值且被弃用了。

Reference: https://www.v2ex.com/t/619264

面对这个问题,我的回答是:「的确,随着时间推移,慢慢地出现了更好的解决方案,但要知道技术的汰换通常是渐进式的,在这个过程中仍然会有许多网站仍在使用旧的技术,也就是说我们得知道怎麽维护旧有的程序。因此我认为此时此刻身为一个前端开发者,还是有必要理解这些技术,即便未来可能会被替换掉,也可以从技术演进的过程学到一些知识。」

像淘宝至今都还在使用 Sprites 来做一些商标图片的显示呢!

本日小结

尽管 Sprites 不是一个非常新潮的技术,也有许多声音说它已经过时且不好维护,没有使用的必要。但我个人认为它的原理还蛮值得大家学习的,减少网路请求的确是一个优化的方向,除了 Sprites 以外也可以套用在其他地方,但它的 trade off 就是会让单一请求的大小提高,如果没有控制好,反而会成为效能的瓶颈。

今天分为两个段落介绍了 CSS Sprites 与 SVG Sprites,分别为点阵图与向量图两种图片形式的 Sprites 使用方法,看起来 SVG Sprites 的使用方式比较新也比较方便一点。不过其实 SVG 的使用方式真的很多元,SVG Sprites 未必适合每个场景。今天也没有机会为没碰过 SVG 的读者讲解一些 SVG tag 与 attribute 代表的意义与使用方式,还得麻烦有兴趣的读者再自行深入研究了。

到今天 Assets Optimizations 的章节就告一段落了,明天之後将会进入 Delivery Optimizations & Render Process Optimizations 的章节,每日的文章长度与强度应该会提升一个档次,希望大家能一起撑过去!明天见罗!

References

https://cythilya.github.io/2018/08/20/svg-sprites/
https://spritegen.website-performance.org/
https://www.itread01.com/p/667601.html
https://www.jianshu.com/p/aae36b521563


<<:  DAY28 Aidea专案实作-AOI瑕疵检测(3/4)

>>:  不只懂 Vue 语法:如何透过路由实现跨页面传递资料?

前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day1 系列影片介绍

2021/10/23 更新:新增范例网站以及程序码连结 (影片教学组的头香我抢走罗XD) 大家好!我...

Soundcloud artists can distribute music to other services

New Soundcloud function: artists from the streamin...

不要再用print来debug 了 ...

笔者相信有非常多的朋友很爱用print 列印相关的讯息来当作除错讯息的参考使用,类似下面的做法 笔者...

找LeetCode上简单的题目来撑过30天啦(DAY8)

题号;100 标题:Same Tree 难度;Easy Given the roots of two...