深入淺出 CSS 動畫

chokcoco發表於2022-01-13

本文將比較全面細緻的梳理一下 CSS 動畫的方方面面,針對每個屬性用法的講解及進階用法的示意,希望能成為一個比較好的從入門到進階的教程。

CSS 動畫介紹及語法

首先,我們來簡單介紹一下 CSS 動畫。

最新版本的 CSS 動畫由規範 -- CSS Animations Level 1 定義。

CSS 動畫用於實現元素從一個 CSS 樣式配置轉換到另一個 CSS 樣式配置。

動畫包括兩個部分: 描述動畫的樣式規則和用於指定動畫開始、結束以及中間點樣式的關鍵幀。

簡單來說,看下面的例子:

div {
    animation: change 3s;
}

@keyframes change {
    0% {
        color: #f00;
    }
    100% {
        color: #000;
    }
}
  1. animation: move 1s 部分就是動畫的第一部分,用於描述動畫的各個規則;
  2. @keyframes move {} 部分就是動畫的第二部分,用於指定動畫開始、結束以及中間點樣式的關鍵幀;

一個 CSS 動畫一定要由上述兩部分組成。

CSS 動畫的語法

接下來,我們簡單看看 CSS 動畫的語法。

建立動畫序列,需要使用 animation 屬性或其子屬性,該屬性允許配置動畫時間、時長以及其他動畫細節,但該屬性不能配置動畫的實際表現,動畫的實際表現是由 @keyframes 規則實現。

animation 的子屬性有:

  • animation-name:指定由 @keyframes 描述的關鍵幀名稱。
  • animation-duration:設定動畫一個週期的時長。
  • animation-delay:設定延時,即從元素載入完成之後到動畫序列開始執行的這段時間。
  • animation-direction:設定動畫在每次執行完後是反向執行還是重新回到開始位置重複執行。
  • animation-iteration-count:設定動畫重複次數, 可以指定 infinite 無限次重複動畫
  • animation-play-state:允許暫停和恢復動畫。
  • animation-timing-function:設定動畫速度, 即通過建立加速度曲線,設定動畫在關鍵幀之間是如何變化。
  • animation-fill-mode:指定動畫執行前後如何為目標元素應用樣式
  • @keyframes 規則,當然,一個動畫想要執行,還應該包括 @keyframes 規則,在內部設定動畫關鍵幀

其中,對於一個動畫:

  • 必須項animation-nameanimation-duration@keyframes規則
  • 非必須項animation-delayanimation-directionanimation-iteration-countanimation-play-stateanimation-timing-functionanimation-fill-mode,當然不是說它們不重要,只是不設定時,它們都有預設值

上面已經給了一個簡單的 DEMO, 就用上述的 DEMO,看看結果:

這就是一個最基本的 CSS 動畫,本文將從 animation 的各個子屬性入手,探究 CSS 動畫的方方面面。

animation-name / animation-duration 詳解

整體而言,單個的 animation-nameanimation-duration 沒有太多的技巧,非常好理解,放在一起。

首先介紹一下 animation-name,通過 animation-name,CSS 引擎將會找到對應的 @keyframes 規則。

當然,它和 CSS 規則命名一樣,也存在一些騷操作。譬如,他是支援 emoji 表情的,所以程式碼中的 animation-name 命名也可以這樣寫:

div {
    animation: ? 3s;
}

@keyframes ? {
    0% {
        color: #f00;
    }
    100% {
        color: #000;
    }
}

animation-duration 設定動畫一個週期的時長,上述 DEMO 中,就是設定動畫整體持續 3s,這個也非常好理解。

animation-delay 詳解

animation-delay 就比較有意思了,它可以設定動畫延時,即從元素載入完成之後到動畫序列開始執行的這段時間。

簡單的一個 DEMO:

<div></div>
<div></div>
div {
    width: 100px;
    height: 100px;
    background: #000;
    animation-name: move;
    animation-duration: 2s;
}

div:nth-child(2) {
    animation-delay: 1s;
}
@keyframes move {
    0% {
        transform: translate(0);
    }
    100% {
        transform: translate(200px);
    }
}

比較下列兩個動畫,一個新增了 animation-delay,一個沒有,非常直觀:

上述第二個 div,關於 animation 屬性,也可以簡寫為 animation: move 2s 1s,第一個時間值表示持續時間,第二個時間值表示延遲時間。

