
華麗的特效很炫沒錯,但往往伴隨著效能卡頓的問題 QQ。在 網頁完整渲染的過程,會經過一系列的步驟,直到讓使用者看見畫面,光是改變元素大小,就會使流程在走一遍,這對瀏覽器是非常複雜的。

有些 CSS 屬性改變會觸發一整個過程,尤其是改變物理上的距離、大小 (margin 、 width 等等),不過並不是所有的過程都必須經歷,在優化上可以盡量選擇影響幅度小的屬性,例如 : transform 就是一個好選擇。
舉例 文字畫線的動畫  就可以好幾種方法表示  :
這幾種牽涉網頁渲染路徑各有不同 ,會發現 width 跟 right 多走了一個步驟
在 motion API 有提供 layer props ,把會使瀏覽器操作 layout 的部份進行優化,改成較少效能消耗的 transform,至於怎麼做呢 ?  初步了解是這樣的 :

這裡只講到大概的原理,實際上應該會有產生正確位置的元素,並且在最後做交換。
有興趣可以參考 Matt Perry (framer motion 的維護者之一) 在 2020 Next.js conf 上 簡單解釋 layout props 做哪些事,以及他在 新的 motion library 上更深入的解釋 。
layout propslayout props 應用時機 ?在前幾篇的文章中都使用 animate 或 initial 達到基本動畫操作,這些操作有一些共通點,他們不會 reflow ,像是 x 、y 位移是對應 transform ,基本上對效能來說負擔不大,因為都只走 paint 這條路。
在 官方範例 舉 flex 改變 justify-content 為例, 其屬性有 : flex-start、 center 與 flex-end,那為什麼位移不能直接 flex-start 安排到 flex-end 呢  ? 這樣也省去 x 計算值的問題。
// 官方範例 : https://codesandbox.io/s/framer-motion-2-layout-animations-kij8p?from-embed
.switch {
  display: flex;
  justify-content: flex-start;
  //... 略
}
// 下面會看到 jsx 怎麼被加入 data-
.switch[data-isOn="true"] {
  justify-content: flex-end;
}
// 官方應用 data- 的方式改變,當 isOn state 改變就改變 justify-content 的值
<div className="switch" data-isOn={isOn} onClick={toggleSwitch}>
  {/* 添加了 layout */}
  <motion.div className="handle" layout transition={spring} />
</div>
先來看看 CSS trigger 對改變 justify-content 所觸發的渲染包含那些階段 :
從上圖可知道會影響到其他元素的位置,進行重排 (reflow) 的操作,來看一下實際上的差別。
layout props 會發現明顯的截斷,有 layout props 動畫會保持絲滑的運行。
Magic !
(圖源自網路)
官方列舉出使用 layout props 在哪些方面 :
目前已知的問題是 box-shadow 與 border-radius 會在 transform 轉化過程中出現扭曲 (distort) 現象,為了解決這個問題,官方建議把他設為動畫值,motion 會自動矯正扭曲的問題,不過還是有一定的限制 :
border-radius 只能是用 px 或 % 。box-shadow : 只有單個 box-shadow 的情況。如果不設成動畫值,就使用 還我漂漂拳,打回 style 裡面 :
<motion.div layout style={{ borderRadius: 20 }} />
可以看這篇 非常清楚的範例文章
// CSS
.box {
  width: 20px;
  height: 20px;
  border-radius: 20px;  // 出現點比較早,因此在動畫操作發生扭曲
}
.box[data-expanded="true"] {
  width: 150px;
  height: 150px;
}
  
// JS
<motion.div
  layout
  className="box"
  data-expanded={expanded}
/>
明顯的漸變斷層
// CSS
.box {
  width: 20px;
  height: 20px; 
}
.box[data-expanded="true"] {
  width: 150px;
  height: 150px;
}
  
// JS
<motion.div
  layout
  className="box"
  data-expanded={expanded}
  style={{
    borderRadius: '20px' // 還原在這裡使用
  }}
/>
變得更滑順多了
我把 duration 調長,以至於可以看清楚轉化過程到底發生什麼問題 :
由於我使用 box-shadow 的 inset 做出月亮的缺口,導致會有個順間閃爍。
原始程式碼 :
<motion.div
    className="panBox"
    animate={{
        background: theme ? "#ABD9FF" : "#182747",
    }}
    >
    <motion.span
        className="panThumb"
        animate={{
            background: theme ? "#fa0" : "#182747",
            // 位移是使用 x 而不是 flex 的排版
            x: theme ? 0 : "calc(100px - 40px)",
            rotate: theme ? 0 : -160,
            // 罪魁禍首在這裡
            boxShadow: theme
                ? "inset 0px 0px rgb(0, 0, 0,0)"
                : "inset 15px 8px #fa0",
        }}
        transition={{
            duration: 1,
        }}
        onPan={(e, info) => {
            if (info.offset.x < 0) {
                setTheme(true);
            }
            if (info.offset.x > 0) {
                setTheme(false);
            }
        }}
    />
</motion.div>
接著補上我們的 layout 跟提到的修正方案 :
.panBox{
    // 改成 flex,不用再測距離
    display: flex;
    width : 100px;
    height: 40px;
    border: 1px solid #aaa;
    justify-content: flex-start;
    border-radius: 30px;
    box-shadow: 0 3px 5px rgba(0,0,0,.3),
}
// 當 theme 變化而觸發
.panBox[data-theme=false]{
    justify-content: flex-end;
}
.panThumb{
    //display: inline-block; 有 flex 之後就不用額外設定 inline-block 屬性
    width: 40px;
    height: 40px;
    cursor: pointer;  
    border-radius: 50%;
}
// JS
<motion.div
  className="panBox"
  data-theme={theme}
  layout // 有用到 flex 排版,使用 layout 補強
  animate={{
      background: theme ? "#ABD9FF" : "#182747",
  }}
  >
  <motion.span
      className="panThumb"
      layout
       animate={{
          background: theme ? "#fa0" : "#182747",
          rotate: theme ? 0 : -160,
          /* 改成動畫值 */
          boxShadow: theme
              ? "inset 15px 8px rgba(255, 170, 0,0)"
              : "inset 15px 8px rgba(255, 170, 0,1)",
      }}
      onPan={(e, info) => {
          if (info.offset.x < 0) {
              setTheme(true);
          }
          if (info.offset.x > 0) {
              setTheme(false);
          }
      }}
    />
</motion.div>
或者另一種還我漂漂拳,打回 style ,效果也是一樣的
style={{
    boxShadow: theme
            ? "inset 15px 8px rgba(255, 170, 0,0)"
            : "inset 15px 8px rgba(255, 170, 0,1)",
}}
一天又平安的滑過去了,感謝 layout 的努力
初期常常寫出不知所以然的 CSS style,不知道什麼原因導致動畫不能動 QQ,曾經對著 span 操作 transform ,結果 span 一動也不動,這是因為 inline 屬性並沒有 transform 可以操作,像是 block 或 inline- 的屬性就可以。
參考 : CSS Transforms Module Level 1
A transformable element is an element in one of these categories:
- all elements whose layout is governed by the CSS box model except for non-replaced inline boxes, table-column boxes, and table-column-group boxes [CSS2],
 
layout props 幫我們優化操作布局的元素,但也絕非萬能的,目前已存在的問題包含 :
下一篇會延續 layout 剩下的部分,包含 LayoutGroup 與 layout 在不同地方上的應用。