CSS效能優化的幾個技巧

前端南玖發表於2022-03-21

前言

隨著網際網路發展至今,對於網站來說,效能顯的越來越重要了,CSS作為頁面渲染和內容展現的重要環節,影響著使用者對整個網站的第一體驗。所以,我們需要重視與CSS相關的效能優化。

專案開發初期我們可能因為各種原因(很大一部分原因是因為專案工期,產品往往把專案上線時間卡的死死的,根本不聽你說的什麼效能優化),怎麼寫的舒服就怎麼來,對於效能優化我們常常在專案完成時才去考慮,經常被推遲到專案的末期,甚至到暴露出嚴重的效能問題時才進行效能優化。

為了更多地避免這一情況,首先要重視起效能優化相關的工作,將其貫穿到整個產品設計與開發中。其次,就是了解效能相關的內容,在專案開發過程中,自然而然地進行效能優化。

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~

css渲染規則

想要優化CSS的效能,我們首先需要了解CSS的渲染規則,CSS選擇器是從右向左進行匹配的

來看個例子?:

.nav h3 a{font-size: 14px;}

渲染過程大概是:首先找到所有的a,沿著a的父元素查詢h3,然後再沿著h3,查詢.nav。中途找到了符合匹配規則的節點就加入結果集。如果找到根元素html都沒有匹配,則不再遍歷這條路徑,從下一個a開始重複這個查詢匹配(只要頁面上有多個最右節點為a)。

Tips:為什麼CSS選擇器是從右向左匹配的?

CSS中更多的選擇器是不會匹配的,所以在考慮效能問題時,需要考慮的是如何在選擇器不匹配時提升效率。從右向左匹配就是為了達成這一目的的,通過這一策略能夠使得CSS選擇器在不匹配的時候效率更高。這樣想來,在匹配時多耗費一些效能也能夠想的通了。

內聯首屏關鍵CSS(Critical CSS)

效能優化中有一個重要的指標——首次有效繪製(First Meaningful Paint,簡稱FMP)即指頁面的首要內容(primary content)出現在螢幕上的時間。這一指標影響使用者看到頁面前所需等待的時間,而 內聯首屏關鍵CSS(即Critical CSS,可以稱之為首屏關鍵CSS) 能減少這一時間。

很多人都喜歡通過link標籤引用外部CSS檔案。但需要知道的是,將CSS直接內聯到HTML文件中能使CSS更快速地下載。而使用外部CSS檔案時,需要在HTML文件下載完成後才知道所要引用的CSS檔案,然後才下載它們。所以說,內聯CSS能夠使瀏覽器開始頁面渲染的時間提前,因為在HTML下載完成之後就能渲染了。

但是我們不應該將所有的CSS都內聯在HTML文件中,因為[初始擁塞視窗]存在限制(TCP相關概念,通常是 14.6kB,壓縮後大小),如果內聯CSS後的檔案超出了這一限制,系統就需要在伺服器和瀏覽器之間進行更多次的往返,這樣並不能提前頁面渲染時間。因此,我們應當只將渲染首屏內容所需的關鍵CSS內聯到HTML中

⚠️還有一點需要注意的是內聯CSS沒有快取,每次都會隨HTML的載入而重新下載,但我們將內聯首屏關鍵CSS控制在 14.6kB以內,它對效能優化還是起到正向作用的。(凡事有利也有弊)

非同步載入非首屏CSS

我們需要知道兩點內容:(具體可以看我之前的文章:這些瀏覽器面試題,看看你能回答幾個?

  • CSS不會阻塞DOM的解析,但會阻塞DOM的渲染
  • CSS會阻塞JS執行,但不會阻塞JS檔案的下載

由於CSS會阻塞DOM的渲染,所以我們將首屏關鍵CSS內聯後,剩餘的非首屏CSS內容可以使用外部CSS,並且非同步載入,防止非首屏CSS內容阻塞頁面的渲染。

CSS非同步載入方式

第一種方法是動態建立

// 建立link標籤
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最後位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );

第二種方法是將link元素的media屬性設定為使用者瀏覽器不匹配的媒體型別(或媒體查詢)

對瀏覽器來說,如果樣式表不適用於當前媒體型別,其優先順序會被放低,會在不阻塞頁面渲染的情況下再進行下載。在首屏檔案載入完成之後,將media的值設為screenall,從而讓瀏覽器開始解析CSS。

<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">

第三種方法是通過rel屬性將link元素標記為alternate可選樣式表

<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">

第四種方法是使用rel=preload來非同步載入CSS

<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">

注意,as是必須的。忽略as屬性,或者錯誤的as屬性會使preload等同於XHR請求,瀏覽器不知道載入的是什麼內容,因此此類資源載入優先順序會非常低。as的可選值可以參考上述標準文件。

