作者:chokcoco?
http://web.jobbole.com/93258/
不同于傳統(tǒng)的 PC Web 或者是移動 WEB,在騰訊視頻客廳盒子端非竿,接大屏顯示器(電視)下银觅,許多能流暢運(yùn)行于 PC 端渐北、移動端的 Web 動畫危虱,受限于硬件水平,在盒子端的表現(xiàn)的往往不盡如人意球匕。
基于此填渠,對于 Web 動畫的性能問題,僅僅停留在感覺已經(jīng)優(yōu)化的OK之上谎倔,是不夠的柳击,想要在盒子端跑出高性能接近 60 FPS 的流暢動畫,就必須要刨根問底片习,深挖每一處可以提升的方法捌肴。
流暢動畫的標(biāo)準(zhǔn)
理論上說,F(xiàn)PS 越高藕咏,動畫會越流暢状知,目前大多數(shù)設(shè)備的屏幕刷新率為 60 次/秒,所以通常來講 FPS 為 60frame/s 時動畫效果最好孽查,也就是每幀的消耗時間為 16.67ms试幽。
直觀感受,不同幀率的體驗(yàn)
1卦碾、幀率能夠達(dá)到 50 ~ 60 FPS 的動畫將會相當(dāng)流暢铺坞,讓人倍感舒適;
2洲胖、幀率在 30 ~ 50 FPS 之間的動畫济榨,因各人敏感程度不同,舒適度因人而異绿映;
3擒滑、幀率在 30 FPS 以下的動畫,讓人感覺到明顯的卡頓和不適感叉弦;
4丐一、幀率波動很大的動畫,亦會使人感覺到卡頓淹冰。
盒子端動畫優(yōu)化
在騰訊視頻客廳盒子端库车,Web 動畫未進(jìn)行優(yōu)化之前,一些復(fù)雜動畫的幀率僅有 10 ~ 30 FPS樱拴,卡頓感非常明顯柠衍,帶來很不好的用戶體驗(yàn)。
而進(jìn)行優(yōu)化之后晶乔,能將 10 ~ 30 FPS的動畫優(yōu)化至 30 ~ 60 FPS珍坊,雖然不算優(yōu)化到最完美,但是當(dāng)前盒子硬件的條件下正罢,已經(jīng)算是非常大的進(jìn)步阵漏。
盒子端 Web 動畫性能比較
首先先給出在盒子端不同類型的Web 動畫的性能比較。經(jīng)過對比,在盒子端 CSS 動畫的性能要優(yōu)于 Javascript 動畫履怯,而在 CSS 動畫里川无,使用 GPU 硬件加速的動畫性能要優(yōu)于不使用硬件加速的性能。
所以在盒子端虑乖,實(shí)現(xiàn)一個 Web 動畫懦趋,優(yōu)先級是:
GPU 硬件加速 CSS 動畫 > 非硬件加速 CSS 動畫 > Javascript 動畫
動畫性能上報分析
要有優(yōu)化,就必須得有數(shù)據(jù)做為支撐疹味。對比優(yōu)化前后是否有提升仅叫。而對于動畫而言,衡量一個動畫的標(biāo)準(zhǔn)也就是 FPS 值糙捺。
所以現(xiàn)在的關(guān)鍵是如何計(jì)算出每個動畫運(yùn)行時的幀率诫咱,這里我使用的是?requestAnimationFrame
這個函數(shù)近似的得到動畫運(yùn)行時的幀率。
考慮到盒子都是安卓系統(tǒng)洪灯,且大多版本較低且硬件性能堪憂坎缭,導(dǎo)致一是許多高級 API 無法使用,二是這里只是近似得到動畫幀率
原理是签钩,正常而言?requestAnimationFrame
?這個方法在一秒內(nèi)會執(zhí)行 60 次掏呼,也就是不掉幀的情況下。假設(shè)動畫在時間 A 開始執(zhí)行铅檩,在時間 B 結(jié)束憎夷,耗時 x ms。而中間?requestAnimationFrame
?一共執(zhí)行了 n 次昧旨,則此段動畫的幀率大致為:n / (B – A)拾给。
核心代碼如下,能近似計(jì)算每秒頁面幀率兔沃,以及我們額外記錄一個?allFrameCount
蒋得,用于記錄 rAF 的執(zhí)行次數(shù),用于計(jì)算每次動畫的幀率 :
var?rAF?=?function?()?{
????return?(
????????window.requestAnimationFrame?||
????????window.webkitRequestAnimationFrame?||
????????function?(callback)?{
????????????window.setTimeout(callback,?1000?/?60);
????????}
????);
}();
var?frame?=?0;
var?allFrameCount?=?0;
var?lastTime?=?Date.now();
var?lastFameTime?=?Date.now();
var?loop?=?function?()?{
????var?now?=?Date.now();
????var?fs?=?(now?-?lastFameTime);
????var?fps?=?Math.round(1000?/?fs);
????lastFameTime?=?now;
????// 不置 0乒疏,在動畫的開頭及結(jié)尾記錄此值的差值算出 FPS
????allFrameCount++;
????frame++;
????if?(now?>?1000?+?lastTime)?{
????????var?fps?=?Math.round((frame *?1000)?/?(now?-?lastTime));
????????// console.log('fps', fps); 每秒 FPS
????????frame?=?0;
????????lastTime?=?now;
????};
????rAF(loop);
}
研究結(jié)論
所以额衙,我們的目標(biāo)就是在使用 GPU 硬件加速的基礎(chǔ)之上,更深入的去優(yōu)化 CSS 動畫缰雇,先給出最后的一個優(yōu)化步驟方案:
1入偷、精簡 DOM ,合理布局
2械哟、使用 transform 代替 left、top殿雪,減少使用耗性能樣式
3暇咆、控制頻繁動畫的層級關(guān)系
4、考慮使用 will-change
5、使用 dev-tool 時間線 timeline 觀察爸业,找出導(dǎo)致高耗時其骄、掉幀的關(guān)鍵操作
下文會有每一步驟的具體分析解釋。
Web 每一幀的渲染
要想達(dá)到 60 FPS扯旷,每幀的預(yù)算時間僅比 16 毫秒多一點(diǎn) (1 秒/ 60 = 16.67 毫秒)拯爽。但實(shí)際上,瀏覽器有整理工作要做钧忽,因此您的所有工作需要盡量在 10 毫秒內(nèi)完成毯炮。
完整的像素管道 JS / CSS > 樣式 > 布局 > 繪制 > 合成:
1、JavaScript耸黑。一般來說桃煎,我們會使用 JavaScript 來實(shí)現(xiàn)一些視覺變化的效果。比如用 jQuery 的 animate 函數(shù)做一個動畫大刊、對一個數(shù)據(jù)集進(jìn)行排序或者往頁面里添加一些 DOM 元素等为迈。當(dāng)然,除了 JavaScript缺菌,還有其他一些常用方法也可以實(shí)現(xiàn)視覺變化效果葫辐,比如:CSS Animations、Transitions 和 Web Animation API伴郁。
2另患、樣式計(jì)算。此過程是根據(jù)匹配選擇器(例如 .headline 或 .nav > .nav__item)計(jì)算出哪些元素應(yīng)用哪些 CSS 3. 規(guī)則的過程蛾绎。從中知道規(guī)則之后昆箕,將應(yīng)用規(guī)則并計(jì)算每個元素的最終樣式。
3租冠、布局鹏倘。在知道對一個元素應(yīng)用哪些規(guī)則之后,瀏覽器即可開始計(jì)算它要占據(jù)的空間大小及其在屏幕的位置顽爹。網(wǎng)頁的布局模式意味著一個元素可能影響其他元素纤泵,例如 元素的寬度一般會影響其子元素的寬度以及樹中各處的節(jié)點(diǎn),因此對于瀏覽器來說镜粤,布局過程是經(jīng)常發(fā)生的捏题。
4、繪制肉渴。繪制是填充像素的過程公荧。它涉及繪出文本、顏色同规、圖像循狰、邊框和陰影窟社,基本上包括元素的每個可視部分。繪制一般是在多個表面(通常稱為層)上完成的绪钥。
5灿里、合成。由于頁面的各部分可能被繪制到多層程腹,由此它們需要按正確順序繪制到屏幕上匣吊,以便正確渲染頁面。對于與另一元素重疊的元素來說寸潦,這點(diǎn)特別重要色鸳,因?yàn)橐粋€錯誤可能使一個元素錯誤地出現(xiàn)在另一個元素的上層。
當(dāng)然甸祭,不一定每幀都總是會經(jīng)過管道每個部分的處理缕碎。我們的目標(biāo)就是,每一幀的動畫池户,對于上述的管道流程咏雌,能避免則避免,不能避免則最大限度優(yōu)化校焦。
優(yōu)化動畫步驟
先給出一個步驟赊抖,調(diào)優(yōu)一個動畫,有一定的指導(dǎo)原則可以遵循寨典,一步一步深入動畫:
1.精簡 DOM 氛雪,合理布局
這個沒什么好說的,如果可以耸成,精簡 DOM 結(jié)構(gòu)在任何時候都是對頁面有幫助的报亩。
2.使用 transform 代替 left、top井氢,減少使用耗性能樣式
現(xiàn)代瀏覽器在完成以下四種屬性的動畫時弦追,消耗成本較低:
1、position(位置):?transform: translate(npx, npx)
2花竞、scale(比例縮放):transform: scale(n)
3劲件、rotation(旋轉(zhuǎn)) :transform: rotate(ndeg)
4、opacity(透明度):opacity: 0...1
如果可以约急,盡量只使用上述四種屬性去控制動畫零远。
不同樣式在消耗性能方面是不同的,改變一些屬性的開銷比改變其他屬性要多厌蔽,因此更可能使動畫卡頓牵辣。
例如,與改變元素的文本顏色相比躺枕,改變元素的?box-shadow
?將需要開銷大很多的繪圖操作服猪。 改變元素的?width
?可能比改變其?transform
?要多一些開銷供填。如?box-shadow
?屬性拐云,從渲染角度來講十分耗性能罢猪,原因就是與其他樣式相比,它們的繪制代碼執(zhí)行時間過長叉瘩。
這就是說膳帕,如果一個耗性能嚴(yán)重的樣式經(jīng)常需要重繪,那么你就會遇到性能問題薇缅。其次你要知道危彩,沒有不變的事情,在今天性能很差的樣式泳桦,可能明天就被優(yōu)化汤徽,并且瀏覽器之間也存在差異。
開啟 GPU 硬件加速
歸根結(jié)底灸撰,上述四種屬性的動畫消耗較低的原因是會開啟了 GPU 硬件加速谒府。動畫元素生成了自己的圖形層(GraphicsLayer)。
通常而言浮毯,開啟 GPU 加速的方法我們可以使用
1完疫、will-change: transform
這會使聲明了該樣式屬性的元素生成一個圖形層,告訴瀏覽器接下來該元素將會進(jìn)行 transform 變換债蓝,讓瀏覽器提前做好準(zhǔn)備壳鹤。
使用?
will-change
?并不一定會有性能的提升,因?yàn)榧词篂g覽器預(yù)料到會有這些更改饰迹,依然會為這些屬性運(yùn)行布局和繪制流程芳誓,所以提前告訴瀏覽器,也并不會有太多性能上的提升啊鸭。這樣做的好處是锹淌,創(chuàng)建新的圖層代價很高,而等到需要時匆忙地創(chuàng)建莉掂,不如一開始直接創(chuàng)建好葛圃。
對于 Safari 及一些舊版本瀏覽器,它們不能識別?will-change
憎妙,則需要使用某種 translate 3D 進(jìn)行 hack库正,通常會使用
2、transform: translateZ(0)
所以厘唾,正常而言褥符,在生產(chǎn)環(huán)境下,我們可能需要使用如下代碼抚垃,開啟硬件加速:
{
????will-change:?transform;
????transform:?translateZ(0);
}
3.控制頻繁動畫的層級關(guān)系
動畫層級的控制的意思是盡量讓需要進(jìn)行 CSS 動畫的元素的?z-index
?保持在頁面最上方喷楣,避免瀏覽器創(chuàng)建不必要的圖形層(GraphicsLayer),能夠很好的提升渲染性能铣焊。
OK逊朽,這里又提到了圖形層(GraphicsLayer)曲伊,這是一個瀏覽器渲染原理相關(guān)的知識(WebKit/blink內(nèi)核下)。它能對動畫進(jìn)行加速岛蚤,但同時也存在相應(yīng)的加速坑!
簡單來說懈糯,瀏覽器為了提升動畫的性能赚哗,為了在動畫的每一幀的過程中不必每次都重新繪制整個頁面她紫。在特定方式下可以觸發(fā)生成一個合成層,合成層擁有單獨(dú)的 GraphicsLayer蜂奸。
需要進(jìn)行動畫的元素包含在這個合成層之下犁苏,這樣動畫的每一幀只需要去重新繪制這個 Graphics Layer 即可,從而達(dá)到提升動畫性能的目的扩所。
那么一個元素什么時候會觸發(fā)創(chuàng)建一個 Graphics Layer 層围详?從目前來說,滿足以下任意情況便會創(chuàng)建層:
1祖屏、硬件加速的 iframe 元素(比如 iframe 嵌入的頁面中有合成層)
2助赞、硬件加速的插件,比如 flash 等等
3袁勺、使用加速視頻解碼的 <video>元素
4雹食、3D 或者 硬件加速的 2D Canvas 元素
5、3D 或透視變換 (perspective期丰、transform) 的 CSS 屬性
6群叶、對自己的 opacity 做 CSS 動畫或使用一個動畫變換的元素
7、擁有加速 CSS 過濾器的元素
8钝荡、元素有一個包含復(fù)合層的后代節(jié)點(diǎn)(換句話說街立,就是一個元素?fù)碛幸粋€子元素,該子元素在自己的層里)
9埠通、元素有一個 z-index 較低且包含一個復(fù)合層的兄弟元素
本小點(diǎn)中說到的動畫層級的控制赎离,原因就在于上面生成層的最后一條:
元素有一個 z-index 較低且包含一個復(fù)合層的兄弟元素。
這里是存在坑的地方端辱,首先我們要明確兩點(diǎn):
1梁剔、我們希望我們的動畫得到 GPU 硬件加速虽画,所以我們會利用類似?transform: translateZ()
這樣的方式生成一個 Graphics Layer 層。
2荣病、Graphics Layer 雖好码撰,但不是越多越好,每一幀的渲染內(nèi)核都會去遍歷計(jì)算當(dāng)前所有的 Graphics Layer 众雷,并計(jì)算他們下一幀的重繪區(qū)域灸拍,所以過量的 Graphics Layer 計(jì)算也會給渲染造成性能影響做祝。
記住這兩點(diǎn)之后砾省,回到上面我們說的坑。
假設(shè)我們有一個輪播圖混槐,有一個 ul 列表编兄,結(jié)構(gòu)如下:
<div?class="container">
? ? <div?class="swiper">輪播圖</div>
? ??<ul?class="list">
? ??? ??<li>列表li</li>
? ??? ??<li>列表li</li>
? ??? ??<li>列表li</li>
? ??? ??<li>列表li</li>
? ??</ul>
</div>
假設(shè)給他們定義如下 CSS:
.swiper?{
????position:?static;
????animation:?10s?move?infinite;
}
.list?{
????position:?relative;
}
@keyframes?move?{
????100%?{
????????transform:?translate3d(10px,?0,?0);
????}
}
由于給?.swiper
?添加了?translate3d(10px, 0, 0)
?動畫,所以它會生成一個 Graphics Layer声登,如下圖所示狠鸳,用開發(fā)者工具可以打開層的展示,圖形外的黃色邊框即代表生成了一個獨(dú)立的復(fù)合層悯嗓,擁有獨(dú)立的 Graphics Layer 件舵。
但是!在上面的圖中脯厨,我們并沒有給下面的?list
?也添加任何能觸發(fā)生成 Graphics Layer 的屬性铅祸,但是它也同樣也有黃色的邊框,生成了一個獨(dú)立的復(fù)合層合武。
原因在于上面那條元素有一個 z-index 較低且包含一個復(fù)合層的兄弟元素临梗。我們并不希望?list
?元素也生成 Graphics Layer ,但是由于 CSS 層級定義原因稼跳,下面的 list 的層級高于上面的 swiper盟庞,所以它被動的也生成了一個 Graphics Layer 。
使用 Chrome汤善,我們也可以觀察到這種層級關(guān)系什猖,可以看到?.list
?的層級高于?.swiper
:
所以,下面我們修改一下 CSS 红淡,改成:
.swiper?{
????position:?relative;
????z-index:?100;
}
.list?{
????position:?relative;
}
這里不狮,我們明確使得?.swiper
?的層級高于?.list
?,再打開開發(fā)者工具觀察一下:
可以看到锉屈,這一次荤傲,.list
?元素已經(jīng)沒有了黃色外邊框,說明此時沒有生成 Graphics Layer 颈渊。再看看層級圖:
此時遂黍,層級關(guān)系才是我們希望看到的终佛,.list
?元素沒有觸發(fā)生成 Graphics Layer 。而我們希望需要硬件加速的?.swiper
?保持在最上方雾家,每次動畫過程中只會獨(dú)立重繪這部分的區(qū)域铃彰。
總結(jié)
這個坑最早見于張?jiān)讫埌l(fā)布的這篇文章CSS3硬件加速也有坑,這里還要總結(jié)補(bǔ)充的是:
1芯咧、GPU 硬件加速也會有坑牙捉,當(dāng)我們希望使用利用類似?transform: translate3d()
?這樣的方式開啟 GPU 硬件加速,一定要注意元素層級的關(guān)系敬飒,盡量保持讓需要進(jìn)行 CSS 動畫的元素的?z-index
?保持在頁面最上方邪铲。
2、Graphics Layer 不是越多越好无拗,每一幀的渲染內(nèi)核都會去遍歷計(jì)算當(dāng)前所有的 Graphics Layer 带到,并計(jì)算他們下一幀的重繪區(qū)域,所以過量的 Graphics Layer 計(jì)算也會給渲染造成性能影響英染。
3揽惹、可以使用 Chrome ,用上面介紹的兩個工具對自己的頁面生成的 Graphics Layer 和元素層級進(jìn)行觀察然后進(jìn)行相應(yīng)修改四康。
4搪搏、上面觀察頁面層級的 chrome 工具非常吃內(nèi)存?好像還是一個處于實(shí)驗(yàn)室的功能闪金,分析稍微大一點(diǎn)的頁面容易直接卡死疯溺,所以要多學(xué)會使用第一種觀察黃色邊框的方式查看頁面生成的 Graphics Layer 這種方式。
4. 使用 will-change 可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)準(zhǔn)備
// 示例
.example?{
????will-change:?transform;
}
上面已經(jīng)提到過 will-change 了毕泌。
will-change 為 web 開發(fā)者提供了一種告知瀏覽器該元素會有哪些變化的方法喝检,這樣瀏覽器可以在元素屬性真正發(fā)生變化之前提前做好對應(yīng)的優(yōu)化準(zhǔn)備工作。 這種優(yōu)化可以將一部分復(fù)雜的計(jì)算工作提前準(zhǔn)備好撼泛,使頁面的反應(yīng)更為快速靈敏挠说。
值得注意的是,用好這個屬性并不是很容易:
1愿题、在一些低端盒子上损俭,will-change
?會導(dǎo)致很多小問題,譬如會使圖片模糊潘酗,有的時候很容易適得其反杆兵,所以使用的時候還需要多加測試。
2仔夺、不要將 will-change 應(yīng)用到太多元素上:瀏覽器已經(jīng)盡力嘗試去優(yōu)化一切可以優(yōu)化的東西了琐脏。有一些更強(qiáng)力的優(yōu)化,如果與 will-change 結(jié)合在一起的話,有可能會消耗很多機(jī)器資源日裙,如果過度使用的話吹艇,可能導(dǎo)致頁面響應(yīng)緩慢或者消耗非常多的資源。
3昂拂、有節(jié)制地使用:通常受神,當(dāng)元素恢復(fù)到初始狀態(tài)時,瀏覽器會丟棄掉之前做的優(yōu)化工作格侯。但是如果直接在樣式表中顯式聲明了 will-change 屬性鼻听,則表示目標(biāo)元素可能會經(jīng)常變化,瀏覽器會將優(yōu)化工作保存得比之前更久联四。所以最佳實(shí)踐是當(dāng)元素變化之前和之后通過腳本來切換 will-change 的值撑碴。
4、不要過早應(yīng)用 will-change 優(yōu)化:如果你的頁面在性能方面沒什么問題碎连,則不要添加 will-change 屬性來榨取一丁點(diǎn)的速度灰羽。 will-change 的設(shè)計(jì)初衷是作為最后的優(yōu)化手段,用來嘗試解決現(xiàn)有的性能問題鱼辙。它不應(yīng)該被用來預(yù)防性能問題。過度使用 will-change 會導(dǎo)致生成大量圖層玫镐,進(jìn)而導(dǎo)致大量的內(nèi)存占用倒戏,并會導(dǎo)致更復(fù)雜的渲染過程,因?yàn)闉g覽器會試圖準(zhǔn)備可能存在的變化過程恐似,這會導(dǎo)致更嚴(yán)重的性能問題杜跷。
5、給它足夠的工作時間:這個屬性是用來讓頁面開發(fā)者告知瀏覽器哪些屬性可能會變化的矫夷。然后瀏覽器可以選擇在變化發(fā)生前提前去做一些優(yōu)化工作葛闷。所以給瀏覽器一點(diǎn)時間去真正做這些優(yōu)化工作是非常重要的。使用時需要嘗試去找到一些方法提前一定時間獲知元素可能發(fā)生的變化双藕,然后為它加上 will-change 屬性淑趾。
5. 使用 dev-tool 時間線 timeline 觀察,找出導(dǎo)致高耗時忧陪、掉幀的關(guān)鍵操作
1扣泊、對比屏幕快照,觀察每一幀包含的內(nèi)容及具體的操作
2嘶摊、找到掉幀的那一幀延蟹,分析該幀內(nèi)不同步驟的耗時占比,進(jìn)行有針對性的優(yōu)化
3叶堆、觀察是否存在內(nèi)存泄漏
總結(jié)一下
對于盒子端 CSS 動畫的性能阱飘,很多方面仍處于探索中,本文大量內(nèi)容在之前文章已經(jīng)出現(xiàn)過,這里更多的是歸納總結(jié)提煉成可參照執(zhí)行的流程沥匈。
本文的優(yōu)化方案研究同樣適用于 PC Web 及移動 Web果录,文章難免有錯誤及疏漏,歡迎不吝賜教咐熙。
感興趣的小伙伴弱恒,可以關(guān)注公眾號【grain先森】,回復(fù)關(guān)鍵詞 “小程序”棋恼,獲取更多資料返弹,更多關(guān)鍵詞玩法期待你的探索~