animation-delay 可以為負值

關於 animation-delay,最有意思的技巧在於,它可以是負數。也就是說,雖然屬性名是動畫延遲時間,但是運用了負數之後,動畫可以提前進行

假設我們要實現這樣一個 loading 動畫效果:

有幾種思路:

  1. 初始 3 個球的位置就是間隔 120°,同時開始旋轉,但是這樣程式碼量會稍微多一點
  2. 另外一種思路,同一個動畫,3 個元素的其中兩個延遲整個動畫的 1/3,2/3 時間出發

方案 2 的核心虛擬碼如下:

.item:nth-child(1) {
    animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
    animation: rotate 3s infinite 1s linear;
}
.item:nth-child(3) {
    animation: rotate 3s infinite 2s linear;
}

但是,在動畫的前 2s,另外兩個元素是不會動的,只有 2s 過後,整個動畫才是我們想要的:

此時,我們可以讓第 2、3 個元素的延遲時間,改為負值,這樣可以讓動畫延遲進行 -1s-2s,也就是提前進行 1s2s

.item:nth-child(1) {
    animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
    animation: rotate 3s infinite -1s linear;
}
.item:nth-child(3) {
    animation: rotate 3s infinite -2s linear;
}

這樣,每個元素都無需等待,直接就是運動狀態中的,並且元素間隔位置是我們想要的結果:

利用 animation-duration 和 animation-delay 構建隨機效果

還有一個有意思的小技巧。

同一個動畫,我們利用一定範圍內隨機的 animation-duration 和一定範圍內隨機的 animation-delay,可以有效的構建更為隨機的動畫效果,讓動畫更加的自然。

我在下述兩個純 CSS 動畫中,都使用了這樣的技巧:

  1. 純 CSS 實現華為充電動畫

純 CSS 實現華為充電動畫

  1. 純 CSS 實現火焰動畫

純 CSS 實現火焰動畫

純 CSS 實現華為充電動畫為例子,簡單講解一下。

仔細觀察這一部分,上升的一個一個圓球,拋去這裡的一些融合效果,只關注不斷上升的圓球,看著像是沒有什麼規律可言:

我們來模擬一下,如果是使用 10 個 animation-durationanimation-delay 都一致的圓的話,核心虛擬碼:

<ul>
    <li></li>
    <!--共 10 個...--> 
    <li></li>
</ul>
ul {
    display: flex;
    flex-wrap: nowrap;
    gap: 5px;
}
li {
    background: #000;
    animation: move 3s infinite 1s linear;
}
@keyframes move {
    0% {
        transform: translate(0, 0);
    }
    100% {
        transform: translate(0, -100px);
    }
}

這樣,小球的運動會是這樣的整齊劃一:

要讓小球的運動顯得非常的隨機,只需要讓 animation-durationanimation-delay 都在一定範圍內浮動即可,改造下 CSS:

@for $i from 1 to 11 {
    li:nth-child(#{$i}) {
        animation-duration: #{random(2000)/1000 + 2}s;
        animation-delay: #{random(1000)/1000 + 1}s;
    }
}

我們利用 SASS 的迴圈和 random() 函式,讓 animation-duration 在 2-4 秒範圍內隨機,讓 animation-delay 在 1-2 秒範圍內隨機,這樣,我們就可以得到非常自然且不同的上升動畫效果,基本不會出現重複的畫面,很好的模擬了隨機效果:

CodePen Demo -- 利用範圍隨機 animation-duration 和 animation-delay 實現隨機動畫效果

animation-timing-function 緩動函式

緩動函式在動畫中非常重要,它定義了動畫在每一動畫週期中執行的節奏。

緩動主要分為兩類:

  1. cubic-bezier-timing-function 三次貝塞爾曲線緩動函式
  2. step-timing-function 步驟緩動函式(這個翻譯是我自己翻的,可能有點奇怪)

三次貝塞爾曲線緩動函式

首先先看看三次貝塞爾曲線緩動函式。在 CSS 中,支援一些緩動函式關鍵字。

/* Keyword values */
animation-timing-function: ease;  // 動畫以低速開始,然後加快,在結束前變慢
animation-timing-function: ease-in;  // 動畫以低速開始
animation-timing-function: ease-out; // 動畫以低速結束
animation-timing-function: ease-in-out; // 動畫以低速開始和結束
animation-timing-function: linear; // 勻速,動畫從頭到尾的速度是相同的

