跳至主要內容
← 返回首頁作品集

Apple 式逐格 Hero 的 React 工程細節

文章發布9 分鐘
ReactJSDesign-to-Code網頁效能動畫

需求翻譯成工程

上一篇講了怎麼把 Spline 場景變成 48 幀圖片;這一篇講那些幀怎麼變成 Apple 產品頁式的逐格 Hero。整個元件的規格其實來自幾句很日常的設計回饋——每一句都能直譯成一個工程決定。

設計師的話工程回答
否決點擊啟動:「使用者不能一開始就看到 還 loading 很久」海報優先:priority 的 next/image 就是 LCP;canvas 透明疊在上面,等幀資料存在才作畫
「給 user 的等待時間要夠短 或是中間要有一個一個出現的動畫」進場文字交錯出現(0.15/0.35/0.6s)遮蔽背景抓取;固定時長的時間軸讓慢網路只會縮短演出,絕不延長等待
「動畫要持續撥放 然後跑快點」30fps 無限乒乓循環——擷取不是閉環,硬切會跳幀
「乾脆跑完」不修剪:完整 48 幀旅程都在循環裡
「更後面的幀數開始」START_FRAME 位移:時間軸從旅程中段開場,海報改由該幀輸出,預載也最優先抓它
永遠不讓人等(貫穿所有回饋的主旋律)開場期間任何滾動/觸控/點擊/按鍵都直接跳到 100svh→40svh 收合完成;循環照常播放
Hero 海報幀:紫色星空中正面朝前的大型 KARENYA LIN 3D 字
海報就是 LCP;canvas 播放只在幀真正存在後才疊上來。

技巧清單

  1. 跨步預載順序(0,8,16…4,12…2,6…其餘,START_FRAME 提到最前):不管使用者跳到哪裡,附近永遠有已載入的幀。
  2. 畫最近已載入幀:部分載入時播放不空白,晚到的幀自動回填。
  3. createImageBitmap 把解碼移出渲染路徑(保護 INP),unmount 時逐一 .close()
  4. rAF 循環由 IntersectionObserver 把關:無限 30fps 循環不能在畫面外燒電池;document 隱藏時 rAF 自帶暫停。
  5. ResizeObserver 跨越 100svh→40svh 的高度過渡重新適配 canvas 並重繪當前幀(DPR 上限 2)。
  6. height: 100vh; height: 100svh; 雙寫:iOS 工具列的老問題。
  7. Reduced-motion 用 CSS media query 直接釘住 banner 版面——不需要 JS 狀態翻轉,序列連抓都不抓;Save-Data/2G 用連線檢查走到同一個終點。
  8. AVIF 探測(memoise 的 data-URI 解碼測試)→ WebP 後備 → 最底層 JPG 海報。
  9. 開場疊層交接契約(start prop):false 時海報照常、幀照抓、不播放、不進場、跳過手勢解除武裝;在覆蓋層全黑的那一刻翻成 true。與蠟燭開場序曲成對(解碼閘門、800ms 上限、跳過而非等待)。
  10. React 19 lint 的兩堂課react-hooks/refs——渲染期間不讀 ref(改用 effect 裡的閉包旗標);react-hooks/set-state-in-effect——effect 本體不同步 setState(推遲到 timer,或干脆用 CSS 釘住狀態)。

降級即設計

在不支援 createImageBitmap 的舊 Safari(15 以前)上:抓取迴圈逐幀靜默失敗、canvas 從不作畫——但海報、進場文字與收合動畫全部照常。訪客得到一個安靜的靜態 hero,而不是一個壞掉的動畫。降級不是意外,是設計的一部分。

業界脈絡

canvas 逐格 scrub/autoplay 是 Apple 產品頁的招牌手法,JS 圈常見以 GSAP ScrollTrigger 實作。CSS 原生的 scroll-driven animations 已在 Chrome/Edge/Safari 出貨,但 Firefox 穩定版至 2026 年中仍在旗標後(全球支援率約 84%)——這就是 JS 路線仍是相容性首選的原因。本文的做法把「捲動驅動」換成「時間驅動+輸入跳過」,同一套幀基建兩者都能吃。

數字

48 幀;桌機 WebP 總量 337KB(平均約 7KB/幀);手機 141KB;海報 27.9KB;一趟 30fps 播放 1.6 秒。整個 hero 在元件本身之外增加的客戶端 JS 是零——framer-motion 本來就在 bundle 裡,元件只用了 useReducedMotion。