本文作者:高峰,360奇舞團前端工程師,W3C效能工作組成員,同時參與WOT工作組的學習。
我們都知道對於網站來說,效能至關重要,CSS作為頁面渲染和內容展現的重要環節,影響著使用者對整個網站的第一體驗。因此,與其相關的效能優化是不容忽視的。
對於效能優化我們常常在專案完成時才去考慮,經常被推遲到專案的末期,甚至到暴露出嚴重的效能問題時才進行效能優化,相信大多數人對此深有體會。
筆者認為,為了更多地避免這一情況,首先要重視起效能優化相關的工作,將其貫穿到整個產品設計與開發中。其次,就是了解效能相關的內容,在專案開發過程中,自然而然地進行效能優化。最後,也是最最重要的,那就是從現在開始實施優化。
推薦大家閱讀下奇舞週刊之前推的《嗨,送你一張Web效能優化地圖》1這篇文章,能夠幫助大家對效能優化需要做的事以及需要考慮的問題形成一個整體的概念。
本文將會詳細介紹CSS效能優化相關的技巧,筆者將它們分為實踐型和建議型兩類,共8個小技巧。實踐型技巧能夠快速地應用在專案中,能夠很好地提升效能,也是筆者經常使用的,建議大家儘快在專案中實踐。建議型技巧中,有的可能對效能影響並不顯著,有的平時大家也並不會那麼用,所以筆者不會著重講述,讀者們可以根據自身情況瞭解一下即可。
在正式開始之前,需要大家對於瀏覽器的工作原理2有些一定的瞭解,需要的小夥伴可以先簡單瞭解下。
下面我們開始介紹實踐型的4個優化技巧,先從首屏關鍵CSS開始。
1. 內聯首屏關鍵CSS(Critical CSS)
效能優化中有一個重要的指標——首次有效繪製(First Meaningful Paint,簡稱FMP)即指頁面的首要內容(primary content)出現在螢幕上的時間。這一指標影響使用者看到頁面前所需等待的時間,而**內聯首屏關鍵CSS(即Critical CSS,可以稱之為首屏關鍵CSS)**能減少這一時間。
大家應該都習慣於通過link標籤引用外部CSS檔案。但需要知道的是,將CSS直接內聯到HTML文件中能使CSS更快速地下載。而使用外部CSS檔案時,需要在HTML文件下載完成後才知道所要引用的CSS檔案,然後才下載它們。所以說,內聯CSS能夠使瀏覽器開始頁面渲染的時間提前,因為在HTML下載完成之後就能渲染了。
既然內聯CSS能夠使頁面渲染的開始時間提前,那麼是否可以內聯所有的CSS呢?答案顯然是否定的,這種方式並不適用於內聯較大的CSS檔案。因為初始擁塞視窗3存在限制(TCP相關概念,通常是 14.6kB,壓縮後大小),如果內聯CSS後的檔案超出了這一限制,系統就需要在伺服器和瀏覽器之間進行更多次的往返,這樣並不能提前頁面渲染時間。因此,我們應當只將渲染首屏內容所需的關鍵CSS內聯到HTML中。
既然已經知道內聯首屏關鍵CSS能夠優化效能了,那下一步就是如何確定首屏關鍵CSS了。顯然,我們不需要手動確定哪些內容是首屏關鍵CSS。Github上有一個專案Critical CSS4,可以將屬於首屏的關鍵樣式提取出來,大家可以看一下該專案,結合自己的構建工具進行使用。當然為了保證正確,大家最好再親自確認下提取出的內容是否有缺失。
不過內聯CSS有一個缺點,內聯之後的CSS不會進行快取,每次都會重新下載。不過如上所說,如果我們將內聯後的檔案大小控制在了14.6kb以內,這似乎並不是什麼大問題。
如上,我們已經介紹了為什麼要內聯關鍵CSS以及如何內聯,那麼剩下的CSS我們怎麼處理好呢?建議使用外部CSS引入剩餘CSS,這樣能夠啟用快取,除此之外還可以非同步載入它們。
2. 非同步載入CSS
CSS會阻塞渲染,在CSS檔案請求、下載、解析完成之前,瀏覽器將不會渲染任何已處理的內容。有時,這種阻塞是必須的,因為我們並不希望在所需的CSS載入之前,瀏覽器就開始渲染頁面。那麼將首屏關鍵CSS內聯後,剩餘的CSS內容的阻塞渲染就不是必需的了,可以使用外部CSS,並且非同步載入。
那麼如何實現CSS的非同步載入呢?有以下四種方式可以實現瀏覽器非同步載入CSS。
第一種方式是使用JavaScript動態建立樣式表link元素,並插入到DOM中。
// 建立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="print"
,甚至可以是完全不存在的型別media="noexist"
。對瀏覽器來說,如果樣式表不適用於當前媒體型別,其優先順序會被放低,會在不阻塞頁面渲染的情況下再進行下載。
當然,這麼做只是為了實現CSS的非同步載入,別忘了在檔案載入完成之後,將media
的值設為screen
或all
,從而讓瀏覽器開始解析CSS。
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
複製程式碼
與第二種方式相似,我們還可以通過rel
屬性將link
元素標記為alternate
可選樣式表,也能實現瀏覽器非同步載入。同樣別忘了載入完成之後,將rel
改回去。
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
複製程式碼
上述的三種方法都較為古老。現在,rel="preload"5這一Web標準指出瞭如何非同步載入資源,包括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。所以儘管這一標準的支援度還不完善,仍建議優先使用該方法。
該標準現在已經是候選標準,相信瀏覽器會逐漸支援該標準。在各瀏覽器的支援度如下圖所示。
從上圖可以看出這一方法在現在的瀏覽器中支援度不算樂觀,不過我們可以通過loadCSS6進行polyfill,所以支援不支援,這都不是事兒。
3. 檔案壓縮
效能優化時有一個最容易想到,也最常使用的方法,那就是檔案壓縮,這一方案往往效果顯著。
檔案的大小會直接影響瀏覽器的載入速度,這一點在網路較差時表現地尤為明顯。相信大家都早已習慣對CSS進行壓縮,現在的構建工具,如webpack、gulp/grunt、rollup等也都支援CSS壓縮功能。壓縮後的檔案能夠明顯減小,可以大大降低了瀏覽器的載入時間。
4. 去除無用CSS
雖然檔案壓縮能夠降低檔案大小。但CSS檔案壓縮通常只會去除無用的空格,這樣就限制了CSS檔案的壓縮比例。那是否還有其他手段來精簡CSS呢?答案顯然是肯定的,如果壓縮後的檔案仍然超出了預期的大小,我們可以試著找到並刪除程式碼中無用的CSS。
一般情況下,會存在這兩種無用的CSS程式碼:一種是不同元素或者其他情況下的重複程式碼,一種是整個頁面內沒有生效的CSS程式碼。對於前者,在編寫的程式碼時候,我們應該儘可能地提取公共類,減少重複。對於後者,在不同開發者進行程式碼維護的過程中,總會產生不再使用的CSS的程式碼,當然一個人編寫時也有可能出現這一問題。而這些無用的CSS程式碼不僅會增加瀏覽器的下載量,還會增加瀏覽器的解析時間,這對效能來說是很大的消耗。所以我們需要找到並去除這些無用程式碼。
當然,如果手動刪除這些無用CSS是很低效的。我們可以藉助Uncss7庫來進行。Uncss可以用來移除樣式表中的無用CSS,並且支援多檔案和JavaScript注入的CSS。
前面已經說完了實踐型的4個優化技巧,下面我們介紹下建議型的4個技巧。
1. 有選擇地使用選擇器
大多數朋友應該都知道CSS選擇器的匹配是從右向左進行的,這一策略導致了不同種類的選擇器之間的效能也存在差異。相比於#markdown-content-h3
,顯然使用#markdown .content h3
時,瀏覽器生成渲染樹(render-tree)所要花費的時間更多。因為後者需要先找到DOM中的所有h3
元素,再過濾掉祖先元素不是.content
的,最後過濾掉.content
的祖先不是#markdown
的。試想,如果巢狀的層級更多,頁面中的元素更多,那麼匹配所要花費的時間代價自然更高。
不過現代瀏覽器在這一方面做了很多優化,不同選擇器的效能差別並不明顯,甚至可以說差別甚微。此外不同選擇器在不同瀏覽器中的效能表現8也不完全統一,在編寫CSS的時候無法兼顧每種瀏覽器。鑑於這兩點原因,我們在使用選擇器時,只需要記住以下幾點,其他的可以全憑喜好。
- 保持簡單,不要使用巢狀過多過於複雜的選擇器。
- 萬用字元和屬性選擇器效率最低,需要匹配的元素最多,儘量避免使用。
- 不要使用類選擇器和ID選擇器修飾元素標籤,如
h3#markdown-content
,這樣多此一舉,還會降低效率。 - 不要為了追求速度而放棄可讀性與可維護性。
如果大家對於上面這幾點還存在疑問,筆者建議大家選擇以下幾種CSS方法論之一(BEM9,OOCSS10,SUIT11,SMACSS12,ITCSS13,Enduring CSS14等)作為CSS編寫規範。使用統一的方法論能夠幫助大家形成統一的風格,減少命名衝突,也能避免上述的問題,總之好處多多,如果你還沒有使用,就趕快用起來吧。
Tips:為什麼CSS選擇器是從右向左匹配的?
CSS中更多的選擇器是不會匹配的,所以在考慮效能問題時,需要考慮的是如何在選擇器不匹配時提升效率。從右向左匹配就是為了達成這一目的的,通過這一策略能夠使得CSS選擇器在不匹配的時候效率更高。這樣想來,在匹配時多耗費一些效能也能夠想的通了。
2. 減少使用昂貴的屬性
在瀏覽器繪製螢幕時,所有需要瀏覽器進行操作或計算的屬性相對而言都需要花費更大的代價。當頁面發生重繪時,它們會降低瀏覽器的渲染效能。所以在編寫CSS時,我們應該儘量減少使用昂貴屬性,如box-shadow
/border-radius
/filter
/透明度/:nth-child
等。
當然,並不是讓大家不要使用這些屬性,因為這些應該都是我們經常使用的屬性。之所以提這一點,是讓大家對此有一個瞭解。當有兩種方案可以選擇的時候,可以優先選擇沒有昂貴屬性或昂貴屬性更少的方案,如果每次都這樣的選擇,網站的效能會在不知不覺中得到一定的提升。
3. 優化重排與重繪
在網站的使用過程中,某些操作會導致樣式的改變,這時瀏覽器需要檢測這些改變並重新渲染,其中有些操作所耗費的效能更多。我們都知道,當FPS為60時,使用者使用網站時才會感到流暢。這也就是說,我們需要在16.67ms內完成每次渲染相關的所有操作,所以我們要儘量減少耗費更多的操作。
3.1 減少重排
重排會導致瀏覽器重新計算整個文件,重新構建渲染樹,這一過程會降低瀏覽器的渲染速度。如下所示,有很多操作會觸發重排,我們應該避免頻繁觸發這些操作。
- 改變
font-size
和font-family
- 改變元素的內外邊距
- 通過JS改變CSS類
- 通過JS獲取DOM元素的位置相關屬性(如width/height/left等)
- CSS偽類啟用
- 滾動滾動條或者改變視窗大小
此外,我們還可以通過CSS Trigger15查詢哪些屬性會觸發重排與重繪。
值得一提的是,某些CSS屬性具有更好的重排效能。如使用Flex
時,比使用inline-block
和float
時重排更快,所以在佈局時可以優先考慮Flex
。
3.2 避免不必要的重繪
當元素的外觀(如color,background,visibility等屬性)發生改變時,會觸發重繪。在網站的使用過程中,重繪是無法避免的。不過,瀏覽器對此做了優化,它會將多次的重排、重繪操作合併為一次執行。不過我們仍需要避免不必要的重繪,如頁面滾動時觸發的hover事件,可以在滾動的時候禁用hover事件,這樣頁面在滾動時會更加流暢。
此外,我們編寫的CSS中動畫相關的程式碼越來越多,我們已經習慣於使用動畫來提升使用者體驗。我們在編寫動畫時,也應當參考上述內容,減少重繪重排的觸發。除此之外我們還可以通過硬體加速16和will-change17來提升動畫效能,本文不對此展開詳細介紹,感興趣的小夥伴可以點選連結進行檢視。
最後需要注意的是,使用者的裝置可能並沒有想象中的那麼好,至少不會有我們的開發機器那麼好。我們可以藉助Chrome的開發者工具進行CPU降速,然後再進行相關的測試,降速方法如下圖所示。
如果需要在移動端訪問的,最好將速度限制更低,因為移動端的效能往往更差。
4. 不要使用@import
最後提一下,不要使用@import引入CSS,相信大家也很少使用。
不建議使用@import主要有以下兩點原因。
首先,使用@import引入CSS會影響瀏覽器的並行下載。使用@import引用的CSS檔案只有在引用它的那個css檔案被下載、解析之後,瀏覽器才會知道還有另外一個css需要下載,這時才去下載,然後下載後開始解析、構建render tree等一系列操作。這就導致瀏覽器無法並行下載所需的樣式檔案。
其次,多個@import會導致下載順序紊亂。在IE中,@import會引發資原始檔的下載順序被打亂,即排列在@import後面的js檔案先於@import下載,並且打亂甚至破壞@import自身的並行下載。
所以不要使用這一方法,使用link標籤就行了。
總結
至此,我們介紹完了CSS效能優化的4個實踐型技巧和4個建議型技巧,在瞭解這些技巧之後,CSS的效能優化從現在就可以開始了。不要猶豫了,儘快開始吧。
致謝
特別感謝@anjia(安佳)、@劉宇晨、@hxl(黃小璐)、@劉觀宇的辛苦審校,感謝你們對於文章結構和內容提出的寶貴建議。
參考文章
- Efficiently Rendering CSS
- How to write CSS for a great performance web application
- CSS performance revisited: selectors, bloat and expensive styles
- Avoiding Unnecessary Paints
- Five CSS Performance Tools to Speed up Your Website
- How and Why You Should Inline Your Critical CSS
- Render blocking css
- Modern Asynchronous CSS Loading
- Preload