關於它們之間的效果對比:

除了 CSS 支援的這 5 個關鍵字,我們還可以使用 cubic-bezier() 方法自定義三次貝塞爾曲線:

animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);

這裡有個非常好用的網站 -- cubic-bezier 用於建立和除錯生成不同的貝塞爾曲線引數。

三次貝塞爾曲線緩動對動畫的影響

關於緩動函式對動畫的影響,這裡有一個非常好的示例。這裡我們使用了純 CSS 實現了一個鐘的效果,對於其中的動畫的運動,如果是 animation-timing-function: linear,效果如下:

b

而如果我們我把緩動函式替換一下,變成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲線對應如下:

整個鐘的動畫律動效果將變成這樣,完全不一樣的感覺:

CodePen Demo - 緩動不同效果不同

對於許多精益求精的動畫,在設計中其實都考慮到了緩動函式。我很久之前看到過一篇《基於物理學的動畫使用者體驗設計》,可惜如今已經無法找到原文。其中傳達出的一些概念是,動畫的設計依據實際在生活中的表現去考量。

譬如 linear 這個緩動,實際應用於某些動畫中會顯得很不自然,因為由於空氣阻力的存在,程式模擬的勻速直線運動在現實生活中是很難實現的。因此對於這樣一個使用者平時很少感知到的運動是很難建立信任感的。這樣的勻速直線運動也是我們在進行動效設計時需要極力避免的。

步驟緩動函式

接下來再講講步驟緩動函式。在 CSS 的 animation-timing-function 中,它有如下幾種表現形態:

{
    /* Keyword values */
    animation-timing-function: step-start;
    animation-timing-function: step-end;

    /* Function values */
    animation-timing-function: steps(6, start)
    animation-timing-function: steps(4, end);
}

在 CSS 中,使用步驟緩動函式最多的,就是利用其來實現逐幀動畫。假設我們有這樣一張圖(圖片大小為 1536 x 256,圖片來源於網路):

可以發現它其實是一個人物行進過程中的 6 種狀態,或者可以為 6 幀,我們利用 animation-timing-function: steps(6) 可以將其用一個 CSS 動畫串聯起來,程式碼非常的簡單:

<div class="box"></div>
.box {
  width: 256px;
  height: 256px;
  background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
  animation: sprite .6s steps(6, end) infinite;
}
@keyframes sprite {
  0% { 
    background-position: 0 0;
  }
  100% { 
    background-position: -1536px 0;
  }
}

簡單解釋一下上述程式碼,首先要知道,剛好 256 x 6 = 1536,所以上述圖片其實可以剛好均分為 6 段:

  1. 我們設定了一個大小都為 256px 的 div,給這個 div 賦予了一個 animation: sprite .6s steps(6) infinite 動畫;
  2. 其中 steps(6) 的意思就是將設定的 @keyframes 動畫分為 6 次(6幀)執行,而整體的動畫時間是 0.6s,所以每一幀的停頓時長為 0.1s
  3. 動畫效果是由 background-position: 0 0background-position: -1536px 0,由於上述的 CSS 程式碼沒有設定 background-repeat,所以其實 background-position: 0 0 是等價於 background-position: -1536px 0,就是圖片在整個動畫過程中推進了一輪,只不過每一幀停在了特點的地方,一共 6 幀;

將上述 1、2、3,3 個步驟畫在圖上簡單示意:

從上圖可知,其實在動畫過程中,background-position 的取值其實只有 background-position: 0 0background-position: -256px 0background-position: -512px 0 依次類推一直到 background-position: -1536px 0,由於背景的 repeat 的特性,其實剛好回到原點,由此又重新開始新一輪同樣的動畫。

所以,整個動畫就會是這樣,每一幀停留 0.1s 後切換到下一幀(注意這裡是個無限迴圈動畫),:

完整的程式碼你可以戳這裡 -- CodePen Demo -- Sprite Animation with steps()

animation-duration 動畫長短對動畫的影響

在這裡再插入一個小章節,animation-duration 動畫長短對動畫的影響也是非常明顯的。

在上述程式碼的基礎上,我們再修改 animation-duration,縮短每一幀的時間就可以讓步行的效果變成跑步的效果,同理,也可以增加每一幀的停留時間。讓每一步變得緩慢,就像是在步行一樣。

