CSS3 3D 行星運(yùn)轉(zhuǎn)以及瀏覽器渲染原理
最近入坑 Web 動(dòng)畫,所以把自己的學(xué)習(xí)過程記錄一下分享給大家。
CSS3 3D 行星運(yùn)轉(zhuǎn) demo 頁面請戳:Demo。(建議使用Chrome打開)
本文完整的代碼,以及更多的 CSS3 效果,在我 Github 上可以看到,也希望大家可以點(diǎn)個(gè) star。
嗯,可能有些人打不開 demo 或者頁面亂了,貼幾張效果圖:(圖片有點(diǎn)大,耐心等待一會(huì))
CSS3 3D 行星運(yùn)轉(zhuǎn)效果圖
隨機(jī)再截屏了一張:
強(qiáng)烈建議你點(diǎn)進(jìn) Demo 頁感受一下 CSS3 3D 的魅力,圖片能展現(xiàn)的東西畢竟有限。
然后,這個(gè) CSS3 3D 行星運(yùn)轉(zhuǎn)動(dòng)畫的制作過程不再詳細(xì)贅述,本篇的重點(diǎn)放在 Web 動(dòng)畫介紹及性能優(yōu)化方面。詳細(xì)的 CSS3 3D 可以回看上一篇博客:【CSS3進(jìn)階】酷炫的3D旋轉(zhuǎn)透視。簡單的思路:
1. 利用上一篇所制作的 3D 照片墻為原型,改造而來;
2. 每一個(gè)球體的制作,想了許多方法,最終使用了這種折中的方式,每一個(gè)球體本身也是一個(gè) CSS3 3D 圖形。然后在制作過程中使用 Sass 編寫 CSS 可以減少很多繁瑣的編寫 CSS 動(dòng)畫的過程;
3. Demo 當(dāng)中有使用 Javascript 寫了一個(gè)鼠標(biāo)跟隨的監(jiān)聽事件,去掉這個(gè)事件,整個(gè)行星運(yùn)動(dòng)動(dòng)畫本身是純 CSS 實(shí)現(xiàn)的。
下面將進(jìn)入本文的重點(diǎn),從性能優(yōu)化的角度講講瀏覽器渲染展示原理,瀏覽器的重繪與重排,動(dòng)畫的性能檢測優(yōu)化等:
瀏覽器渲染展示原理及對web動(dòng)畫的影響
小標(biāo)題起得有點(diǎn)大,我們知道,不同瀏覽器的內(nèi)核(渲染引擎,Rendering Engine)是不一樣的,例如現(xiàn)在最主流的 chrome 瀏覽器的內(nèi)核是 Blink 內(nèi)核(在Chrome(28及往后版本)、Opera(15及往后版本)和Yandex瀏覽器中使用),火狐是 Gecko,IE 是 Trident ,瀏覽器內(nèi)核負(fù)責(zé)對網(wǎng)頁語法的解釋并渲染(顯示)網(wǎng)頁,不同瀏覽器內(nèi)核的工作原理并不完全一致。
所以其實(shí)下面將主要討論的是 chrome 瀏覽器下的渲染原理。因?yàn)?chrome 內(nèi)核渲染可查證的資料較多,對于其他內(nèi)核的瀏覽器不敢妄下定論,所以下面展開的討論默認(rèn)是針對 chrome 瀏覽器的。
首先,我要拋出一點(diǎn)結(jié)論:
使用 transform3d api 代替 transform api,強(qiáng)制開始 GPU 加速
這里談到了 GPU 加速,為什么 GPU 能夠加速 3D 變換?這一切又必須要從瀏覽器底層的渲染講起,瀏覽器渲染展示網(wǎng)頁的過程,老生常談,面試必問,大致分為:
1. 解析HTML(HTML Parser)
2. 構(gòu)建DOM樹(DOM Tree)
3. 渲染樹構(gòu)建(Render Tree)
4. 繪制渲染樹(Painting)
找到了一張很經(jīng)典的圖:
這個(gè)渲染過程作為一個(gè)基礎(chǔ)知識(shí),繼續(xù)往下深入。
當(dāng)頁面加載并解析完畢后,它在瀏覽器內(nèi)代表了一個(gè)大家十分熟悉的結(jié)構(gòu):DOM(Document Object Model,文檔對象模型)。在瀏覽器渲染一個(gè)頁面時(shí),它使用了許多沒有暴露給開發(fā)者的中間表現(xiàn)形式,其中最重要的結(jié)構(gòu)便是層(layer)。
這個(gè)層就是本文重點(diǎn)要討論的內(nèi)容:
而在 Chrome 中,存在有不同類型的層: RenderLayer(負(fù)責(zé) DOM 子樹),GraphicsLayer(負(fù)責(zé) RenderLayer 的子樹)。接下來我們所討論的將是 GraphicsLayer 層。
GraphicsLayer 層是作為紋理(texture)上傳給 GPU 的。
這里這個(gè)紋理很重要,那么,
什么是紋理(texture)?
這里的紋理指的是 GPU 的一個(gè)術(shù)語:可以把它想象成一個(gè)從主存儲(chǔ)器(例如 RAM)移動(dòng)到圖像存儲(chǔ)器(例如 GPU 中的 VRAM)的位圖圖像(bitmap image)。一旦它被移動(dòng)到 GPU 中,你可以將它匹配成一個(gè)網(wǎng)格幾何體(mesh geometry),在 Chrome 中使用紋理來從 GPU 上獲得大塊的頁面內(nèi)容。通過將紋理應(yīng)用到一個(gè)非常簡單的矩形網(wǎng)格就能很容易匹配不同的位置(position)和變形(transformation),這也就是 3D CSS 的工作原理。
說起來很難懂,直接看例子,在 chrome 中,我們是可以看到上文所述的 GraphicsLayer — 層的概念。在開發(fā)者工具中,我們進(jìn)行如下選擇調(diào)出 show layer borders 選項(xiàng):
在一個(gè)極簡單的頁面,我們可以看到如下所示,這個(gè)頁面只有一個(gè)層。藍(lán)色網(wǎng)格表示瓦片(tile),你可以把它們當(dāng)作是層的單元(并不是層),Chrome 可以將它們作為一個(gè)大層的部分上傳給 GPU:
元素自身層的創(chuàng)建
因?yàn)樯厦娴捻撁媸趾唵危圆]有產(chǎn)生層,但是在很復(fù)雜的頁面中,譬如我們給元素設(shè)置一個(gè) 3D CSS 屬性來變換它,我們就能看到當(dāng)元素?fù)碛凶约旱膶訒r(shí)是什么樣子。
注意橘黃色的邊框,它畫出了該視圖中層的輪廓:
何時(shí)觸發(fā)創(chuàng)建層 ?
上面示意圖中黃色邊框框住的層,就是 GraphicsLayer ,它對于我們的 Web 動(dòng)畫而言非常重要,通常,Chrome 會(huì)將一個(gè)層的內(nèi)容在作為紋理上傳到 GPU 前先繪制(paint)進(jìn)一個(gè)位圖中。如果內(nèi)容不會(huì)改變,那么就沒有必要重繪(repaint)層。
這樣做的意義在于:花在重繪上的時(shí)間可以用來做別的事情,例如運(yùn)行 JavaScript,如果繪制的時(shí)間很長,還會(huì)造成動(dòng)畫的故障與延遲。
那么一個(gè)元素什么時(shí)候會(huì)觸發(fā)創(chuàng)建一個(gè)層?從目前來說,滿足以下任意情況便會(huì)創(chuàng)建層:
-
3D 或透視變換(perspective、transform) CSS 屬性
-
使用加速視頻解碼的 <video> 元素
-
擁有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
-
混合插件(如 Flash)
-
對自己的 opacity 做 CSS 動(dòng)畫或使用一個(gè)動(dòng)畫變換的元素
-
擁有加速 CSS 過濾器的元素
-
元素有一個(gè)包含復(fù)合層的后代節(jié)點(diǎn)(換句話說,就是一個(gè)元素?fù)碛幸粋€(gè)子元素,該子元素在自己的層里)
-
元素有一個(gè) z-index 較低且包含一個(gè)復(fù)合層的兄弟元素(換句話說就是該元素在復(fù)合層上面渲染)
層的重繪
對于靜態(tài) Web 頁面而言,層在第一次被繪制出來之后將不會(huì)被改變,但對于 Web 動(dòng)畫,頁面的 DOM 元素是在不斷變換的,如果層的內(nèi)容在變換過程中發(fā)生了改變,那么層將會(huì)被重繪(repaint)。
強(qiáng)大的 chrome 開發(fā)者工具提供了工具讓我們可以查看到動(dòng)畫頁面運(yùn)行中,哪些內(nèi)容被重新繪制了:
在舊版的 chrome 中,是有 show paint rects 這一個(gè)選項(xiàng)的,可以查看頁面有哪些層被重繪了,并以紅色邊框標(biāo)識(shí)出來。
但是新版的 chrome 貌似把這個(gè)選項(xiàng)移除了,現(xiàn)在的選項(xiàng)是 enable paint flashing ,其作用也是標(biāo)識(shí)出網(wǎng)站動(dòng)態(tài)變換的地方,并且以綠色邊框標(biāo)識(shí)出來。
看上面的示意圖,可以看到頁面中有幾處綠色的框,表示發(fā)生了重繪。注意 Chrome 并不會(huì)始終重繪整個(gè)層,它會(huì)嘗試智能的去重繪 DOM 中失效的部分。
按照道理,頁面發(fā)生這么多動(dòng)畫,重繪應(yīng)該很頻繁才對,但是上圖我的行星動(dòng)畫中我只看到了寥寥綠色重繪框,我的個(gè)人理解是,一是 GPU 優(yōu)化,二是如果整個(gè)動(dòng)畫頁面只有一個(gè)層,那么運(yùn)用了 transform 進(jìn)行變換,頁面必然需要重繪,但是采用分層(GraphicsLayer )技術(shù),也就是上面說符合情況的元素各自創(chuàng)建層,那么一個(gè)元素所創(chuàng)建的層運(yùn)用 transform 變換,譬如 rotate 旋轉(zhuǎn),這個(gè)時(shí)候該層的旋轉(zhuǎn)變換并沒有影響到其他層,那么該層不一定需要被重繪。(個(gè)人之見,還請?zhí)岢鲋刚?/p>
了解層的重繪對 Web 動(dòng)畫的性能優(yōu)化至關(guān)重要。
是什么原因?qū)е率?invalidation)進(jìn)而強(qiáng)制重繪的呢?這個(gè)問題很難詳盡回答,因?yàn)榇嬖诖罅繉?dǎo)致邊界失效的情況。最常見的情況就是通過操作 CSS 樣式來修改 DOM 或?qū)е轮嘏拧?/p>
查找引發(fā)重繪和重排根源的最好辦法就是使用開發(fā)者工具的時(shí)間軸和 enable paint flashing 工具,然后試著找出恰好在重繪/重排前修改了 DOM 的地方。
總結(jié)
那么瀏覽器是如何從 DOM 元素到最終動(dòng)畫的展示呢?
-
瀏覽器解析 HTML 獲取 DOM 后分割為多個(gè)圖層(GraphicsLayer)
-
對每個(gè)圖層的節(jié)點(diǎn)計(jì)算樣式結(jié)果(Recalculate style–樣式重計(jì)算)
-
為每個(gè)節(jié)點(diǎn)生成圖形和位置(Layout–回流和重布局)
-
將每個(gè)節(jié)點(diǎn)繪制填充到圖層位圖中(Paint Setup和Paint–重繪)
-
圖層作為紋理(texture)上傳至 GPU
-
符合多個(gè)圖層到頁面上生成最終屏幕圖像(Composite Layers–圖層重組)
Web 動(dòng)畫很大一部分開銷在于層的重繪,以層為基礎(chǔ)的復(fù)合模型對渲染性能有著深遠(yuǎn)的影響。當(dāng)不需要繪制時(shí),復(fù)合操作的開銷可以忽略不計(jì),因此在試著調(diào)試渲染性能問題時(shí),首要目標(biāo)就是要避免層的重繪。那么這就給動(dòng)畫的性能優(yōu)化提供了方向,減少元素的重繪與回流。
回流(reflow)與重繪(repaint)
這里首先要分清兩個(gè)概念,重繪與回流。
回流(reflow)
當(dāng)渲染樹(render Tree)中的一部分(或全部)因?yàn)樵氐囊?guī)模尺寸,布局,隱藏等改變而需要重新構(gòu)建。這就稱為回流(reflow),也就是重新布局(relayout)。
每個(gè)頁面至少需要一次回流,就是在頁面第一次加載的時(shí)候。在回流的時(shí)候,瀏覽器會(huì)使渲染樹中受到影響的部分失效,并重新構(gòu)造這部分渲染樹,完成回流后,瀏覽器會(huì)重新繪制受影響的部分到屏幕中,該過程成為重繪。
重繪(repaint)
當(dāng)render tree中的一些元素需要更新屬性,而這些屬性只是影響元素的外觀,風(fēng)格,而不會(huì)影響布局的,比如 background-color 。則就叫稱為重繪。
值得注意的是,回流必將引起重繪,而重繪不一定會(huì)引起回流。
明顯,回流的代價(jià)更大,簡單而言,當(dāng)操作元素會(huì)使元素修改它的大小或位置,那么就會(huì)發(fā)生回流。
回流何時(shí)觸發(fā):
-
調(diào)整窗口大小(Resizing the window)
-
改變字體(Changing the font)
-
增加或者移除樣式表(Adding or removing a stylesheet)
-
內(nèi)容變化,比如用戶在input框中輸入文字(Content changes, such as a user typing text in
-
an input box)
-
激活 CSS 偽類,比如 :hover (IE 中為兄弟結(jié)點(diǎn)偽類的激活)(Activation of CSS pseudo classes such as :hover (in IE the activation of the pseudo class of a sibling))
-
操作 class 屬性(Manipulating the class attribute)
-
腳本操作 DOM(A script manipulating the DOM)
-
計(jì)算 offsetWidth 和 offsetHeight 屬性(Calculating offsetWidth and offsetHeight)
-
設(shè)置 style 屬性的值 (Setting a property of the style attribute)
所以對于頁面而言,我們的宗旨就是盡量減少頁面的回流重繪,簡單的一個(gè)栗子:
// 下面這種方式將會(huì)導(dǎo)致回流reflow兩次
- var newWidth = aDiv.offsetWidth + 10; // Read
- aDiv.style.width = newWidth + 'px'; // Write
- var newHeight = aDiv.offsetHeight + 10; // Read
- aDiv.style.height = newHeight + 'px'; // Write
// 下面這種方式更好,只會(huì)回流reflow一次
- var newWidth = aDiv.offsetWidth + 10; // Read
- var newHeight = aDiv.offsetHeight + 10; // Read
- aDiv.style.width = newWidth + 'px'; // Write
- aDiv.style.height = newHeight + 'px'; // Write
上面四句,因?yàn)樯婕傲?offsetHeight 操作,瀏覽器強(qiáng)制 reflow 了兩次,而下面四句合并了 offset 操作,所以減少了一次頁面的回流。
減少回流、重繪其實(shí)就是需要減少對渲染樹的操作(合并多次多DOM和樣式的修改),并減少對一些style信息的請求,盡量利用好瀏覽器的優(yōu)化策略。
flush隊(duì)列
其實(shí)瀏覽器自身是有優(yōu)化策略的,如果每句 Javascript 都去操作 DOM 使之進(jìn)行回流重繪的話,瀏覽器可能就會(huì)受不了。所以很多瀏覽器都會(huì)優(yōu)化這些操作,瀏覽器會(huì)維護(hù) 1 個(gè)隊(duì)列,把所有會(huì)引起回流、重繪的操作放入這個(gè)隊(duì)列,等隊(duì)列中的操作到了一定的數(shù)量或者到了一定的時(shí)間間隔,瀏覽器就會(huì) flush 隊(duì)列,進(jìn)行一個(gè)批處理。這樣就會(huì)讓多次的回流、重繪變成一次回流重繪。
但是也有例外,因?yàn)橛械臅r(shí)候我們需要精確獲取某些樣式信息,下面這些:
-
offsetTop, offsetLeft, offsetWidth, offsetHeight
-
scrollTop/Left/Width/Height
-
clientTop/Left/Width/Height
-
width,height
-
請求了getComputedStyle(), 或者 IE的 currentStyle
這個(gè)時(shí)候,瀏覽器為了反饋?zhàn)罹_的信息,需要立即回流重繪一次,確保給到我們的信息是準(zhǔn)確的,所以可能導(dǎo)致 flush 隊(duì)列提前執(zhí)行了。
display:none 與 visibility:hidden
兩者都可以在頁面上隱藏節(jié)點(diǎn)。不同之處在于,
-
display:none 隱藏后的元素不占據(jù)任何空間。它的寬度、高度等各種屬性值都將“丟失”
-
visibility:hidden 隱藏的元素空間依舊存在。它仍具有高度、寬度等屬性值
從性能的角度而言,即是回流與重繪的方面,
-
display:none 會(huì)觸發(fā) reflow(回流)
-
visibility:hidden 只會(huì)觸發(fā) repaint(重繪),因?yàn)闆]有發(fā)現(xiàn)位置變化
他們兩者在優(yōu)化中 visibility:hidden 會(huì)顯得更好,因?yàn)槲覀儾粫?huì)因?yàn)樗ジ淖兞宋臋n中已經(jīng)定義好的顯示層次結(jié)構(gòu)了。
對子元素的影響:
-
display:none 一旦父節(jié)點(diǎn)元素應(yīng)用了 display:none,父節(jié)點(diǎn)及其子孫節(jié)點(diǎn)元素全部不可見,而且無論其子孫元素如何設(shè)置 display 值都無法顯示;
-
visibility:hidden 一旦父節(jié)點(diǎn)元素應(yīng)用了 visibility:hidden,則其子孫后代也都會(huì)全部不可見。不過存在隱藏“失效”的情況。當(dāng)其子孫元素應(yīng)用了 visibility:visible,那么這個(gè)子孫元素又會(huì)顯現(xiàn)出來。
動(dòng)畫的性能檢測及優(yōu)化
耗性能樣式
不同樣式在消耗性能方面是不同的,譬如 box-shadow 從渲染角度來講十分耗性能,原因就是與其他樣式相比,它們的繪制代碼執(zhí)行時(shí)間過長。這就是說,如果一個(gè)耗性能嚴(yán)重的樣式經(jīng)常需要重繪,那么你就會(huì)遇到性能問題。其次你要知道,沒有不變的事情,在今天性能很差的樣式,可能明天就被優(yōu)化,并且瀏覽器之間也存在差異。
因此關(guān)鍵在于,你要借助開發(fā)工具來分辨出性能瓶頸所在,然后設(shè)法減少瀏覽器的工作量。
好在 chrome 瀏覽器提供了許多強(qiáng)大的功能,讓我們可以檢測我們的動(dòng)畫性能,除了上面提到的,我們還可以通過勾選下面這個(gè) show FPS meter 顯示頁面的 FPS 信息,以及 GPU 的使用率:
使用 will-change 提高頁面滾動(dòng)、動(dòng)畫等渲染性能
官方文檔說,這是一個(gè)仍處于實(shí)驗(yàn)階段的功能,所以在未來版本的瀏覽器中該功能的語法和行為可能隨之改變。
使用方法示例:(具體每個(gè)取值的意義,去翻翻文檔)
- will-change: auto
- will-change: scroll-position
- will-change: contents
- will-change: transform // Example of <custom-ident>
- will-change: opacity // Example of <custom-ident>
- will-change: left, top // Example of two <animateable-feature>
- will-change: unset
- will-change: initial
- will-change: inherit
- // 示例
- .example{
- will-change: transform;
- }
will-change 為 web 開發(fā)者提供了一種告知瀏覽器該元素會(huì)有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)的優(yōu)化準(zhǔn)備工作。 這種優(yōu)化可以將一部分復(fù)雜的計(jì)算工作提前準(zhǔn)備好,使頁面的反應(yīng)更為快速靈敏。
值得注意的是,用好這個(gè)屬性并不是很容易:
-
不要將 will-change 應(yīng)用到太多元素上:瀏覽器已經(jīng)盡力嘗試去優(yōu)化一切可以優(yōu)化的東西了。有一些更強(qiáng)力的優(yōu)化,如果與 will-change 結(jié)合在一起的話,有可能會(huì)消耗很多機(jī)器資源,如果過度使用的話,可能導(dǎo)致頁面響應(yīng)緩慢或者消耗非常多的資源。
-
有節(jié)制地使用:通常,當(dāng)元素恢復(fù)到初始狀態(tài)時(shí),瀏覽器會(huì)丟棄掉之前做的優(yōu)化工作。但是如果直接在樣式表中顯式聲明了 will-change 屬性,則表示目標(biāo)元素可能會(huì)經(jīng)常變化,瀏覽器會(huì)將優(yōu)化工作保存得比之前更久。所以最佳實(shí)踐是當(dāng)元素變化之前和之后通過腳本來切換 will-change 的值。
-
不要過早應(yīng)用 will-change 優(yōu)化:如果你的頁面在性能方面沒什么問題,則不要添加 will-change 屬性來榨取一丁點(diǎn)的速度。 will-change 的設(shè)計(jì)初衷是作為最后的優(yōu)化手段,用來嘗試解決現(xiàn)有的性能問題。它不應(yīng)該被用來預(yù)防性能問題。過度使用 will-change 會(huì)導(dǎo)致大量的內(nèi)存占用,并會(huì)導(dǎo)致更復(fù)雜的渲染過程,因?yàn)闉g覽器會(huì)試圖準(zhǔn)備可能存在的變化過程。這會(huì)導(dǎo)致更嚴(yán)重的性能問題。
-
給它足夠的工作時(shí)間:這個(gè)屬性是用來讓頁面開發(fā)者告知瀏覽器哪些屬性可能會(huì)變化的。然后瀏覽器可以選擇在變化發(fā)生前提前去做一些優(yōu)化工作。所以給瀏覽器一點(diǎn)時(shí)間去真正做這些優(yōu)化工作是非常重要的。使用時(shí)需要嘗試去找到一些方法提前一定時(shí)間獲知元素可能發(fā)生的變化,然后為它加上 will-change 屬性。
使用 transform3d api 代替 transform api,強(qiáng)制開始 GPU 加速
GPU 能夠加速 Web 動(dòng)畫,這個(gè)上文已經(jīng)反復(fù)提到了。
3D transform 會(huì)啟用GPU加速,例如 translate3D, scaleZ 之類,當(dāng)然我們的頁面可能并沒有 3D 變換,但是不代表我們不能啟用 GPU 加速,在非 3D 變換的頁面也使用 3D transform 來操作,算是一種 hack 加速法。我們實(shí)際上不需要z軸的變化,但是還是假模假樣地聲明了,去欺騙瀏覽器。
參考文獻(xiàn):
Rendering: repaint, reflow/relayout, restyle
How (not) to trigger a layout in WebKit
Accelerated Rendering in Chrome
到此本文結(jié)束,如果還有什么疑問或者建議,可以多多交流,原創(chuàng)文章,文筆有限,才疏學(xué)淺,文中若有不正之處,萬望告知。