對於移動端的Web單頁應用來講,爲了達到媲美原生應用的效果,頁面過渡動畫是必不可少的。經常使用的頁面過渡動畫包括:javascript
(注意:如下討論和實驗均在 Chrome 68 瀏覽器環境下進行)css
目前大多數設備的屏幕刷新率爲60次/秒,算下來每一個幀的預算時間約爲16.66毫秒(1/60秒)。考慮到瀏覽器還有其餘工做要執行,實際上預算時間只有10毫秒。跟此預算時間的差值越大,用戶就會以爲動畫過程越卡。那麼,在這10毫秒內要完成什麼事情呢?當使用JavaScript實現視覺交互效果時,通常要通過如下流程:html
值得注意的是,並不是每一幀都會通過上述每個步驟的處理。若是元素的幾何屬性(尺寸、位置)沒有變化,就不須要進行佈局;若是連元素的外觀都沒有改變,就不須要繪製。因此,實現流暢動畫的關鍵就在於如何減小布局和繪製。java
對於位移動畫來講,最直接的實現方式,就是把元素設成絕對定位,而後去改變它的left樣式值。例如:web
<!DOCTYPE html>
<html>
<head>
<style> .page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: left; } .leave { left: -100%; } </style>
</head>
<body>
<div id="page" class="page"></div>
<script> var page = document.getElementById('page'); setTimeout(function() { page.classList.add('leave'); }, 2000); </script>
</body>
</html>
複製代碼
使用Chrome開發者工具中的Performance面板錄製動畫過程的性能日誌,以下圖所示:瀏覽器
可見,元素在移動的過程當中不斷觸發了佈局和繪製。因此,這種實現方式的性能是極低的。網上諸多文獻會推薦以transform的變化代替left的變化,而實際狀況又是怎麼樣呢?把樣式代碼稍做修改:性能優化
.page {
position: absolute;
left: 0;
top: 0;
width: 100%;
min-height: 100%;
background: #ddd;
transition-duration: 2s;
transition-property: transform;
}
.leave {
transform: translateX(-100%);
}
複製代碼
錄製性能日誌以下圖所示:app
可見,僅僅是在動畫開始和結束兩個時間點觸發了繪製,而佈局則徹底沒有觸發。這樣一來,性能就有了很大的提高。可是,這裏還有兩個疑問:框架
要回答這兩個問題,就得了解合成層。工具
當知足某些條件的時候,元素在渲染時會被分配到一個獨立的層中進行渲染,只要該層的內容不發生改變,就不會觸發繪製,瀏覽器會直接經過合成造成一個新的幀。常見的提高爲合成層的條件包括:
很明顯,上一節的transform位移動畫知足了第一個條件。因此整個動畫的渲染過程是這樣的:
若是讓div.page一直在獨立的合成層中渲染,則能夠省掉上述過程當中繪製的環節。在樣式代碼添加「will-change: transform」:
.page {
position: absolute;
left: 0;
top: 0;
width: 100%;
min-height: 100%;
background: #ddd;
transition-duration: 2s;
transition-property: transform;
will-change: transform;
}
複製代碼
錄製性能日誌以下:
可見,已經不存在繪製的步驟了。
順帶一提,Chrome開發者工具中有一個Layers面板,能夠方便地查看頁面上合成層以及成爲合成層的緣由。
(注意:因爲低版本瀏覽器不支持will-change,因此實際應用中,若是想把元素提高到獨立的合成層中渲染,能夠用「transform: translateZ(0)」)
衆所周知,不透明度就是經過opacity樣式來控制的。那麼opacity的變化是否會觸發佈局和繪製呢?把樣式代碼修改以下:
.page {
position: absolute;
left: 0;
top: 0;
width: 100%;
min-height: 100%;
background: #ddd;
transition-duration: 2s;
transition-property: opacity;
}
.leave {
opacity: 0;
}
複製代碼
錄製性能日誌以下圖所示:
在常規認知中,opacity的變化並不會致使元素位置和尺寸的變化,理應不會觸發佈局。但上述過程當中確實觸發了一次佈局,表現較爲詭異。接下來給div.page添加「will-change: opacity」使其一直在獨立的合成層中渲染。錄製性能日誌以下:
可見,仍是會觸發一次繪製。而針對這「一次的佈局」和「一次的繪製」,我進行了進一步的實驗,得出的結論是:opacity從1(包括未設置的狀況,下同)變動到小於1,以及從小於1變動到1,都會觸發佈局和繪製;即便在獨立的合成層中渲染,也只能省掉佈局,沒法省掉繪製。
因爲在opacity動畫過程當中從1到小於1的變動只會有一次,因此上述的佈局和繪製都只觸發一次。
同時使用兩種動畫,修改樣式代碼以下:
.page {
position: absolute;
left: 0;
top: 0;
width: 100%;
min-height: 100%;
background: #ddd;
transition-duration: 2s;
transition-property: transform, opacity;
}
.leave {
transform: translateX(-100%);
opacity: 0;
}
複製代碼
按照前文的描述,動畫過程會觸發:
假若加上「will-change: transform, opacity」,使div.page一直在獨立的合成層中渲染,則只觸發一次繪製,由opacity引發。
然而,建立一個新的合成層並非免費的,它會致使額外的內存開銷。在單頁應用中,應用頁面過渡動畫的元素是頁面的最外層容器,包含了該頁面全部內容結構。若是讓其長期在獨立的合成層中渲染,那內存的消耗是很是大的。
因此,能夠僅在動畫過程當中讓其在獨立的合成層中渲染,而在其餘狀況下則維持常規狀態。
若是用transform實現頁面過渡動畫,想必你們都遇到過一個問題:頁面上固定定位的元素,其位置變得不太正常了。
下面經過一段代碼模擬頁面進入的過程,來演示這個問題:
<!DOCTYPE html>
<html>
<head>
<style> .page { position: absolute; left: 0; top: 0; width: 100%; height: 150%; background: #ddd; transition-duration: 3s; transition-timing-function: cubic-bezier(.55, 0, .1, 1); transition-property: transform, opacity; } .before-enter { transform: translateX(100%); opacity: 0; } .fixed { position: fixed; right: 0; bottom: 0; width: 100%; height: 160px; background: #ffc100; } </style>
</head>
<body>
<div id="page" class="page before-enter">
<div class="fixed"></div>
</div>
<script> var page = document.getElementById('page'); setTimeout(() => { page.classList.remove('before-enter'); }, 2000); </script>
</body>
</html>
複製代碼
運行效果以下:
能夠看到,固定定位的黃色元素是在動畫結束後才忽然出現的。那在這以前它跑到哪去了呢?
若是給一個固定定位元素的任意一個祖先元素設置樣式「transform」或者「will-change: transform」,那麼該元素就會相對於最近的設置了上述樣式的祖先元素定位。
由於div.page的高度設成了150%,因此,在動畫過程當中,黃色元素其實是跑到了頁面的最底下(超出了瀏覽器可視範圍)去了。而在某些比較舊(如 iOS 9 的Safari)的移動端瀏覽器中,問題更爲嚴重,固定定位的元素可能會消失掉不再出現。
網上能查到的解決方案有兩種:
因此,這裏介紹第三種方案——在頁面過渡動畫結束以後(此時transform樣式已被移除,再也不影響fixed),再讓固定定位的元素插入到頁面容器。而且,爲了讓它的出現顯得不那麼忽然,增長緩動動畫。代碼主要修改點以下:
@keyframes kf-move-in {
0% { transform: translateY(100%); }
100% { transform: translateY(0); }
}
.move-in {
animation-name: kf-move-in;
animation-duration: 0.45s;
}
複製代碼
<div id="page" class="page before-enter"></div>
<script> var page = document.getElementById('page'); setTimeout(function() { // 監聽過渡結束 page.addEventListener('transitionend', function() { // 建立、插入固定定位元素 var div = document.createElement('div'); div.className = 'fixed move-in'; page.appendChild(div); }); page.classList.remove('before-enter'); }, 2000); </script>
複製代碼
運行效果以下:
這樣一來,整個交互就較爲友好了。這同時也說明:技術上的問題,不必定只能經過技術去解決,也能夠從交互上去尋求解決方案。
本文同時發佈於做者我的博客 mrluo.life/article/det… 。