需要提出的是,上文說的每一幀,和瀏覽器渲染過程中的 FPS 的每一幀不是同一個概念。

看看效果,設定不同的 animation-duration 的效果(這裡是 0.6s -> 0.2s),GIF 錄屏丟失了一些關鍵幀,實際效果會更好點:

當然,在 steps() 中,還有 steps(6, start)steps(6, end) 的差異,也就是其中關鍵字 startend 的差異。對於上述的無限動畫而言,其實基本是可以忽略不計的,它主要是控制動畫第一幀的開始和持續時長,比較小的一個知識點但是想講明白需要比較長的篇幅,限於本文的內容,在這裡不做展開,讀者可以自行了解。

同個動畫效果的補間動畫和逐幀動畫演繹對比

上述的三次貝塞爾曲線緩動和步驟緩動,其實就是對應的補間動畫和逐幀動畫。

對於同個動畫而言,有的時候兩種緩動都是適用的。我們在具體使用的時候需要具體分析選取。

假設我們用 CSS 實現了這樣一個圖形:

現在想利用這個圖形制作一個 Loading 效果,如果利用補間動畫,也就是三次貝塞爾曲線緩動的話,讓它旋轉起來,得到的效果非常的一般:

.g-container{
    animation: rotate 2s linear infinite;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}

動畫效果如下:

但是如果這裡,我們將補間動畫換成逐幀動畫,因為有 20 個點,所以設定成 steps(20),再看看效果,會得到完全不一樣的感覺:

.g-container{
    animation: rotate 2s steps(20) infinite;
}
@keyframes rotate {
    0% {
        transform: rotate(0);
    }
    100% {
        transform: rotate(360deg);
    }
}

動畫效果如下:

整個 loading 的圈圈看上去好像也在旋轉,實際上只是 20 幀關鍵幀在切換,整體的效果感覺更適合 Loading 的效果。

因此,兩種動畫效果都是很有必要掌握的,在實際使用的時候靈活嘗試,選擇更適合的。

上述 DEMO 效果完整的程式碼:CodePen Demo -- Scale Loading steps vs linear

animation-play-state

接下來,我們講講 animation-play-state,顧名思義,它可以控制動畫的狀態 -- 執行或者暫停。類似於視訊播放器的開始和暫停。是 CSS 動畫中有限的控制動畫狀態的手段之一。

它的取值只有兩個(預設為 running):

{
    animation-play-state: paused | running;
}

使用起來也非常簡單,看下面這個例子,我們在 hover 按鈕的時候,實現動畫的暫停:

<div class="btn stop">stop</div>
<div class="animation"></div>
.animation {
    width: 100px;
    height: 100px;
    background: deeppink;
    animation: move 2s linear infinite alternate;
}

@keyframes move {
    100% {
        transform: translate(100px, 0);
    }
}

.stop:hover ~ .animation {
    animation-play-state: paused;
}

一個簡單的 CSS 動畫,但是當我們 hover 按鈕的時候,給動畫元素新增上 animation-play-state: paused

animation-play-state 小技巧,預設暫停,點選執行

正常而言,按照正常思路使用 animation-play-state: paused 是非常簡單的。

但是,如果我們想創造一些有意思的 CSS 動畫效果,不如反其道而行之。

我們都知道,正常情況下,動畫應該是執行狀態,那如果我們將一些動畫的預設狀態設定為暫停,只有當滑鼠點選或者 hover 的時候,才設定其 animation-play-state: running,這樣就可以得到很多有趣的 CSS 效果。

看個倒酒的例子,這是一個純 CSS 動畫,但是預設狀態下,動畫處於 animation-play-state: paused,也就是暫停狀態,只有當滑鼠點選杯子的時,才設定 animation-play-state: running,讓酒倒下,利用 animation-play-state 實現了一個非常有意思的互動效果:

完整的 DEMO 你可以戳這裡:CodePen Demo -- CSS Beer!

在非常多 Web 創意互動動畫我們都可以看到這個技巧的身影。

  1. 頁面 render 後,無任何操作,動畫不會開始。只有當滑鼠對元素進行 click ,通過觸發元素的 :active 偽類效果的時候,賦予動畫 animation-play-state: running,動畫才開始進行;
  2. 動畫進行到任意時刻,滑鼠停止點選,偽類消失,則動畫停止;

animation-fill-mode 控制元素在各個階段的狀態