看起來,rel="preload"的用法和上面兩種沒什麼區別,都是通過更改某些屬性,使得瀏覽器非同步載入CSS檔案但不解析,直到載入完成並將修改還原,然後開始解析。

但是它們之間其實有一個很重要的不同點,那就是使用preload,比使用不匹配的media方法能夠更早地開始載入CSS。所以儘管這一標準的支援度還不完善,仍建議優先使用該方法。

CSS檔案壓縮

這應該是最容易想到的一個方法了,通過壓縮CSS檔案大小來提高頁面載入速度。現在的構建工具,如webpack、gulp/grunt、rollup等也都支援CSS壓縮功能。壓縮後的檔案能夠明顯減小,可以大大降低了瀏覽器的載入時間。

CSS層級巢狀最好不要超過3層

一般情況下,元素的巢狀層級不能超過3級,過度的巢狀會導致程式碼變得臃腫,沉餘,複雜。導致css檔案體積變大,造成效能浪費,影響渲染的速度!而且過於依賴HTML文件結構。這樣的css樣式,維護起來,極度麻煩,如果以後要修改樣式,可能要使用!important覆蓋。儘量保持簡單,不要使用巢狀過多過於複雜的選擇器。

刪除無用CSS程式碼

一般情況下,會存在這兩種無用的CSS程式碼:一種是不同元素或者其他情況下的重複程式碼,一種是整個頁面內沒有生效的CSS程式碼。

對於前者,在編寫的程式碼時候,我們應該儘可能地提取公共類,減少重複。對於後者,在不同開發者進行程式碼維護的過程中,總會產生不再使用的CSS的程式碼,當然一個人編寫時也有可能出現這一問題。而這些無用的CSS程式碼不僅會增加瀏覽器的下載量,還會增加瀏覽器的解析時間,這對效能來說是很大的消耗。所以我們需要找到並去除這些無用程式碼。

那麼我們如何知道哪些CSS程式碼是無用程式碼呢?

谷歌的Chrome瀏覽器就有這種開箱即用的功能。只需轉到檢視>開發人員>開發人員工具,並在最近的版本中開啟Sources選項卡,然後開啟命令選單。然後,點選Coverage,在Coverage analysis視窗中高亮顯示當前頁面上未使用的程式碼。
unused-css.png

慎用*萬用字元

我們有時候可能會寫下面這種程式碼來消除一些標籤的預設樣式或統一瀏覽器對標籤渲染的差異化:

*{
  margin:0;
  padding:0;
}

這樣雖然程式碼量少,但它的效能可不是最佳的,我們最好還是寫對應的標籤選擇器:

body,dl,dd,h1,h2,h3,h4,h5,h6,p,form,ol,ul{
  margin:0;
  padding:0;
}

開發時儘量避免使用萬用字元選擇器

小圖片處理方式

一般來講一個網站上肯定會有很多個小圖示,對於這些小圖示,目前的主流的解決方案有三個,cssSprite(雪碧圖)字型圖示把圖片轉成base64

  • cssSprite: 把所有icon圖片合成一張png圖片,使用時對節點設定寬高,加上bacgroud-position進行背景定位。以背景圖方式顯展示需要的icon,如果一個網站有20圖示,那麼就要請求20次,使用cssSprite,只需要請求一次,大大的減少了http請求。缺點就是管理不靈活,如果需要新增一個圖示,都需要改合併圖片的原始檔,圖示定位也要規範,不然容易干擾圖片之間的定位。
  • 字型圖示: 簡單粗暴的理解就是把所有的圖示當成一個字型處理!這樣不用去請求圖片。一般是使用class來定義圖示,要替換圖示時,只需更換樣式名,管理方便,語意明確,靈活放大縮小,並且不會造成失真。但是隻支援單色的圖片。
  • base64: 另一種方案就是把小的icon圖片轉成base64編碼,這樣可以不用去請求圖片,把base64編碼直接整合到js或者css裡面,可以防止因為一些相對路徑,或者圖片被不小刪除了等問題導致圖片404錯誤。但是找個方式會生成一大串的base64編碼。一般來說,8K以下的圖片才轉換成base64編碼。如果把一張50K的圖片轉成base64編碼,那麼會生成超過65000個字元的base64編碼,字元的大小就已經是將近70K了!建議就是:8K以下的圖片才轉換成base64編碼。

避免使用@import

