# 行動裝置按鈕卡頓:原因與改善

# 為什麼第一次 tap 會卡?

手機的 tap 事件處理比桌面滑鼠複雜很多,有三個層面的問題同時發生。


# 問題一:300ms 延遲

瀏覽器為了判斷使用者是「單擊」還是「雙擊縮放」,預設會等待 300ms 才觸發 click 事件。

使用者手指離開螢幕
        ↓
瀏覽器等待 300ms...(確認是否為雙擊)
        ↓
觸發 click 事件

這 300ms 是肉眼可感受到的延遲。低階手機 JS 執行本來就慢,再加上 300ms,體感上就像「卡了一下」。

解法: 加上 touch-action: manipulation

.btn {
  touch-action: manipulation;
}

告訴瀏覽器「這個元素不支援雙擊縮放」,瀏覽器就不需要等待,直接觸發 click,延遲從 300ms 降到接近 0。


# 問題二:行動裝置的 hover 陷阱

桌面的 hover 是滑鼠移過去觸發,手機沒有游標,瀏覽器的處理方式是:

第一次 tap  →  觸發 :hover(停在 hover 狀態)
第二次 tap  →  觸發 click,離開 hover 狀態

也就是說,如果 CSS 有 :hover 效果(例如位移、變色、放大),使用者第一次 tap 只會看到 hover 動畫,要第二次 tap 才真正觸發點擊。

原本的程式碼:

/* 這段在手機上很有問題 */
.menu-item-card:not(.sold-out):hover {
  transform: translateY(-6px);     /* 第一次 tap 只會看到這個 */
  box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}

解法: 用 @media (hover: hover) 只在「有真正滑鼠的裝置」啟用 hover 效果

/* 只有真實滑鼠的裝置才套用 hover */
@media (hover: hover) {
  .menu-item-card:not(.sold-out):hover {
    transform: translateY(-6px);
    box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
  }
}
/* 行動裝置改用 :active 給即時的視覺回饋 */
.menu-item-card:not(.sold-out):active {
  transform: scale(0.97);
  opacity: 0.85;
}

hover: hover 的媒體查詢意思是:「主要輸入裝置支援 hover(有懸停狀態)」,手機觸控不符合,桌面滑鼠符合。


# 問題三:CSS transition 觸發昂貴的重繪

不同的 CSS 屬性改變,瀏覽器的處理成本差異很大。

# 渲染流水線

JavaScript → Style → Layout → Paint → Composite
步驟 說明 成本
Layout 重新計算元素的位置與尺寸 最高(影響整個頁面)
Paint 重新繪製像素 高
Composite 合成 GPU layer 最低

# 哪些屬性便宜,哪些貴?

屬性 觸發階段 效能
transform Composite only 最快,GPU 處理
opacity Composite only 最快,GPU 處理
background-color Paint 中等
box-shadow Paint 中等偏慢
width , height , margin Layout + Paint 最慢
border-radius Paint 中等

transform 和 opacity 之所以快,是因為它們在 Composite 階段處理,不需要重新 Layout 或 Paint,直接在 GPU 上操作已有的 layer。

# 原本的問題

/* 壞寫法:all 會監聽所有屬性變化 */
transition: all 0.3s ease;

當任何 CSS 屬性改變時(包括 box-shadow 、 color 、甚至 border ),都會觸發 transition 計算。 box-shadow 每一幀都要重新 Paint,低階手機很吃力。

/* 好寫法:只指定 GPU 可加速的屬性 */
transition:
  transform 0.2s ease,
  opacity 0.2s ease;

# 問題四: -webkit-tap-highlight-color

行動裝置 tap 元素時,瀏覽器(尤其是 iOS Safari)會閃一個藍 / 灰色的半透明背景作為點擊回饋,這個閃爍也是一種「視覺卡頓感」。

.btn {
  -webkit-tap-highlight-color: transparent;
}

設成 transparent 後,閃爍消失,改由 :active 的自訂樣式負責給回饋。


# 問題五: will-change 預先建立 GPU Layer

.menu-item-card {
  will-change: transform;
}

瀏覽器看到 will-change: transform ,會在元素靜止時就預先把它提升到獨立的 GPU layer(composite layer)。

沒有這個提示時,動畫第一幀要臨時建立 layer,這個建立過程就是「第一次動比較卡」的原因之一。

注意: will-change 不能亂用,每個加了的元素都會佔用 GPU 記憶體。只加在真的有動畫的關鍵元素上。


# 改動對照表

改動前 改動後 原因
transition: all 0.3s ease transition: transform 0.2s, opacity 0.2s 避免觸發 Paint
:hover 直接寫 @media (hover: hover) 包起來 行動裝置 hover 陷阱
:hover 做位移動畫 :active 做縮放動畫 行動裝置沒有 hover
沒有 touch-action touch-action: manipulation 消除 300ms 延遲
沒有 will-change will-change: transform 預先建立 GPU layer
改動前 改動後 原因
沒有 touch-action touch-action: manipulation 消除 300ms 延遲
沒有 -webkit-tap-highlight-color transparent 消除 iOS 點擊閃爍
:active 沿用長 transition :active 用 0.08s 讓回饋更即時

# 效果預期

問題 改善前 改善後
第一次 tap 延遲 ~300ms ~0ms
第一次 tap 只觸發 hover 是 否(行動裝置不再有 hover)
動畫 GPU 成本 Paint 級別 Composite 級別
點擊視覺回饋 延遲且依賴 hover 即時 :active 縮放

# 延伸:如何自己判斷效能問題

Chrome DevTools → Performance 面板 → 錄製一次 tap 互動

觀察:

  • Long Tasks(超過 50ms 的任務):紅色標記,代表 JS 阻塞主執行緒
  • Layout Shift:綠色標記,代表 Layout 被觸發
  • Paint:綠色區塊,Paint 面積越大越慢

或用 Rendering 面板開啟「Paint flashing」,頁面上會以綠色高亮顯示每次 Paint 的範圍,可以直接看到哪些元素在動畫時造成 Paint。

更新於 閱讀次數 次