Web 傳輸的內容當然是越少越好,最近一段時間的工作一直致力於 Web 性能優化,這是我近期使用過的一些縮減 Web 體積的手段
這些手段主要是為了減少 Web 傳輸的內容大小,只有干貨
CSS
🐛 刪除無用的樣式
在使用 UI 庫的時候,UI 庫提供的樣式並不是所有的都會使用到
例如一個 button 組件一般都會提供 default/primary/success/warning/danger 五顏六色好幾款樣式
但我們實際一個項目中也許只會用到其中的一兩種款式,為了減少樣式表的體積,需要將那些沒有使用的樣式挑選出來刪除掉
使用 uncss 工具來刪除無用的樣式
該工具提供有在線版,只需要復制自己的 HTML 以及 CSS,點擊按鈕就可以生成精簡后的樣式
另外也可以通過瀏覽器工具 Coverage 挑選出未使用的樣式,如下圖
經過分析得出每個文件未使用樣式的百分占比,其中紅色標記的為未使用到的樣式,從下圖中可以看到具體未使用到的樣式有哪些
⚠️ 上面兩種方法都是通過樣式規則的選擇器在頁面上查找元素,如果能找到對應的元素,則說明該樣式規則有被使用,隨着在頁面上進行各種操作,該百分比可能會降低,因為有些樣式會在某些操作執行之后才會被使用到,比如 :hover
偽類相關的樣式,在鼠標移入元素之前不會被標記為已使用的
所以,這兩種方式都有一定的局限性,並不是挑選出的樣式就一定是沒有用的,也許某個樣式是在用戶執行相當復雜的操作后才會起作用,需要嚴格測試
ℹ️ 許多框架和庫也提供自定義打包版本,從源頭舍去那些無用的代碼
🐛 刪除被層疊的樣式
CSS 全名 層疊樣式表(Cascading Style Sheets),對同一個元素多次指定同一個樣式只會讓優先級高的覆蓋優先級低的
在樣式規則的選擇器完全相同的情況下(比如這里 .selector-1 > .selector-2 和 .selector-1 > .selector-2 是完全相同的),被覆蓋的樣式可以安全地刪除,如下
通過瀏覽器的開發者工具可以輕松看到哪些樣式被覆蓋了
如果這兩條樣式規則是相鄰的,還可以合並成一條,不過壓縮工具會幫助完成這個事情,無需動手
⚠️ 在選擇器不相同的時候,也有可能會匹配到同一個元素,這個時候本條規則並不適用,需要注意
⚠️ 有時候同一個樣式屬性反復出現只是為了兼容一些舊瀏覽器,也需要注意
🐛 使用復合屬性
有些樣式屬性可以合並為一條,比如
.selector {
flex-direction: column;
flex-wrap: wrap;
}
/* 合並后 */
.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
該工具還提供了 在線版
壓縮前后對比
/* normalize selectors */
h1::before, h1:before {
/* reduce shorthand even further */
margin: 10px 20px 10px 20px;
/* reduce color values */
color: #ff0000;
/* remove duplicated properties */
font-weight: 400;
font-weight: 400;
/* reduce position values */
background-position: bottom right;
/* normalize wrapping quotes */
quotes: '«' "»";
/* reduce gradient parameters */
background: linear-gradient(to bottom, #ffe500 0%, #ffe500 50%, #121 50%, #121 100%);
/* replace initial values */
min-width: initial;
}
/* correct invalid placement */
@charset "utf-8";
@charset "utf-8";h1:before{margin:10px 20px;color:red;font-weight:400;background-position:100% 100%;quotes:"«" "»";background:linear-gradient(180deg,#ffe500,#ffe500 50%,#121 0,#121);min-width:0}
體積減少了一半
ℹ️ cssnano 屬於 PostCSS
生態系統,並且自帶 autoprefixer 工具幫助清理多余的瀏覽器廠商前綴
JavaScript
🐛 刪除無用的 JavaScript
瀏覽器的 Coverage 工具也能挑選出未使用的 JavaScript 代碼,不再重復
⚠️ 同樣的,挑選出來的代碼也不一定全是無用的,需要經過仔細測試
🐛 刪除歷史遺留代碼
同 CSS 一樣,JavaScript 也有一些代碼是為了兼容舊瀏覽器而存在的
像 es5-shim.js 就是為了給那些不支持 ES5 的瀏覽器准備的,現在已經可以放心地從項目中去掉了,目前全球使用支持 ES5 的瀏覽器的用戶占比高達98%
另外一些框架或庫的新版本通常將不會包含那些兼容舊瀏覽器的代碼,需要時保持更新即可,比如用 jQuery3.0 替換 jQuery1.12
🐛 刪除功能重復的插件
一個項目經手的人多了之后,會出現一些匪夷所思的膨脹,比如同一個項目中引入了好幾個功能相似的插件
找出相關代碼,根據需求確定真正需要使用的插件,去掉其它多余的
⚠️ 此條需要經過嚴格的測試
🐛 用 CSS 代替 JavaScript 實現效果
常見的比如鼠標移入區域的時候顯示元素,移出的時候隱藏元素,用 CSS 可以輕易實現
.selector + .item {
display: none;
}
.selector:hover + .item {
display: block;
}
You-Dont-Need-JavaScript 這個倉庫展示了很多可以不依賴 JavaScript 實現的效果
🐛 使用新的 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,瀏覽器支持范圍最廣的格式
根據目標設備選擇合適的字體格式,不同的字體格式兼容的瀏覽器也是不一樣的
下圖是圖一套字體的不同文件格式的大小對比
我們應該優先選用壓縮率更高的 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 可以打開並編輯字體以及保存為其它格式
這是經過我編輯過后的文件大小對比,文件大小差距很大,確實用到的字體比較少
圖像
在 Web 網頁中,圖像的體積才是占了大部分,減少圖像可以大幅改進性能
🐛 選擇適合的圖像格式
不同文件格式的圖像其文件大小,圖像質量是不一樣的,根據具體情況選擇合適的圖像格式
常用 Web 圖像格式
格式 | 透明 | 動畫 | 說明 | 瀏覽器支持 |
---|---|---|---|---|
GIF | ✔️ | ✔️ | 顏色較少 | 全 |
JPEG | ❌ | ❌ | 有損格式,常用於照片 | 全 |
PNG | ✔️ | ❌ | 無損 | 全 |
WebP | ✔️ | ✔️ | 支持無損/有損壓縮,比JPEG,PNG和GIF更好的壓縮效果 | 較新 |
AVIF | ✔️ | ✔️ | 比 WebP,JPEG,PNG 和 GIF 更好的壓縮效果 | 最新 |
JPEGXL | ✔️ | ✔️ | 無損壓縮,更快的解碼和其他各種改進 | 暫無 |
ℹ️ 我寫這篇博客的時候暫時沒有瀏覽器支持 JPEGXL 格式的圖像
ℹ️ 轉換圖像格式可以使用 Sqoosh 這個在線工具,在圖像大小和質量之間手動調整權衡,並不是說優秀的圖像格式體積就一定小
🐛 使用 WebP
一些新的圖像格式擁有較高的性能,比如 AVIF 和 WebP
不過這些新的圖像格式不是所有瀏覽器都支持,此時可以使用一個 <picture>
元素來包裹 <img>
元素,再通過使用 <source>
元素來為 <img>
元素提供多個備胎資源供其自行選擇
<source>
元素可以有多個,srcset
屬性是必須的(注意是 srcset)
<picture>
<source srcset="logo.webp" type="image/webp">
<img alt="logo" src="logo.png">
</picture>
值得一提的是 <picture>
元素內部必須包含一個 <img>
元素,否則圖像不會顯示(因為 <picture>
元素並不是一個獨立顯示的元素,而是單純為 <img>
元素服務的)
還有 <img>
元素始終都不應該忘記的 alt
屬性,當任何圖像格式都無法顯示或者圖像下載失敗的時候,至少還能顯示替代的文字說明
要在 CSS 中使用 WebP 通常用 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 格式加載不同格式的圖像了,<noscript>
的情況就要額外考慮了
注意,不要寫成下面這樣
.webp .logo {
background: url(./logo.webp);
}
.logo {
background: url(./logo.png);
}
這樣寫,雖然最終顯示沒有什么問題,但是對於支持 WebP 的瀏覽器來說就加載了多余的圖像了,這與本文的主旨就背道而馳了
轉換為 WebP 的在線工具 WebP Converter
或者在 libwebp 下載最新版本的 libwebp 工具,在其目錄執行命令
.\bin\cwebp -q 80 logo.png -o logo.webp
其中參數 -q
表示圖像質量
🐛 使用 AVIF
AVIF 有比 WebP 更優秀的性能,同樣是 漸進增強 的方式來使用
瀏覽器會自行忽略不支持的格式,如果瀏覽器支持 AVIF 格式就使用 logo.avif,退一步,如果支持 WebP 格式就使用 logo.webp
再退一步,如果上面倆都不支持,就會使用 logo.png 作為后備,如果不支持 <picture>
元素的瀏覽器會直接顯示 <img>
元素
🐛 使用 JPEGXL
<picture>
元素也是面向未來而設計,等以后瀏覽器開始支持 JPEGXL 圖像格式的時候就可以很方便的使用這種格式,甚至可能出現的其它類型圖像格式
<picture>
<source srcset="logo.jxl" type="image/jxl">
<img alt="logo" src="logo.png">
</picture>
🐛 響應式圖像
而 <img>
元素通過其新增的 srcset
和 sizes
屬性來實現響應式圖像
<img alt="avator"
src="avator.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
屬性的瀏覽器提供的降級方案
🐛 響應式的 AVIF 以及 WebP
將前面相關東西結合起來
<picture>
<source
sizes="(max-width: 600px) 120px, 240px"
srcset="avator-120.avif 120w, avator-240.avif 240w, avator-480.avif 480w"
type="image/avif"
/>
<source
sizes="(max-width: 600px) 120px, 240px"
srcset="avator-120.webp 120w, avator-240.webp 240w, avator-480.webp 480w"
type="image/webp"
/>
<source
sizes="(max-width: 600px) 120px, 240px"
srcset="avator-120.jpg 120w, avator-240.jpg 240w, avator-480.jpg 480w"
type="image/jpeg"
/>
<img alt="avator" src="fallback-avator.jpg" width="120" height="120">
</picture>
放心,瀏覽器並不會加載不需要顯示的圖像
🐛 響應式背景圖像
在 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
目前還在草案中,需要添加 -webkit-
前綴支持主流瀏覽器,而這應該使用 Autoprefixer 工具自動添加
⚠️ Safari 當前只支持圖像的 url 路徑和 1x/2x 這樣的設備像素比描述符
🐛 圖像壓縮
有些格式的圖像往往還會包含一些沒有用的信息,清理掉這些信息有助於縮小圖像體積
這通常使用工具來進行
使用 imagemin 壓縮圖像
🐛圖像懶加載
頁面上有很多圖像我們一開始是看不到的,有的在我們滾動頁面之后才會出現在屏幕上,又有的在某個對話框彈出后才能看到
對於這類圖像,我們可以推遲它們的加載時機,等到它們需要真正展示在屏幕上的時候才加載,而不是在頁面一開始時就加載,這將大大節省頁面初始化時加載的資源大小
使用瀏覽器原生的懶加載方案,這非常簡單,只需要給 <img>
元素添加一個 loading="lazy"
屬性即可
<img alt="avator" loading="lazy" src="avator.jpg">
和 <picture>
元素搭配使用沒問題,注意 loading
是 <img>
元素的屬性,不要寫錯位置
<picture>
<source type="image/avif" srcset="avator.avif">
<source type="image/webp" srcset="avator.webp">
<img alt="avator" loading="lazy" src="avator.jpg">
</picture>
目前該屬性只得到一部分瀏覽器的支持,不支持的瀏覽器會自行忽略
該屬性的 polyfill
還可以混合使用 JavaScript 插件,首先要判斷 <img>
元素是否支持該屬性
if ('loading' in HTMLImageElement.prototype) {
// 支持,使用瀏覽器懶加載方案
} else {
// 不支持,引入插件
}
插件我們這里使用 lazysizes,注意這里去掉了 <img>
元素的原本的 src
屬性,改成了 data-src
,並且添加了 class="lazyload"
如果瀏覽器支持,獲取所有需要懶加載的圖像元素,將這些元素 data-src
的值賦給 src
如果瀏覽器不支持,則加載並執行 lazysizes 插件
<img alt="avator" class="lazyload" data-src="avator.jpg" loading="lazy">
<script>
;(function() {
if ('loading' in HTMLImageElement.prototype) {
var images = document.querySelectorAll('img.lazyload')
images.forEach(function(img) {
img.src = img.dataset.src
})
} else {
var script = document.createElement('script')
script.async = true
script.src = '//afarkas.github.io/lazysizes/lazysizes.min.js'
document.body.appendChild(script)
}
})()
</script>
lazysizes 插件會自動選取那些 class="lazyload"
的 <img>
元素進行處理
⚠️ 引入一個插件會增加 JavaScript 的代碼量,但是延遲了部分圖像的加載時機,具體需要權衡
🐛 使用其它方案替換圖像
減少圖像最好的辦法就是沒有圖像
使用 SVG 替換圖像
上面這張圖像格式為 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 效果就是純樣式寫的
相對於圖像來說,純代碼的字節數就少得多了
<div class="loading"></div>
@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;
}