Web性能優化之瘦身秘笈

Web 傳輸的內容當然是越少越好,最近一段時間的工作一直致力於 Web 性能優化,這是我近期使用過的一些縮減 Web 體積的手段

這些手段主要是為了減少 Web 傳輸的內容大小,只有乾貨

CSS

🐛刪除無用的樣式

在使用 UI 庫的時候,UI 庫提供的樣式並不是所有的都會使用到

例如一個 button 組件一般都會提供 default/primary/success/warning/danger 五顏六色好幾款樣式

但我們實際一個項目中也許只會用到其中的一兩種,為了減少樣式表的體積,需要將那些沒有使用的樣式挑選出來刪除掉

使用 uncss 工具來刪除無用的樣式

該工具提供有在線版,只需要複製自己的 HTML 以及 CSS,點擊按鈕就可以生成精簡後的樣式

另外也可以通過瀏覽器工具 Coverage 挑選出未使用的樣式,如下圖

coverage

經過分析得出每個文件未使用樣式的百分佔比,其中紅色標記的為未使用到的樣式,從下圖中可以看到具體未使用到的樣式有哪些

coverage-style

⚠️ 上面兩種方法都是通過樣式規則的選擇器在頁面上查找元素,如果能找到對應的元素,則說明該樣式規則有被使用
隨著在頁面上進行各種操作,該百分比可能會降低,因為有些樣式會在某些操作執行之後才會被使用到,比如 :hover 偽類相關的樣式,在滑鼠移入元素之前不會被標記為已使用的
所以,這兩種方式都有一定的局限性,並不是挑選出的樣式就一定是沒有用的,也許某個樣式是在用戶執行相當複雜的操作後才會起作用,需要嚴格測試

🐛刪除重複的樣式

CSS 全名 層疊樣式表(Cascading Style Sheets),對同一個元素多次指定同一個樣式只會讓優先順序高的覆蓋優先順序低的

在樣式規則的選擇器完全相同的情況下(比如這裡 .selector-1 > .selector-2 和 .selector-1 > .selector-2 是完全相同的),被覆蓋的樣式可以安全地刪除,如下

/* Before */
.selector-1 > .selector-2 {
  display: none;
  width: 200px !important;
}

.selector-1 > .selector-2 {
  display: block;
  width: 100px;
}

/* After */
.selector-1 > .selector-2 {
  width: 200px !important;
}

.selector-1 > .selector-2 {
  display: block;
}

通過瀏覽器的開發者工具可以輕鬆看到哪些樣式被覆蓋了

uncss

⚠️ 在選擇器不相同的時候,也有可能會匹配到同一個元素,這個時候本條規則並不適用,需要注意

⚠️ 有時候同一個樣式屬性反覆出現只是為了兼容一些舊瀏覽器,也需要注意

🐛使用複合屬性

有些樣式屬性可以合併為一條,比如

/* Before */
.selector {
  flex-direction: column;
  flex-wrap: wrap;
}

/* After */
.selector {
  flex-flow: column wrap;
}

合併之後位元組數減少

🐛刪除過時的樣式

有些樣式是為了兼容一些老舊瀏覽器而提供的,當前已經不需要再兼容這些瀏覽器了,對應的樣式可以刪除掉,比如如下這些

- header {
-   display: block;
- }

ℹ️ 使用 autoprefixer 刪除過時的瀏覽器廠商前綴(比如 -moz-,-ms- 這些)

🐛利用繼承

部分樣式會繼承給後代元素,後代元素沒有必要再寫一遍,除非是確實需要覆蓋的

之所以會有這條是因為之前在項目中看到隨處可見的 box-sizing: border-box 屬性其實可以主動設置為繼承

*,
*:before,
*:after {
  box-sizing: inherit;
}

html {
  box-sizing: border-box;
}

這樣所有元素都會繼承這個屬性,不用反覆定義

🐛提取公共樣式

將多個規則集中相同的樣式提取出來,並使用群組選擇器放在一起,比如

/* Before */
.badge {
  background-color: orange;
  border-raidus: 5px;
  color: #fff;
  font-size: 13px;
}

.label {
  background-color: orange;
  border-raidus: 5px;
  color: #fff;
  font-size: 12px;
}

/* After */
.badge,
.label {
  background-color: orange;
  border-raidus: 5px;
  color: #fff;
}

.badge {
  font-size: 13px;
}

.label {
  font-size: 12px;
}

csscss 可以用來分析冗餘的 CSS 程式碼

這是一個 Ruby 工具,使用前需要先安裝 ruby1.9 或以上版本

