一個(gè) CSS 屬性引發(fā)的血案Web 頁(yè)面性能是前端開(kāi)發(fā)特別需要關(guān)注的重點(diǎn),評(píng)判前端 Web 頁(yè)面性能的指標(biāo)有很多,頁(yè)面的流暢度是其中的一種,如何讓頁(yè)面變得 “柔順絲滑”,要討論起來(lái)可就是個(gè)相當(dāng)有料的話題了。之前開(kāi)發(fā)移動(dòng)端 H5 頁(yè)面的時(shí)候,就遇到過(guò)一個(gè)有趣的性能問(wèn)題 —— 某個(gè)賣(mài)場(chǎng)頁(yè)面在 IOS 手機(jī)上出現(xiàn)了嚴(yán)重的卡頓,但在安卓機(jī)型下卻表現(xiàn)得十分流暢。歸納一下在 iPhoneX 上測(cè)試的具體表現(xiàn):
根據(jù)這些表征情況不難推斷出,應(yīng)該是有什么東西在瘋狂占用 CPU,卡住了渲染進(jìn)程。 然而具體是什么東西,要問(wèn)我我也并不知道。對(duì)于這種沒(méi)法通過(guò)斷點(diǎn)定位到的問(wèn)題,恐怕只有用上祖師爺親傳的 “代碼二分法” 才能制服得了了。一番艱苦排查之后,問(wèn)題的根源終于聚焦到了下邊這行 CSS 代碼上: filter: blur(100px); 這行 CSS 代碼用于實(shí)現(xiàn)一個(gè)高斯模糊,來(lái)構(gòu)造一個(gè)優(yōu)惠券模塊的底部陰影。由于活動(dòng)配置了多個(gè)優(yōu)惠券,導(dǎo)致頁(yè)面里存在多個(gè)設(shè)置了這個(gè)屬性的 div 元素,而 IOS 手機(jī)的瀏覽器似乎對(duì)這個(gè)屬性的渲染十分吃力(然而為何吃力的原因不得而知),進(jìn)而導(dǎo)致渲染進(jìn)程的 CPU 占用率過(guò)高,最終造成卡頓。 哦?CPU 忙不過(guò)來(lái)了?好辦嘛!我給優(yōu)惠券模塊又加了這樣一行代碼,然后問(wèn)題迎刃而解 ......
你沒(méi)看錯(cuò),我也沒(méi)寫(xiě)少,確實(shí)就是靠一行代碼解決的。 認(rèn)識(shí)它的人可能已經(jīng)看出來(lái)了,大致原理其實(shí)很簡(jiǎn)單,這行代碼能夠開(kāi)啟 GPU 加速頁(yè)面渲染,從而大大降低了 CPU 的負(fù)載壓力,達(dá)到優(yōu)化頁(yè)面渲染性能的目的,不了解 CSS 硬件加速的可以看看這篇文章 Increase Your Site’s Performance with Hardware-Accelerated CSS[1] 。 問(wèn)題解決了,但是真的就這么完事了嗎?本著 “拔樹(shù)尋根” 的偉大原則,我把這個(gè)東西好好地研究了一番,才發(fā)現(xiàn) GPU 加速其實(shí)沒(méi)那么簡(jiǎn)單。 瀏覽器渲染流程在具體討論原理之前,我們需要了解一下瀏覽器渲染流程的一些基本概念。瀏覽器渲染流程是個(gè)老生常談的話題了,對(duì)于 “瀏覽器如何呈現(xiàn)一個(gè)頁(yè)面的內(nèi)容” 的這類問(wèn)題,不少人都可以講出一個(gè)相對(duì)完整的過(guò)程,從網(wǎng)絡(luò)請(qǐng)求到瀏覽器解析,可以具體到很多的細(xì)節(jié)。除去網(wǎng)絡(luò)資源獲取的步驟,我們理解的 Web 頁(yè)面的展示,一般可以分為
這是一個(gè)基本的瀏覽器從解析到繪制一個(gè) Web 頁(yè)面的過(guò)程,跟上邊頁(yè)面卡頓問(wèn)題的解決方法相關(guān)的,主要是最后一個(gè)環(huán)節(jié) —— 渲染層合成。 渲染層合成一、什么是渲染層合成在 DOM 樹(shù)中每個(gè)節(jié)點(diǎn)都會(huì)對(duì)應(yīng)一個(gè)渲染對(duì)象(RenderObject),當(dāng)它們的渲染對(duì)象處于相同的坐標(biāo)空間(z 軸空間)時(shí),就會(huì)形成一個(gè) RenderLayers,也就是渲染層。渲染層將保證頁(yè)面元素以正確的順序堆疊,這時(shí)候就會(huì)出現(xiàn)層合成(composite),從而正確處理透明元素和重疊元素的顯示。 這個(gè)模型類似于 Photoshop 的圖層模型,在 Photoshop 中,每個(gè)設(shè)計(jì)元素都是一個(gè)獨(dú)立的圖層,多個(gè)圖層以恰當(dāng)?shù)捻樞蛟?z 軸空間上疊加,最終構(gòu)成一個(gè)完整的設(shè)計(jì)圖。 對(duì)于有位置重疊的元素的頁(yè)面,這個(gè)過(guò)程尤其重要,因?yàn)橐坏﹫D層的合并順序出錯(cuò),將會(huì)導(dǎo)致元素顯示異常。 二、瀏覽器的渲染原理從瀏覽器的渲染過(guò)程中我們知道,頁(yè)面 HTML 會(huì)被解析成 DOM 樹(shù),每個(gè) HTML 元素對(duì)應(yīng)了樹(shù)結(jié)構(gòu)上的一個(gè) node 節(jié)點(diǎn)。而從 DOM 樹(shù)轉(zhuǎn)化到一個(gè)個(gè)的渲染層,并最終執(zhí)行合并、繪制的過(guò)程,中間其實(shí)還存在一些過(guò)渡的數(shù)據(jù)結(jié)構(gòu),它們記錄了 DOM 樹(shù)到屏幕圖形的轉(zhuǎn)化原理,其本質(zhì)也就是樹(shù)結(jié)構(gòu)到層結(jié)構(gòu)的演化。 1、渲染對(duì)象(RenderObject)一個(gè) DOM 節(jié)點(diǎn)對(duì)應(yīng)了一個(gè)渲染對(duì)象,渲染對(duì)象依然維持著 DOM 樹(shù)的樹(shù)形結(jié)構(gòu)。一個(gè)渲染對(duì)象知道如何繪制一個(gè) DOM 節(jié)點(diǎn)的內(nèi)容,它通過(guò)向一個(gè)繪圖上下文(GraphicsContext)發(fā)出必要的繪制調(diào)用來(lái)繪制 DOM 節(jié)點(diǎn)。 2、渲染層(RenderLayer)這是瀏覽器渲染期間構(gòu)建的第一個(gè)層模型,處于相同坐標(biāo)空間(z 軸空間)的渲染對(duì)象,都將歸并到同一個(gè)渲染層中,因此根據(jù)層疊上下文,不同坐標(biāo)空間的的渲染對(duì)象將形成多個(gè)渲染層,以體現(xiàn)它們的層疊關(guān)系。所以,對(duì)于滿足形成層疊上下文條件的渲染對(duì)象,瀏覽器會(huì)自動(dòng)為其創(chuàng)建新的渲染層。能夠?qū)е聻g覽器為其創(chuàng)建新的渲染層的,包括以下幾類常見(jiàn)的情況:
DOM 節(jié)點(diǎn)和渲染對(duì)象是一一對(duì)應(yīng)的,滿足以上條件的渲染對(duì)象就能擁有獨(dú)立的渲染層。當(dāng)然這里的獨(dú)立是不完全準(zhǔn)確的,并不代表著它們完全獨(dú)享了渲染層,由于不滿足上述條件的渲染對(duì)象將會(huì)與其第一個(gè)擁有渲染層的父元素共用同一個(gè)渲染層,因此實(shí)際上,這些渲染對(duì)象會(huì)與它的部分子元素共用這個(gè)渲染層。 3、圖形層(GraphicsLayer)GraphicsLayer 其實(shí)是一個(gè)負(fù)責(zé)生成最終準(zhǔn)備呈現(xiàn)的內(nèi)容圖形的層模型,它擁有一個(gè)圖形上下文(GraphicsContext),GraphicsContext 會(huì)負(fù)責(zé)輸出該層的位圖。存儲(chǔ)在共享內(nèi)存中的位圖將作為紋理上傳到 GPU,最后由 GPU 將多個(gè)位圖進(jìn)行合成,然后繪制到屏幕上,此時(shí),我們的頁(yè)面也就展現(xiàn)到了屏幕上。 所以 GraphicsLayer 是一個(gè)重要的渲染載體和工具,但它并不直接處理渲染層,而是處理合成層。 4、合成層(CompositingLayer)滿足某些特殊條件的渲染層,會(huì)被瀏覽器自動(dòng)提升為合成層。合成層擁有單獨(dú)的 GraphicsLayer,而其他不是合成層的渲染層,則和其第一個(gè)擁有 GraphicsLayer 的父層共用一個(gè)。 那么一個(gè)渲染層滿足哪些特殊條件時(shí),才能被提升為合成層呢?這里列舉了一些常見(jiàn)的情況:
因此,文首例子的解決方案,其實(shí)就是利用 will-change 屬性,將 CPU 消耗高的渲染元素提升為一個(gè)新的合成層,才能開(kāi)啟 GPU 加速的,因此你也可以使用 這里值得注意的是,不少人會(huì)將這些合成層的條件和渲染層產(chǎn)生的條件混淆,這兩種條件發(fā)生在兩個(gè)不同的層處理環(huán)節(jié),是完全不一樣的。 另外,有些文章會(huì)把 CSS Filter 也列為影響 Composite 的因素之一,然而我驗(yàn)證后發(fā)現(xiàn)并沒(méi)有效果。 三、隱式合成上邊提到,滿足某些顯性的特殊條件時(shí),渲染層會(huì)被瀏覽器提升為合成層。除此之外,在瀏覽器的 Composite 階段,還存在一種隱式合成,部分渲染層在一些特定場(chǎng)景下,會(huì)被默認(rèn)提升為合成層。 對(duì)于隱式合成,CSS GPU Animation[2] 中是這么描述的:
這句話可能不好理解,它其實(shí)是在描述一個(gè)交疊問(wèn)題(overlap)。舉個(gè)例子說(shuō)明一下:
四、層爆炸和層壓縮1、層爆炸從上邊的研究中我們可以發(fā)現(xiàn),一些產(chǎn)生合成層的原因太過(guò)于隱蔽了,尤其是隱式合成。在平時(shí)的開(kāi)發(fā)過(guò)程中,我們很少會(huì)去關(guān)注層合成的問(wèn)題,很容易就產(chǎn)生一些不在預(yù)期范圍內(nèi)的合成層,當(dāng)這些不符合預(yù)期的合成層達(dá)到一定量級(jí)時(shí),就會(huì)變成層爆炸。 層爆炸會(huì)占用 GPU 和大量的內(nèi)存資源,嚴(yán)重?fù)p耗頁(yè)面性能,因此盲目地使用 GPU 加速,結(jié)果有可能會(huì)是適得其反。CSS3 硬件加速也有坑[3] 這篇文章提供了一個(gè)很有趣的 DEMO[4],這個(gè) DEMO 頁(yè)面中包含了一個(gè) h1 標(biāo)題,它對(duì) transform 應(yīng)用了 animation 動(dòng)畫(huà),進(jìn)而導(dǎo)致被放到了合成層中渲染。由于 animation transform 的特殊性(動(dòng)態(tài)交疊不確定),隱式合成在不需要交疊的情況下也能發(fā)生,就導(dǎo)致了頁(yè)面中所有 消除隱式合成就是要消除元素交疊,拿這個(gè) DEMO 來(lái)說(shuō),我們只需要給 h1 標(biāo)題的 2、層壓縮當(dāng)然了,面對(duì)這種問(wèn)題,瀏覽器也有相應(yīng)的應(yīng)對(duì)策略,如果多個(gè)渲染層同一個(gè)合成層重疊時(shí),這些渲染層會(huì)被壓縮到一個(gè) GraphicsLayer 中,以防止由于重疊原因?qū)е驴赡艹霈F(xiàn)的“層爆炸”。這句話不好理解,具體可以看看這個(gè)例子:
當(dāng)然了,瀏覽器的自動(dòng)層壓縮并不是萬(wàn)能的,有很多特定情況下,瀏覽器是無(wú)法進(jìn)行層壓縮的,無(wú)線性能優(yōu)化:Composite[5] 這篇文章列舉了許多詳細(xì)的場(chǎng)景。 基于層合成的頁(yè)面渲染優(yōu)化一、層合成的得與失層合成是一個(gè)相對(duì)復(fù)雜的瀏覽器特性,為什么我們需要關(guān)注這么底層又難理解的東西呢?那是因?yàn)殇秩緦犹嵘秊楹铣蓪又?,?huì)給我們帶來(lái)不少好處:
當(dāng)然了,利弊是相對(duì)和共存的,層合成也存在一些缺點(diǎn),這很多時(shí)候也成為了我們網(wǎng)頁(yè)性能問(wèn)題的根源所在:
二、Chrome Devtools 如何查看合成層層合成的特性給我們提供了一個(gè)利用終端硬件能力來(lái)優(yōu)化頁(yè)面性能的方式,對(duì)于一些重交互、重動(dòng)畫(huà)的頁(yè)面,合理地利用層合成可以讓頁(yè)面的渲染效率得到極大提升,改善交互體驗(yàn)。而我們需要關(guān)注的是如何規(guī)避層合成對(duì)頁(yè)面造成的負(fù)面影響,或者換個(gè)說(shuō)法來(lái)講,更多時(shí)候是如何權(quán)衡利害,合理組織頁(yè)面的合成層,這就要求我們事先要對(duì)頁(yè)面的層合成情況有一個(gè)詳細(xì)的了解。Chrome Devtools 給我們提供了一些工具,可以方便的查看頁(yè)面的合成層情況。 首先是看看頁(yè)面的渲染情況,以一個(gè)欄目頁(yè)為例,點(diǎn)擊 這還不夠,我們還需要更加詳盡的層合成情況,點(diǎn)擊 左側(cè)列出了所有提升為獨(dú)立合成層的元素,右側(cè)則是一個(gè)整體合成層邊界視圖,以及選定合成層的詳細(xì)情況,包括以下幾個(gè)比較關(guān)鍵的信息:
可以看出我們?cè)诓唤?jīng)意間就已經(jīng)制造出了很多意料之外的合成層,這些沒(méi)有實(shí)際意義的合成層都是可以被優(yōu)化的。 三、一些優(yōu)化建議1、動(dòng)畫(huà)使用 transform 實(shí)現(xiàn)對(duì)于一些體驗(yàn)要求較高的關(guān)鍵動(dòng)畫(huà),比如一些交互復(fù)雜的玩法頁(yè)面,存在持續(xù)變化位置的 animation 元素,我們最好是使用 transform 來(lái)實(shí)現(xiàn)而不是通過(guò)改變 left/top 的方式。這樣做的原因是,如果使用 left/top 來(lái)實(shí)現(xiàn)位置變化,animation 節(jié)點(diǎn)和 Document 將被放到了同一個(gè) GraphicsLayer 中進(jìn)行渲染,持續(xù)的動(dòng)畫(huà)效果將導(dǎo)致整個(gè) Document 不斷地執(zhí)行重繪,而使用 transform 的話,能夠讓 animation 節(jié)點(diǎn)被放置到一個(gè)獨(dú)立合成層中進(jìn)行渲染繪制,動(dòng)畫(huà)發(fā)生時(shí)不會(huì)影響到其它層。并且另一方面,動(dòng)畫(huà)會(huì)完全運(yùn)行在 GPU 上,相比起 CPU 處理圖層后再發(fā)送給顯卡進(jìn)行顯示繪制來(lái)說(shuō),這樣的動(dòng)畫(huà)往往更加流暢。 2、減少隱式合成雖然隱式合成從根本上來(lái)說(shuō)是為了保證正確的圖層重疊順序,但具體到實(shí)際開(kāi)發(fā)中,隱式合成很容易就導(dǎo)致一些無(wú)意義的合成層生成,歸根結(jié)底其實(shí)就要求我們?cè)陂_(kāi)發(fā)時(shí)約束自己的布局習(xí)慣,避免踩坑。 比如上邊提到的欄目頁(yè)面,就因?yàn)槠綍r(shí)開(kāi)發(fā)的不注意造成頁(yè)面生成了過(guò)多的合成層,我在試圖查看頁(yè)面合成層情況的時(shí)候,在 PC 上已經(jīng)能明顯感到卡頓了。利用 Chrome Devtools 分析之后不難發(fā)現(xiàn),頁(yè)面里邊存在的一個(gè)帶動(dòng)畫(huà) transform 的 button 按鈕,提升為了合成層,動(dòng)畫(huà)交疊的不確定性使得頁(yè)面內(nèi)其他 這個(gè)時(shí)候我們只需要把這個(gè)動(dòng)畫(huà)節(jié)點(diǎn)的 改善后的頁(yè)面效果如下,可以看到相比優(yōu)化前,我們消除了很多無(wú)意義的合成層。 ![]() 3、減小合成層的尺寸舉個(gè)簡(jiǎn)單的例子,分別畫(huà)兩個(gè)尺寸一樣的 div,但實(shí)現(xiàn)方式有點(diǎn)差別:一個(gè)直接設(shè)置尺寸 100x100,另一個(gè)設(shè)置尺寸 10x10,然后通過(guò) <style> .bottom, .top { position: absolute; will-change: transform; } .bottom { width: 100px; height: 100px; top: 20px; left: 20px; z-index: 3; background: rosybrown; } .top { width: 10px; height: 10px; transform: scale(10); top: 200px; left: 200px; z-index: 5; background: indianred; } </style> <body> <div class='bottom'></div> <div class='top'></div> </body> 利用 Chrome Devtools 查看這兩個(gè)合成層的內(nèi)存占用后發(fā)現(xiàn), |
|
來(lái)自: 西北望msm66g9f > 《編程》