不建議使用@import主要有以下兩點原因:

  • 使用@import引入CSS會影響瀏覽器的並行下載。使用@import引用的CSS檔案只有在引用它的那個css檔案被下載、解析之後,瀏覽器才會知道還有另外一個css需要下載,這時才去下載,然後下載後開始解析、構建render tree等一系列操作。這就導致瀏覽器無法並行下載所需的樣式檔案。

  • 多個@import會導致下載順序紊亂。在IE中,@import會引發資原始檔的下載順序被打亂,即排列在@import後面的js檔案先於@import下載,並且打亂甚至破壞@import自身的並行下載

不要在ID選擇器前面進行巢狀其它選擇器

在ID選擇器前面巢狀其它選擇器純粹是多餘的

  • ID選擇器本來就是唯一的而且人家權值那麼大,前面巢狀(.content #text)完全是浪費效能。
  • 除了巢狀,在ID選擇器前面也不需要加標籤或者其它選擇器。比如 div#text或者.box#text。這兩種方式完全是多餘的,理由就是ID在頁面就是唯一的。前面加任何東西都是多餘的!

刪除不必要的單位和零

CSS 支援多種單位和數字格式,可以刪除尾隨和跟隨的零,零始終是零,新增維度不會為包含的資訊附帶任何價值。

.box {
  padding: .2px;
  margin: 20px;
  avalue: 0;
}

優化迴流與重繪

在網站的使用過程中,某些操作會導致樣式的改變,這時瀏覽器需要檢測這些改變並重新渲染,其中有些操作所耗費的效能更多。我們都知道,當FPS為60時,使用者使用網站時才會感到流暢。這也就是說,我們需要在16.67ms內完成每次渲染相關的所有操作,所以我們要儘量減少耗費更多的操作。

減少迴流與重繪

合併對DOM樣式的修改,採用css class來修改

const el = document.querySelector('.box')
el.style.margin = '5px'
el.style.borderRadius = '12px'
el.style.boxShadow = '1px 3px 4px #ccc'

建議使用css class

.update{
  margin: 5px;
  border-dadius: 12px;
  box-shadow: 1px 3px 4px #ccc
}
const el = document.querySelector('.box')
el.classList.add('update')

如果需要對DOM進行多次訪問,儘量使用區域性變數快取該DOM

避免使用table佈局,可能很⼩的⼀個⼩改動會造成整個table的重新佈局

CSS選擇符從右往左匹配查詢,避免節點層級過多

DOM離線處理,減少迴流重繪次數

離線的DOM不屬於當前DOM樹中的任何一部分,這也就意味著我們對離線DOM處理就不會引起頁面的迴流與重繪。

  • 使用display: none,上面我們說到了 (display: none) 將元素從渲染樹中完全移除,元素既不可見,也不是佈局的組成部分,之後在該DOM上的操作不會觸發迴流與重繪,操作完之後再將display屬性改為顯示,只會觸發這一次迴流與重繪。

​ 提醒⏰:visibility : hidden 的元素只對重繪有影響,不影響重排。

  • 通過 documentFragment 建立一個 dom 文件片段,在它上面批量操作 dom,操作完成之後,再新增到文件中,這樣只會觸發一次重排。
const el = document.querySelector('.box')
const fruits = ['front', 'nanjiu', 'study', 'code'];
const fragment = document.createDocumentFragment();
fruits.forEach(item => {
  const li = document.createElement('li');
  li.innerHTML = item;
  fragment.appendChild(li);
});
el.appendChild(fragment);
  • 克隆節點,修改完再替換原始節點
const el = document.querySelector('.box')
const fruits = ['front', 'nanjiu', 'study', 'code'];
const cloneEl = el.cloneNode(true)
fruits.forEach(item => {
  const li = document.createElement('li');
  li.innerHTML = item;
  cloneEl.appendChild(li);
});
el.parentElement.replaceChild(cloneEl,el)

DOM脫離普通文件流

使用absoultfixed讓元素脫離普通文件流,使用絕對定位會使的該元素單獨成為渲染樹中 body 的一個子元素,重排開銷比較小,不會對其它節點造成太多影響。

CSS3硬體加速(GPU加速)

使用css3硬體加速,可以讓transform、opacity、filters這些動畫不會引起迴流重繪 。但是對於動畫的其它屬性,比如background-color這些,還是會引起迴流重繪的,不過它還是可以提升這些動畫的效能。

常見的觸發硬體加速的css屬性:

  • transform
  • opacity
  • filters
  • Will-change

將節點設定為圖層

圖層能夠阻⽌該節點的渲染⾏為影響別的節點。⽐如對於video標籤來說,瀏覽器會⾃動將該節點變為圖層。

具體迴流與重繪知識點可以看我這篇文章:介紹迴流與重繪(Reflow & Repaint),以及如何進行優化?

推薦閱讀

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」,如果你想進前端交流群一起學習,請點這裡

我是南玖,我們下期見!!!

相關文章