這個工具只是用來分析冗餘樣式的,並不會主動刪除樣式,需要自己手動調整

⚠️ 在 CSS 中,樣式的先後順序是有意義的,隨意移動樣式規則可能會讓樣式出現問題,需要經過嚴格測試

ℹ️ csso 可以用來刪除冗餘,合併樣式規則

🐛壓縮 CSS

壓縮主要是刪除無用的空白和注釋

使用 cssnano 壓縮 CSS

該工具提供 在線版

ℹ️ cssnano 自帶 autoprefixer 工具幫助清理瀏覽器廠商前綴

JavaScript

🐛刪除無用的 JavaScript

瀏覽器的 Coverage 工具也能挑選出未使用的 JavaScript 程式碼,不再重複

⚠️ 同樣的,挑選出來的程式碼也不一定全是無用的,需要經過仔細測試

🐛刪除歷史遺留程式碼

同 CSS 一樣,JavaScript 也有一些程式碼是為了兼容舊瀏覽器而存在的

像 es5-shim.js 就是為了給那些不支援 ES5 的瀏覽器準備的,現在已經可以放心地從項目中去掉了,目前全球使用支援 ES5 的瀏覽器的用戶佔比高達98%

另外一些框架或庫的新版本通常將不會包含那些兼容舊瀏覽器的程式碼,需要時保持更新即可,比如用 jQuery3.0 替換 jQuery1.12

🐛刪除功能重複的插件

一個項目經手的人多了之後,會出現一些匪夷所思的膨脹,比如同一個項目中引入了好幾個功能相似的插件

找出相關程式碼,根據需求確定真正需要使用的插件,去掉其它多餘的

⚠️ 此條需要經過嚴格的測試

🐛使用新的 API

隨著 Web 標準的豐富以及瀏覽器的更新換代,越來越多的功能可以通過設備/瀏覽器原生的 API 來實現

比如 IntersectionObserver 可以用來探測 DOM 元素是否位於窗口可視區域內,這就不需要藉助插件來實現這些功能了

相應的插件程式碼可以從項目中安全地刪除,或者只為那些老舊設備/瀏覽器提供

🐛壓縮 JavaScript

主要是刪除沒用的空白和注釋等等

使用 Terser 來壓縮 JavaScript,通過 NPM 安裝 npm install terser -g

執行命令 terser main.js -o main.min.js -c -m

字體

🐛選擇合適的格式

常用的字體格式有如下這些

WOFF2/WOFF

Web 開放字體格式(Web Open Font Format),載入快,壓縮率高

WOFF2 是 WOFF 的升級版本,壓縮率更高

SVG/SVGZ

矢量圖形字體(Scalable Vector Graphics Font),僅有少部分瀏覽器支援(比如 iOS Safari 4.1-)

EOT

Embedded Open Type,IE 獨佔

TTF/OTF

OpenType Font 和 TrueType Font,瀏覽器支援範圍最廣的格式

根據目標設備選擇合適的字體格式,不同的字體格式兼容的瀏覽器也是不一樣的

下圖是圖一套字體的不同文件格式的大小對比

fonts

我們應該優先選用壓縮率更高的 WOFF2 文件格式,如果瀏覽器不支援該格式,降級到 WOFF,甚至 OTF/TTF

下面是完整定義字體的方式,瀏覽器會根據優先順序下載自身能識別但體積相對更小的字體文件

@font-face {
  font-family: 'My Font';
  src: url('path/my-font.eot');
  src: url('path/my-font.eot?#iefix') format('embedded-opentype'),
       url('path/my-font.woff2') format('woff2'),
       url('path/my-font.woff') format('woff'),
       url('path/my-font.ttf')  format('truetype'),
       url('path/my-font.svg#svgFontName') format('svg');
}
  • TTF/OTF 的兼容性僅比 WOFF 多出一點點而已,已經到了可以忽略不計的地步
  • SVG 字體和 EOT 是針對部分舊版本瀏覽器的兼容方案,目前已經沒有太大使用的價值

所以上面的字體定義也可以精簡為如下,基本可以滿足市面上的主流瀏覽器

@font-face {
  font-family: 'My Font';
  src: url('path/my-font.woff2') format('woff2'),
       url('path/my-font.woff') format('woff');
}

🐛剔除多餘的字體

在一個字體文件中不是所有字體都會使用到,特別是在使用圖標字體的時候

裡面有很多圖標是我在項目中沒有用到的,這種時候就需要編輯字體文件,刪除那些沒用上的字體