下一個屬性 animation-fill-mode,很多人會誤認為它只是用於控制元素在動畫結束後是否復位。這個其實是不準確的,不全面的。

看看它的取值:

{
    // 預設值,當動畫未執行時,動畫將不會將任何樣式應用於目標,而是使用賦予給該元素的 CSS 規則來顯示該元素的狀態
    animation-fill-mode: none;
    // 動畫將在應用於目標時立即應用第一個關鍵幀中定義的值,並在 `animation-delay` 期間保留此值,
    animation-fill-mode: backwards; 
    // 目標將保留由執行期間遇到的最後一個關鍵幀計算值。 最後一個關鍵幀取決於 `animation-direction` 和 `animation-iteration-count`
    animation-fill-mode: forwards;    
    // 動畫將遵循 `forwards` 和 `backwards` 的規則,從而在兩個方向上擴充套件動畫屬性
    animation-fill-mode: both; 
}

對於 animation-fill-mode 的解讀,我在 Segment Fault 上的一個問答中(SF - 如何理解 animation-fill-mode)看到了 4 副很好的解讀圖,這裡借用一下:

假設 HTML 如下:

<div class="box"></div>

CSS如下:

.box{
    transform: translateY(0);
}
.box.on{
    animation: move 1s;
}

@keyframes move{
    from{transform: translateY(-50px)}
    to  {transform: translateY( 50px)}
}

使用圖片來表示 translateY 的值與 時間 的關係:

  • 橫軸為表示 時間,為 0 時表示動畫開始的時間,也就是向 box 加上 on 類名的時間,橫軸一格表示 0.5s
  • 縱軸表示 translateY 的值,為 0 時表示 translateY 的值為 0,縱軸一格表示 50px
  1. animation-fill-mode: none 表現如圖:

一句話總結,元素在動畫時間之外,樣式只受到它的 CSS 規則限制,與 @keyframes 內的關鍵幀定義無關。

  1. animation-fill-mode: backwards 表現如圖:

一句話總結,元素在動畫開始之前(包含未觸發動畫階段及 animation-delay 期間)的樣式為動畫執行時的第一幀,而動畫結束後的樣式則恢復為 CSS 規則設定的樣式。

  1. animation-fill-mode: forwards 表現如圖:

一句話總結,元素在動畫開始之前的樣式為 CSS 規則設定的樣式,而動畫結束後的樣式則表現為由執行期間遇到的最後一個關鍵幀計算值(也就是停在最後一幀)。

  1. animation-fill-mode: both 表現如圖:

一句話總結,綜合了 animation-fill-mode: backwardsanimation-fill-mode: forwards 的設定。動畫開始前的樣式為動畫執行時的第一幀,動畫結束後停在最後一幀。

animation-iteration-count/animation-direction 動畫迴圈次數和方向

講到了 animation-fill-mode,我們就可以順帶講講這個兩個比較好理解的屬性 -- animation-iteration-countanimation-direction

  • animation-iteration-count 控制動畫執行的次數,可以是數字或者 infinite,注意,數字可以是小數
  • animation-direction 控制動畫的方向,正向、反向、正向交替與反向交替

在上面講述 animation-fill-mode 時,我使用了動畫執行時的第一幀替代了@keyframes 中定義的第一幀這種說法,因為動畫執行的第一幀和最後一幀的實際狀態還會受到動畫執行方向 animation-directionanimation-iteration-count 的影響。

在 CSS 動畫中,由 animation-iteration-countanimation-direction 共同決定動畫執行時的第一幀和最後一幀的狀態。

  1. 動畫執行的第一幀由 animation-direction 決定
  2. 動畫執行的最後一幀由 animation-iteration-countanimation-direction 決定

動畫的最後一幀,也就是動畫執行的最終狀態,並且我們可以利用 animation-fill-mode: forwards 讓動畫在結束後停留在這一幀,這個還是比較好理解的,但是 animation-fill-mode: backwardsanimation-direction 的關係很容易弄不清楚,這裡簡答講解下。

設定一個 100px x 100px 的滑塊,在一個 400px x 100px 的容器中,其程式碼如下:

<div class="g-father">
    <div class="g-box"></div>
</div>
.g-father {
    width: 400px;
    height: 100px;
    border: 1px solid #000;
}
.g-box {
    width: 100px;
    height: 100px;
    background: #333;
}

表現如下:

