iT邦幫忙

2021 iThome 鐵人賽

DAY 7
0
Modern Web

今晚,我想來點 Web 前端效能優化大補帖!系列 第 7

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


上一篇
Day06 X 圖片最佳化
下一篇
Day08 X 瀏覽器架構演進史 & 渲染機制
系列文
今晚,我想來點 Web 前端效能優化大補帖!30

尚未有邦友留言

立即登入留言