百度有個在線字體編輯工具 //fontstore.baidu.com/static/editor/index.html 可以打開並編輯字體以及保存為其它格式

這是經過我編輯過後的文件大小對比,文件大小差距很大,確實用到的字體比較少

fonts-edit

影像

在 Web 網頁中,影像的體積佔了大頭,減少影像可以大幅增加性能

🐛選擇適合的影像格式

不同文件格式的影像其文件大小,影像品質是不一樣的,根據具體情況選擇合適的影像格式

常用 Web 影像格式

格式 透明 動畫 說明 瀏覽器支援
GIF ✔️ ✔️ 顏色較少
JPEG 有損格式,常用於照片
PNG ✔️ 無損
WebP ✔️ ✔️ 支援無損/有損壓縮,比JPEG,PNG和GIF更好的壓縮效果 較新
AVIF ✔️ ✔️ 比WebP,JPEG,PNG和GIF更好的壓縮效果 最新
JPEGXL ✔️ ✔️ 無損壓縮,更快的解碼和其他各種改進 暫無

一些新的影像格式擁有較高的性能,比如 AVIF 和 WebP

不過這些新的影像格式不是所有瀏覽器都支援,此時可以使用一個 <picture> 元素來包裹 <img> 元素,再通過使用 <source> 元素來為 <img> 元素提供多個備胎資源供其自行選擇

<source> 元素可以有多個,srcset 屬性是必須的(注意是 srcset)

<picture>
  <source type="image/avif" srcset="logo.avif">
  <source type="image/webp" srcset="logo.webp">
  <img alt="logo" src="logo.png">
</picture>

瀏覽器會自行忽略不支援的格式,如果瀏覽器支援 AVIF 格式就使用 logo.avif,如果支援 WebP 格式就使用 logo.webp

如果上面倆都不支援,就會使用 logo.png,不支援 <picture> 元素的瀏覽器會直接顯示 <img> 元素

值得一提的是 <picture> 元素內部必須包含一個 <img> 元素,否則影像不會顯示(因為 <picture> 元素並不是一個獨立顯示的元素,而是為 <img> 元素服務的)

還有 <img> 元素始終都不應該忘記的 alt 屬性,當任何影像格式都無法顯示或者影像下載失敗的時候,至少還能顯示替代的文字說明

要在 CSS 中使用這些新的格式通常用 JavaScript 來判斷瀏覽器是否支援

創建一個 Image 對象,然後載入一張較小的需要判斷格式的影像,如果載入成功則說明瀏覽器支援該格式,下面是 Google 提供的判斷瀏覽器是否支援 WebP 的方法

const img = new Image()
img.onload = img.onerror = () => {
  document.body.classList.add(img.height > 0 ? 'webp' : 'no-webp')
}
img.src = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='

如果該瀏覽器支援,則給 <body> 元素添加 webp 類,否則添加 no-webp,在 CSS 中就可以這樣寫

.webp .logo {
  background: url(./logo.webp);
}

.no-webp .logo {
  background: url(./logo.png);
}

這樣就能根據該瀏覽器是否支援 webp 格式載入不同格式的影像了

ℹ️ 轉換影像格式可以使用 Sqoosh,在影像大小和品質之間手動調整權衡

🐛使用響應式影像

在 CSS 中使用媒體查詢結合 image-set 可以依據設備/瀏覽器的寬度以及像素比顯示不同解析度的影像

ℹ️ 為了方便一眼看出來,影像的名稱包含了影像的真實寬度,比如 logo-240.png 表示這張影像寬度為 240 像素

.logo {
  background-image: url(./images/logo-120.png);
  background-image: -webkit-image-set(url(./images/logo-120.png) 1x, url(./images/logo-240.png) 2x);
  background-image:         image-set(url(./images/logo-120.png) 1x, url(./images/logo-240.png) 2x);
}

@media (min-width: 600px) {
  .logo {
    background-image: url(./images/logo-240.png);
    background-image: -webkit-image-set(url(./images/logo-240.png) 1x, url(./images/logo-480.png) 2x);
    background-image:         image-set(url(./images/logo-240.png) 1x, url(./images/logo-480.png) 2x);
  }
}

@media (min-width: 1200px) {
  .logo {
    background-image: url(./images/logo-480.png);
    background-image: -webkit-image-set(url(./images/logo-480.png) 1x, url(./images/logo-960.png) 2x);
    background-image:         image-set(url(./images/logo-480.png) 1x, url(./images/logo-960.png) 2x);
  }
}

根據 移動優先 的原則,使用媒體查詢應該從小往大