那麼,加入 animation 之後,在不同的 animation-iteration-countanimation-direction 作用下,動畫的初始和結束狀態都不一樣。

如果設定了 animation-fill-mode: backwards,則元素在動畫未開始前的狀態由 animation-direction 決定:

.g-box {
    ...
    animation: move 4s linear;
    animation-play-state: paused;
    transform: translate(0, 0);
}
@keyframes move {
    0% {
        transform: translate(100px, 0);
    }
    100% {
        transform: translate(300px, 0);
    }
}

注意這裡 CSS 規則中,元素沒有設定位移 transform: translate(0, 0),而在動畫中,第一個關鍵幀和最後一個關鍵的 translateX 分別是 100px300px,配合不同的 animation-direction 初始狀態如下。

下圖假設我們設定了動畫預設是暫停的 -- animation-play-state: paused,那麼動畫在開始前的狀態為:

動畫的分治與複用

講完了每一個屬性,我們再來看看一些動畫使用過程中的細節。

看這樣一個動畫:

<div></div>
div {
    width: 100px;
    height: 100px;
    background: #000;
    animation: combine 2s;
}
@keyframes combine {
    100% {
        transform: translate(0, 150px);
        opacity: 0;
    }
}

這裡我們實現了一個 div 塊下落動畫,下落的同時產生透明度的變化:

對於這樣一個多個屬性變化的動畫,它其實等價於:

div {
    animation: falldown 2s, fadeIn 2s;
}

@keyframes falldown {
    100% {
        transform: translate(0, 150px);
    }
}
@keyframes fadeIn {
    100% {
        opacity: 0;
    }
}

在 CSS 動畫規則中,animation 是可以接收多個動畫的,這樣做的目的不僅僅只是為了複用,同時也是為了分治,我們對每一個屬性層面的動畫能夠有著更為精確的控制。

keyframes 規則的設定

我們經常能夠在各種不同的 CSS 程式碼見到如下兩種 CSS @keyframes 的設定:

  1. 使用百分比

    @keyframes fadeIn {
     0% {
         opacity: 1;
     }
     100% {
         opacity: 0;
     }
    }
  2. 使用 fromto

    @keyframes fadeIn {
     from {
         opacity: 1;
     }
     to {
         opacity: 0;
     }
    }

在 CSS 動畫 @keyframes 的定義中,from 等同於 0%,而 to 等同於 100%

當然,當我們的關鍵幀不止 2 幀的時,更推薦使用百分比定義的方式。

除此之外,當動畫的起始幀等同於 CSS 規則中賦予的值並且沒有設定 animation-fill-mode0%from 這一幀是可以刪除的。

動畫狀態的高優先順序性

我曾經在這篇文章中 -- 深入理解 CSS(Cascading Style Sheets)中的層疊(Cascading) 講過一個很有意思的 CSS 現象。

這也是很多人對 CSS 優先順序的一個認知誤區,在 CSS 中,優先順序還需要考慮選擇器的層疊(級聯)順序

只有在層疊順序相等時,使用哪個值才取決於樣式的優先順序。

那什麼是層疊順序呢?

根據 CSS Cascading 4 最新標準:

CSS Cascading and Inheritance Level 5(Current Work)

定義的當前規範下申明的層疊順序優先順序如下(越往下的優先順序越高,下面的規則按升序排列):

  • Normal user agent declarations
  • Normal user declarations
  • Normal author declarations
  • Animation declarations
  • Important author declarations
  • Important user declarations
  • Important user agent declarations
  • Transition declarations

簡單翻譯一下:

按照上述演算法,大概是這樣:

過渡動畫過程中每一幀的樣式 > 使用者代理、使用者、頁面作者設定的!important樣式 > 動畫過程中每一幀的樣式優先順序 > 頁面作者、使用者、使用者代理普通樣式。

然而,經過多個瀏覽器的測試,實際上並不是這樣。(尷尬了)

舉個例子,我們可以通過這個特性,覆蓋掉行內樣式中的 !important 樣式:

<p class="txt" style="color:red!important">123456789</p>
.txt {
    animation: colorGreen 2s infinite;
}
@keyframes colorGreen {
    0%,
    100% {
        color: green;
    }
}

在 Safari 瀏覽器下,上述 DEMO 文字的顏色為綠色,也就是說,處於動畫狀態中的樣式,能夠覆蓋掉行內樣式中的 !important 樣式,屬於最最高優先順序的一種樣式,我們可以通過無限動畫、或者 animation-fill-mode: forwards,利用這個技巧,覆蓋掉本來應該是優先順序非常非常高的行內樣式中的 !important 樣式。

