Web效能優化之瘦身祕笈

by.Genesis發表於2021-03-01

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');
}

?剔除多餘的字型

在一個字型檔案中不是所有字型都會使用到,特別是在使用圖示字型的時候

裡面有很多圖示是我在專案中沒有用到的,這種時候就需要編輯字型檔案,刪除那些沒用上的字型

百度有個線上字型編輯工具 http://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="http://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>

相關文章