原文鏈接?http://web.jobbole.com/92871/
瀏覽器 101:像素是怎么來(lái)的
在深入研究之前,我們要先搞清楚一個(gè)很重要的問(wèn)題:瀏覽器是怎么把代碼轉(zhuǎn)化成為用戶可見(jiàn)的像素點(diǎn)呢斩郎?
首次加載時(shí)悍汛,瀏覽器會(huì)下載并解析 HTML,將 HTML 元素轉(zhuǎn)變?yōu)橐粋€(gè) DOM 節(jié)點(diǎn)的「內(nèi)容樹(shù)」(content tree)娃善。除此之外憔涉,樣式同樣會(huì)被解析生成「渲染樹(shù)」 (render tree)。為了提升性能链嘀,渲染引擎會(huì)分開(kāi)完成這些工作,甚至?xí)霈F(xiàn)渲染樹(shù)比 DOM 樹(shù)更快生成出來(lái)档玻。
布局
渲染樹(shù)生成后怀泊,瀏覽器會(huì)從頁(yè)面左上角開(kāi)始迭代地計(jì)算出每個(gè)元素尺寸和位置,最終生成布局误趴。這個(gè)過(guò)程可能是一氣呵成的霹琼,但也可能由于元素的排列導(dǎo)致反復(fù)地繪制。元素間的位置關(guān)系都緊密相關(guān)凉当。為了優(yōu)化必要的任務(wù)枣申,瀏覽器會(huì)追蹤元素的變化情況,并將這些元素以及它們的子節(jié)點(diǎn)標(biāo)記為 ‘dirty’(臟元素)看杭。但是元素間耦合緊密忠藤,任何布局上的改變代價(jià)都是重大的,應(yīng)該盡量避免楼雹。
繪制
生成布局后模孩,瀏覽器將頁(yè)面繪制到屏幕上尖阔。這個(gè)環(huán)節(jié)和「布局」步驟類似,瀏覽器會(huì)追蹤臟元素榨咐,將它們合并到一個(gè)超大的矩形區(qū)域中介却。每一幀內(nèi)只會(huì)發(fā)生一次重繪,用于繪制這個(gè)被污染區(qū)域块茁。重繪也會(huì)消耗大量性能齿坷,能免則免。
復(fù)合
最后一步龟劲,將所有繪制好的元素進(jìn)行復(fù)合胃夏。默認(rèn)情況下,所有元素將會(huì)被繪制到同一個(gè)層中昌跌;如果將元素分開(kāi)到不同的復(fù)合層中仰禀,更新元素對(duì)性能友好,不在同一層的元素不容易受到影響蚕愤。CPU 繪制層答恶,GPU 生成層∑加眨基礎(chǔ)繪圖操作在硬件加速合成中完成效率高悬嗓。層的分離允許非破壞性的改變,正如你所猜測(cè)的裕坊,GPU 復(fù)合層上的改變代價(jià)最小性能消耗最少包竹。
激發(fā)創(chuàng)造力
一般情況下,更改復(fù)合層是相對(duì)消耗性能較少的一個(gè)操作籍凝,所以盡量通過(guò)改變opacity和transform的值觸發(fā)復(fù)合層繪制周瞎。看起來(lái)好像…我們能做出的效果會(huì)很有限饵蒂,但真的是這樣嗎声诸?要好好開(kāi)發(fā)自己的創(chuàng)造力哦。
變換
「變換」為元素提供了無(wú)限的可能性:位置可以改變 (translateX,translateY, 或translate3d)退盯、大小也可以通過(guò)縮放 (scale) 改變彼乌、還能旋轉(zhuǎn)、斜切甚至 3D 變換渊迁。就是在某些場(chǎng)景下慰照,開(kāi)發(fā)者需要換一種思考方式,通過(guò)使用變換減少重排和重繪琉朽。 比如給一個(gè)元素添加 active 類名后它會(huì)向左移動(dòng) 10px毒租,可以通過(guò)改變 left 屬性:
透明度
可以通過(guò)改變opacity的值,實(shí)現(xiàn)元素的顯示和隱藏(與改變display或者visibility的值達(dá)到類似的效果類似漓骚,但性能更好)蝌衔。比如實(shí)現(xiàn)菜單的切換效果:菜單展開(kāi)時(shí),opacity值為1蝌蹂;收起時(shí)噩斟,opacity值變?yōu)?0。要注意的是pointer-events的值也要隨之改變孤个,防止用戶操作到明明收起的菜單剃允。closed 類名會(huì)根據(jù)用戶點(diǎn)擊 ‘open’ 時(shí),closed 類名會(huì)被加上齐鲤;點(diǎn)擊 ‘close’ 按鈕時(shí)斥废,closed 類名會(huì)被移除。對(duì)應(yīng)的代碼是這樣的:
另外给郊,透明度可變意味著開(kāi)發(fā)者可以控制元素的可見(jiàn)程度牡肉。多多思考應(yīng)用透明度的場(chǎng)景 — 比如直接給元素的陰影 (box-shadow) 做動(dòng)效很可能會(huì)造成嚴(yán)重的性能問(wèn)題:
如果把陰影放到偽元素上,控制偽元素的透明度從而控制陰影淆九,效果一樣但性能更好统锤,代碼如下:
手動(dòng)優(yōu)化
還有一個(gè)好消息 — 開(kāi)發(fā)者可以選擇想要控制的屬性,創(chuàng)建復(fù)合層炭庙,并將元素拖到該層饲窿。通過(guò)手動(dòng)優(yōu)化,確保元素總能被繪制好焕蹄,這也是通知瀏覽器準(zhǔn)備繪制該元素的最簡(jiǎn)單方式逾雄。需要獨(dú)立層的場(chǎng)景包括:元素的狀態(tài)將發(fā)生一些變化(比如動(dòng)畫)、改變了很消耗性能的樣式(比如position:fixed和overflow:scroll)腻脏⊙挥荆可能你也見(jiàn)過(guò)了糟糕的性能導(dǎo)致了頁(yè)面閃爍、震動(dòng)…或其他不如預(yù)期的效果迹卢,例如移動(dòng)端常見(jiàn)的固定在視口頂部的頭部辽故,會(huì)在頁(yè)面滾動(dòng)的時(shí)候閃爍。將這樣的元素獨(dú)立到自己的復(fù)合層腐碱,就是常見(jiàn)的解決這類問(wèn)題的方法誊垢。
hack 方法
從前,開(kāi)發(fā)者通常是通過(guò)backface-visibility:hidden或者trasform: translate3d(0,0,0)觸發(fā)瀏覽器生成新的復(fù)合層症见,但這并不是標(biāo)準(zhǔn)的寫法喂走,這兩種寫法也對(duì)元素的視覺(jué)效果不起作用。
新方法
現(xiàn)在有了will-change谋作,它能夠顯式地通知瀏覽器對(duì)某一個(gè)元素的某個(gè)或某些元素做渲染優(yōu)化芋肠。will-change接收各種各樣的屬性值,比如一個(gè)或多個(gè) CSS 屬性 (transform,opacity)遵蚜、contents或者scroll-position帖池。不過(guò)最常用值可能就是auto奈惑,這個(gè)值表示的是瀏覽器將進(jìn)行默認(rèn)的優(yōu)化:
優(yōu)化有度,我們總能聽(tīng)到關(guān)于「復(fù)合層過(guò)多反而阻礙渲染」的討論睡汹。因?yàn)闉g覽器已經(jīng)為優(yōu)化做了能做的一切肴甸,will-change的性能優(yōu)化方案本身對(duì)資源要求很高。如果瀏覽器持續(xù)在執(zhí)行某個(gè)元素的will-change囚巴,就意味著瀏覽器要持續(xù)對(duì)這個(gè)元素的進(jìn)行優(yōu)化原在,性能消耗造成頁(yè)面卡頓。過(guò)多的復(fù)合層降低頁(yè)面性能的現(xiàn)象在移動(dòng)端很常見(jiàn)彤叉。
動(dòng)畫方法
想要元素動(dòng)起來(lái)可以用 CSS(聲明式)庶柿,也可以使用 JavaScript(命令式),按需選擇秽浇。
聲明式動(dòng)畫
CSS 動(dòng)畫是聲明式的(告訴瀏覽器要做什么)浮庐,瀏覽器需要知道動(dòng)畫的起始狀態(tài)和終止?fàn)顟B(tài),這樣它才知道如何優(yōu)化兼呵。CSS 動(dòng)畫不是在主線程中執(zhí)行兔辅,不會(huì)妨礙主線程中的任務(wù)執(zhí)行』魑梗總的來(lái)說(shuō)维苔,CSS 動(dòng)畫對(duì)性能更友好。關(guān)鍵幀的動(dòng)畫組合提供了相當(dāng)豐富的視覺(jué)效果懂昂,比如下面是一個(gè)元素的無(wú)限旋轉(zhuǎn)動(dòng)畫:
但 CSS 動(dòng)畫缺乏 JS 的表達(dá)能力介时,將兩者結(jié)合起來(lái)效果更好:比如用 JS 監(jiān)聽(tīng)用戶輸入,根據(jù)動(dòng)作切換類名凌彬。類名對(duì)應(yīng)著不同的動(dòng)畫效果沸柔。下面的代碼實(shí)現(xiàn)的是當(dāng)元素被點(diǎn)擊時(shí)切換類名:
值得一提的是,如果你在操作「出血」(注:設(shè)計(jì)中在畫布四邊留出的一定區(qū)域稱為「出血」)時(shí)铲敛,新的Web Animation API會(huì)利用 CSS 的性能褐澎。通過(guò)這個(gè) API察藐,開(kāi)發(fā)者能輕松地在性能友好的基礎(chǔ)上處理動(dòng)畫的同步和時(shí)間問(wèn)題凉袱。
命令式動(dòng)畫
命令式動(dòng)畫告訴瀏覽器如何去演繹動(dòng)畫。CSS 動(dòng)畫代碼在某些場(chǎng)景下會(huì)變得很臃腫讲冠,或者需要更多的交互控制先鱼,此時(shí) JS 就要介入了俭正。注意!和 CSS 動(dòng)畫不同焙畔,JS 動(dòng)畫是在主線程中執(zhí)行的(也就是說(shuō)丟幀的可能性大于 CSS 動(dòng)畫的)掸读,性能相對(duì)差一些。在使用 JS 動(dòng)畫的場(chǎng)景中,考慮范圍中的性能之選比較少儿惫。
requestAnimationFrame
requestAnimationFrame對(duì)性能友好澡罚,你可以將它視作setTimeout的進(jìn)化版,不過(guò)這其實(shí)是一個(gè)動(dòng)畫執(zhí)行的 API肾请。理論上調(diào)用了這個(gè) API 就能保證 60fps 的幀率始苇,但實(shí)踐證明這個(gè)函數(shù)是請(qǐng)求在下一次可用時(shí)繪制動(dòng)畫,也就是并沒(méi)有固定的時(shí)間間隔筐喳。瀏覽器會(huì)把頁(yè)面上發(fā)生的變化組合接著一次繪制,而不會(huì)為每一次變化都進(jìn)行繪制函喉,通過(guò)這個(gè)方式提升 CPU 的使用率避归。 RAF 可以遞歸地使用:
另外,類似縮放窗口或頁(yè)面滾動(dòng)這樣的場(chǎng)景管呵,直接綁定事件是相對(duì)消耗性能的梳毙,開(kāi)發(fā)者可以考慮在類似情況下用 RAF 提升性能。
滾動(dòng)
實(shí)現(xiàn)性能良好的平滑滾動(dòng)可是個(gè)挑戰(zhàn)捐下。幸運(yùn)的是账锹,最近規(guī)范提供一些可配置選項(xiàng)。開(kāi)發(fā)者不再需要通過(guò)禁止瀏覽器默認(rèn)行為 (preventDefault)坷襟,開(kāi)啟Passive event listeners即可提升滾動(dòng)性能(聲明之后奸柬,就不需要通過(guò)阻止元素的 touch 事件監(jiān)聽(tīng)和鼠標(biāo)滾輪事件監(jiān)聽(tīng)以優(yōu)化滾動(dòng)性能)。使用方法僅是在需要的監(jiān)聽(tīng)器中聲明{passive: true}:
從 Chrome 56 開(kāi)始婴程,這個(gè)選項(xiàng)將在touchmove和touchstart中默認(rèn)開(kāi)啟廓奕。
新出的Intersection Observer API能夠告訴開(kāi)發(fā)者某個(gè)元素是不是在視口內(nèi),或者是不是和其他元素有交互档叔。和通過(guò)事件處理這種會(huì)阻塞主線程的交互方式相比桌粉,Intersection Observer API 可以監(jiān)聽(tīng)元素,只有當(dāng)元素交叉路徑的時(shí)候才會(huì)執(zhí)行相應(yīng)操作衙四。這個(gè) API 在無(wú)限滾動(dòng)和懶加載的場(chǎng)景都可以使用铃肯。
先讀后寫
不斷地讀寫 DOM 會(huì)導(dǎo)致「強(qiáng)制同步布局」(forced synchronous layouts),不過(guò)在技術(shù)發(fā)展過(guò)程中它演變成了更形象的詞 — 「布局抖動(dòng)」(layout thrashing)传蹈。前文也有提到押逼,瀏覽器會(huì)追蹤「臟元素」,在合適的時(shí)候?qū)⒆儞Q過(guò)程儲(chǔ)存起來(lái)卡睦。在讀取了特定屬性以后宴胧,開(kāi)發(fā)者可以強(qiáng)制瀏覽器提前計(jì)算。這樣反復(fù)的讀寫會(huì)導(dǎo)致重排表锻。幸運(yùn)的是有一個(gè)簡(jiǎn)單的解決方式:讀完再寫恕齐。
為了模擬上述效果,請(qǐng)看下面這個(gè)對(duì)讀寫有嚴(yán)苛要求的例子:
將「讀」放到forEach外面,而不是和「寫」一起在每個(gè)迭代里都執(zhí)行显歧,就能提高性能:
優(yōu)化的未來(lái)
瀏覽器在性能優(yōu)化方面持續(xù)投入了越來(lái)越多的精力仪或。通過(guò)新屬性contain可以聲明一個(gè)元素的子樹(shù)獨(dú)立于頁(yè)面的其他元素(目前只有 Chrome 和 Opera 支持該屬性)。這就等于告訴了瀏覽器「這個(gè)元素是安全的士骤,它不會(huì)影響到其他元素」范删。contain的屬性值根據(jù)變化的范圍確定,可以是strict拷肌、content到旦、size、layout巨缘、style或者paint添忘。這確保了子樹(shù)被更新的時(shí)候,不會(huì)造成父元素的重排若锁。特別是在引入第三方控件的時(shí)候:
性能測(cè)試
知道了如何優(yōu)化頁(yè)面性能后搁骑,還要做性能測(cè)試才行。依我之見(jiàn)又固,Chrome 開(kāi)發(fā)者工具就是最棒的測(cè)試工具仲器。在 ‘More Tools’ 中有一個(gè) ‘Rendering’ 面板,其中包含了一些選項(xiàng):比如追蹤「臟元素」仰冠、計(jì)算每秒的幀率乏冀、高亮每層的邊界還有監(jiān)測(cè)滾動(dòng)性能問(wèn)題。
‘Performance’ 面板中的 ‘Timeline’ 工具能記錄動(dòng)畫過(guò)程洋只,開(kāi)發(fā)者可以直接定位到出問(wèn)題的部分煤辨。很簡(jiǎn)單,紅色表示有問(wèn)題木张,綠色表示渲染正常众辨。開(kāi)發(fā)者可以直接點(diǎn)擊紅色區(qū)域,看看是哪個(gè)函數(shù)造成了性能問(wèn)題的函數(shù)舷礼。
另一個(gè)有趣的工具是在 ‘Caputrue Settings’ 中的 ‘CPU throtting’鹃彻,開(kāi)發(fā)者可以通過(guò)這個(gè)選項(xiàng)模擬頁(yè)面運(yùn)行在一臺(tái)非常卡的設(shè)備上妻献。開(kāi)發(fā)者在桌面瀏覽器上測(cè)試頁(yè)面的時(shí)候效果可能很好蛛株,那是因?yàn)?PC 或者 Mac 的本身性能就優(yōu)于移動(dòng)設(shè)備。這個(gè)選項(xiàng)提供了很好的真機(jī)模擬育拨。
測(cè)試和迭代
動(dòng)畫性能優(yōu)化最簡(jiǎn)單的方案就是減少每一幀的工作量谨履。最有效緩解性能壓力的方法就是,盡量只更新在復(fù)合層中的元素熬丧,重新渲染復(fù)合層元素不容易影響到頁(yè)面上其他元素笋粟。性能優(yōu)化往往意味著反復(fù)地測(cè)試和驗(yàn)證,以及跳出慣性思維找到奇技淫巧實(shí)現(xiàn)高性能動(dòng)畫 — 無(wú)論怎么樣,最終受益的會(huì)是用戶和開(kāi)發(fā)者害捕。