我在早兩年的 Chrome 中也能得到同樣的結果,但是到今天(2022-01-10),最新版的 Chrome 已經不支援動畫過程中關鍵幀樣式優先順序覆蓋行內樣式 !important 的特性。

對於不同瀏覽器,感興趣的同學可以利用我這個 DEMO 自行嘗試,CodePen Demo - the priority of CSS Animation

CSS 動畫的優化

這也是非常多人非常關心的一個重點。

我的 CSS 動畫很卡,我應該如何去優化它?

動畫元素生成獨立的 GraphicsLayer,強制開始 GPU 加速

CSS 動畫很卡,其實是一個現象描述,它的本質其實是在動畫過程中,瀏覽器重新整理渲染頁面的幀率過低。通常而言,目前大多數瀏覽器重新整理率為 60 次/秒,所以通常來講 FPS 為 60 frame/s 時動畫效果較好,也就是每幀的消耗時間為 16.67ms。

頁面處於動畫變化時,當幀率低於一定數值時,我們就感覺到頁面的卡頓。

而造成幀率低的原因就是瀏覽器在一幀之間處理的事情太多了,超過了 16.67ms,要優化每一幀的時間,又需要完整地知道瀏覽器在每一幀幹了什麼,這個就又涉及到了老生常談的瀏覽器渲染頁面。

到今天,雖然不同瀏覽器的渲染過程不完全相同,但是基本上大同小異,基本上都是:

簡化一下也就是這個圖:

這兩張圖,你可以在非常多不同的文章中看到。

迴歸本文的重點,Web 動畫很大一部分開銷在於層的重繪,以層為基礎的複合模型對渲染效能有著深遠的影響。當不需要繪製時,複合操作的開銷可以忽略不計,因此在試著除錯渲染效能問題時,首要目標就是要避免層的重繪。那麼這就給動畫的效能優化提供了方向,減少元素的重繪與迴流

這其中,如何減少頁面的迴流與重繪呢,這裡就會運用到我們常說的 GPU 加速

GPU 加速的本質其實是減少瀏覽器渲染頁面每一幀過程中的 reflow 和 repaint,其根本,就是讓需要進行動畫的元素,生成自己的 GraphicsLayer

瀏覽器渲染一個頁面時,它使用了許多沒有暴露給開發者的中間表現形式,其中最重要的結構便是層(layer)。

在 Chrome 中,存在有不同型別的層: RenderLayer(負責 DOM 子樹),GraphicsLayer(負責 RenderLayer 的子樹)。

GraphicsLayer ,它對於我們的 Web 動畫而言非常重要,通常,Chrome 會將一個層的內容在作為紋理上傳到 GPU 前先繪製(paint)進一個點陣圖中。如果內容不會改變,那麼就沒有必要重繪(repaint)層。

而當元素生成了自己的 GraphicsLayer 之後,在動畫過程中,Chrome 並不會始終重繪整個層,它會嘗試智慧地去重繪 DOM 中失效的部分,也就是發生動畫的部分,在 Composite 之前,頁面是處於一種分層狀態,藉助 GPU,瀏覽器僅僅在每一幀對生成了自己獨立 GraphicsLayer 元素層進行重繪,如此,大大的降低了整個頁面重排重繪的開銷,提升了頁面渲染的效率。

因此,CSS 動畫(Web 動畫同理)優化的第一條準則就是讓需要動畫的元素生成了自己獨立的 GraphicsLayer,強制開始 GPU 加速,而我們需要知道是,GPU 加速的本質是利用讓元素生成了自己獨立的 GraphicsLayer,降低了頁面在渲染過程中重繪重排的開銷。

當然,生成自己的獨立的 GraphicsLayer,不僅僅只有 transform3d api,還有非常多的方式。在 CSS 中,包括但不限於(找了很多文件,沒有很全面的,需要一個一個去嘗試,通過開啟 Chrome 的 Layer border 選項):

  • 3D 或透視變換(perspective、transform) CSS 屬性
  • 使用加速視訊解碼的 <video> 元素
  • 擁有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
  • 混合外掛(如 Flash)
  • 對自己的 opacity 做 CSS 動畫或使用一個動畫變換的元素
  • 擁有加速 CSS 過濾器的元素
  • 元素有一個包含複合層的後代節點(換句話說,就是一個元素擁有一個子元素,該子元素在自己的層裡)
  • 元素有一個 z-index 較低且包含一個複合層的兄弟元素(換句話說就是該元素在複合層上面渲染)