ℹ️ 不支援 image-set 的瀏覽器將會使用前面定義的傳統 url 路徑

⚠️ image-set 目前還在草案中,需要添加對應的瀏覽器廠商前綴,示例已添加

⚠️ Safari 只支援 url 路徑和 1x/2x 這樣的設備像素比

<img> 元素通過其新增的 srcset 和 sizes 屬性來實現響應式影像

<img alt="avator"
  src="avator-120.jpg"
  srcset="avator-120.jpg 120w, avator-240.jpg 240w, avator-480.jpg 480w"
  sizes="(max-width: 600px) 120px, 240px">

srcset 屬性為影像提供多個源供設備/瀏覽器自行選擇,其中影像路徑後面的 120w/240w/480w 用於告訴設備/瀏覽器每張影像的實際寬度

sizes 屬性為影像提供渲染尺寸,可以通過媒體查詢提供多個渲染尺寸以及一個默認尺寸(這裡 240px 就是默認的渲染尺寸)

設備/瀏覽器會根據這些資訊選擇最合適的影像載入顯示

當設備/瀏覽器寬度在 600 像素以下時影像將佔據 120 像素的寬度,此時如果設備像素比為 1 則顯示 avator-120.jpg,如果設備像素比為 2 則顯示 avator-240.jpg,為 4 則應該顯示 avator-480.jpg

當設備/瀏覽器寬度大於 600 像素的時候影像將佔據 240 像素的寬度,此時如果設備像素比為 1 則顯示 avator-240.jpg,如果設備像素比為 2 則顯示 avator-480.jpg

瀏覽器寬度 設備像素比 顯示哪張影像
<= 600px 1 avator-120.jpg
2 avator-240.jpg
4 avator-480.jpg
> 600px 1 avator-240.jpg
2 avator-480.jpg

ℹ️ 設備像素比也有可能是小數,比如 1.5,設備/瀏覽器會選擇它自己認為最合適的那張影像來顯示

ℹ️ 其中 src 屬性是給不支援 srcset 和 sizes 屬性的瀏覽器提供的

🐛影像壓縮

有些格式的影像往往還會包含一些沒有用的資訊,清理掉這些資訊有助於縮小影像體積

這通常使用工具來進行

使用 imagemin 壓縮影像

🐛影像懶載入

頁面上有很多影像我們一開始是看不到的,有的在我們滾動頁面之後才會出現在螢幕上,又有的在某個對話框彈出後才能看到

對於這類影像,我們可以推遲它們的載入時機,等到它們需要真正展示在螢幕上的時候才載入,而不是在頁面一開始時就載入,這將大大節省頁面初始化時載入的資源大小

使用瀏覽器原生的懶載入方案,這非常簡單,只需要給影像元素添加一個 loading="lazy" 屬性即可

<img alt="avator" loading="lazy" src="avator.jpg">

目前該屬性只得到一部分瀏覽器的支援,不支援的瀏覽器會忽略

caniuse/loading-lazy-attr

該屬性的 polyfill

還可以使用 JavaScript 插件,市面上有不少這類型的插件

⚠️ 引入一個插件會增加 JavaScript 的程式碼量,但是延遲了部分影像的載入時機,具體需要權衡

🐛使用其它方案替換影像

減少影像最好的辦法就是沒有影像

使用 SVG 替換影像

warning

該影像格式為 png 大小為 1.46kb

如果使用 SVG 來表示同樣的影像則只有 300 多位元組,體積大幅度減小

<svg viewBox="0 0 100 100" xmlns="//www.w3.org/2000/svg">
  <path d="M50 92.5H6.09a4.47 4.47 0 01-3.87-6.71l22-38 22-38a4.46 4.46 0 017.74 0l22 38 22 38a4.47 4.47 0 01-3.87 6.71z" fill="##ff7f00"></path>
  <path d="M57.41 78.1A7.41 7.41 0 1150 70.7a7.39 7.39 0 017.41 7.4zm-2.14-14.89H44.81l-1.72-36h13.82z" fill="#fff"></path>
</svg>

SVG 既可以改顏色,也可以任意放大縮小

SVG 可以使用 SVGO 來優化

使用純樣式替換影像

比如下面這個 loading 效果就是純樣式寫的

相對於影像來說,純程式碼的位元組數就少得多了

@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

.loading {
  animation: spin 1.2s infinite linear;
  border: 4px solid rgba(0, 0, 0, 0.1);
  border-left-color: #46aaff;
  border-radius: 50%;
  height: 30px;
  width: 30px;
}
<div class="loading"></div>