對於上述一大段非常繞的內容,你可以再看看這幾篇文章:

除了上述準則之外,還有一些提升 CSS 動畫效能的建議:

減少使用耗效能樣式

不同樣式在消耗效能方面是不同的,改變一些屬性的開銷比改變其他屬性要多,因此更可能使動畫卡頓。

例如,與改變元素的文字顏色相比,改變元素的 box-shadow 將需要開銷大很多的繪圖操作。box-shadow 屬性,從渲染角度來講十分耗效能,原因就是與其他樣式相比,它們的繪製程式碼執行時間過長。這就是說,如果一個耗效能嚴重的樣式經常需要重繪,那麼你就會遇到效能問題。

類似的還有 CSS 3D 變換、mix-blend-modefilter,這些樣式相比其他一些簡單的操作,會更加的消耗效能。我們應該儘可能的在動畫過程中降低其使用的頻率或者尋找替代方案。

當然,沒有不變的事情,在今天效能很差的樣式,可能明天就被優化,並且瀏覽器之間也存在差異。

因此關鍵在於,我們需要針對每一起卡頓的例子,藉助開發工具來分辨出效能瓶頸所在,然後設法減少瀏覽器的工作量。學會 Chrome 開發者工具的 Performance 皮膚及其他渲染相關的皮膚非常重要,當然這不是本文的重點。大家可以自行探索。

使用 will-change 提高頁面滾動、動畫等渲染效能

will-change 為 Web 開發者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器可以在元素屬性真正發生變化之前提前做好對應的優化準備工作。 這種優化可以將一部分複雜的計算工作提前準備好,使頁面的反應更為快速靈敏。

值得注意的是,用好這個屬性並不是很容易:

  • 不要將 will-change 應用到太多元素上:瀏覽器已經盡力嘗試去優化一切可以優化的東西了。有一些更強力的優化,如果與 will-change 結合在一起的話,有可能會消耗很多機器資源,如果過度使用的話,可能導致頁面響應緩慢或者消耗非常多的資源。
  • 有節制地使用:通常,當元素恢復到初始狀態時,瀏覽器會丟棄掉之前做的優化工作。但是如果直接在樣式表中顯式宣告瞭 will-change 屬性,則表示目標元素可能會經常變化,瀏覽器會將優化工作儲存得比之前更久。所以最佳實踐是當元素變化之前和之後通過指令碼來切換 will-change 的值。
  • 不要過早應用 will-change 優化:如果你的頁面在效能方面沒什麼問題,則不要新增 will-change 屬性來榨取一丁點的速度。 will-change 的設計初衷是作為最後的優化手段,用來嘗試解決現有的效能問題。它不應該被用來預防效能問題。過度使用 will-change 會導致大量的記憶體佔用,並會導致更復雜的渲染過程,因為瀏覽器會試圖準備可能存在的變化過程。這會導致更嚴重的效能問題。
  • 給它足夠的工作時間:這個屬性是用來讓頁面開發者告知瀏覽器哪些屬性可能會變化的。然後瀏覽器可以選擇在變化發生前提前去做一些優化工作。所以給瀏覽器一點時間去真正做這些優化工作是非常重要的。使用時需要嘗試去找到一些方法提前一定時間獲知元素可能發生的變化,然後為它加上 will-change 屬性。

有人說 will-change 是良藥,也有人說是毒藥,在具體使用的時候,可以多測試一下。

最後

好了,本文從多個方面,由淺入深地描述了 CSS 動畫我認為的一些比較重要、值得一講、需要注意的點。當然很多地方點到即止,或者限於篇幅沒有完全展開,很多細節還需要讀者進一步閱讀規範或者自行嘗試驗證,實踐出真知,紙上得來終覺淺。

OK,本文到此結束,希望本文對你有所幫助 :)

想 Get 到最有意思的 CSS 資訊,千萬不要錯過我的公眾號 -- iCSS前端趣聞 ?

更多精彩 CSS 技術文章彙總在我的 Github -- iCSS ,持續更新,歡迎點個 star 訂閱收藏。

如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。