手機滑文章會卡?兇手是紙紋

寫於 2026-10-23・霜降

今天早上起來把文章列表的計數功能修好了,想說順便用手機打開自己的文章看一下。結果一滑就卡,卡得滿誇張的,滑一滑還會整塊先變空白,過一下才補上來。

我本來以為是 React 一直在重算,就錄了兩份 Performance trace 來看,結果主執行緒根本沒在忙。後來一個一個排除,最後發現問題出在信紙的紙紋上。這篇記錄一下怎麼找到的。

環境

Samsung S24 Ultra(120Hz)、Chrome,頁面是 kitchen-sink 這篇,整頁高約 46,000px。

症狀

滑動文章時會頓、偶爾整塊先變空白再補上,碰互動元件也不順。第一個直覺是 React 在重算,但 trace 完全不是這樣。

trace 說了什麼

兩份 10 秒的錄製,數字長得很像:

  • 主執行緒只忙了約 0.7 到 1.2 秒,沒有超過 50ms 的長任務(唯一的長任務是頁面載入和錄製工具自己啟動)
  • GPU 程序卻忙了 3.1 到 3.8 秒,單一任務最長 360ms
  • 滑動時出現約 40 次 has_missing_content,也就是新內容還沒畫好就先顯示;其中 10 個幀花了中位數 240ms、最長 673ms

120Hz 螢幕每幀預算只有 8.3ms,所以 DevTools 裡一大片「掉幀」有一部分只是預算太小。真正讓人感覺「卡住」的,是那 10 個空白幀。

重點

Bottom-Up 表格看不出問題,因為工作發生在 GPU,不在主執行緒。看 JS 和 Layout 永遠找不到它。

用排除法找兇手

我一直猜錯,所以改成直接在手機 Console 注入 CSS,一類一類關掉,看會不會變順:

  1. 先關背景的雲

    document.querySelector('.cloud-sea').remove(),沒用。那是固定在畫面下方的動畫雲,看起來最可疑,但不是主因。

  2. 全部效果一起關

    動畫、transition、backdrop-filter、filter、mask-image、box-shadow、will-change 全部 none,還是卡。這代表問題不在這幾類。

  3. 隱藏文章本體

    article.prose { visibility: hidden },還是卡。問題在文章「外面」,也就是包住文章的那張信紙。

  4. 只拿掉紙紋

    把 --paper-texture 設成 none,立刻順很多。

前兩個測試之所以沒抓到,是因為我關的清單漏了 background-image、background-blend-mode 和 mask-border,而紙紋正好都在這些裡面。

為什麼紙紋這麼貴

信紙是這樣組成的:

styles.css
.letter-paper::before,
.letter-sheet {
  background: var(--paper-texture), var(--sheet-color, var(--card));
  background-blend-mode: var(--paper-blend); /* multiply */
  mask-border: url("data:image/svg+xml,..."); /* 毛邊 */
}

--paper-texture 裡是 4 張 SVG,全都用 feTurbulence 即時產生雜訊。幾個因素疊在一起:

  1. 濾鏡不會被存成圖:Chrome 把 SVG 背景當成繪製指令,每次光柵化磚塊都重新跑一次濾鏡,numOctaves 越高越貴。
  2. 倍數很大:紙有三層(::before 加兩張底紙),每層疊 4 張雜訊再 multiply,而且每層都和文章一樣高。
  3. 像素很多:S24U 的 DPR 約 3,實際像素是一般桌機螢幕的好幾倍。
  4. 滑動就是不斷產生新磚塊:每個新進畫面的磚塊都要把這堆雜訊重算一遍,趕不上 8.3ms 的預算,就先顯示空白。

下面是同一塊深色紙面,有紋理和沒紋理的差別。拖曳中間的滑桿比較:

深色紙面有紙紋與純色的對比(純色)深色紙面有紙紋與純色的對比(有紙紋)
深色紙面有紙紋與純色的對比

肉眼看有紋理確實比較有質感,但它的代價是每個磚塊都要重算一次。

修法

最小的修法是在手機和平板直接關掉紋理:

styles.css
@media (pointer: coarse), (hover: none), (max-width: 1024px) {
  .letter-paper,
  .dark .letter-paper {
    --paper-texture: none;
  }
}

要保留質感,有兩個更根本的做法:

兩張底紙本來就只露出右邊和下面一小條邊,紋理根本看不到。讓 .letter-sheet 不用 --paper-texture,只留 --sheet-color,雜訊成本直接降到三分之一。

問答

共 3 項

不是。這頁的 Layout 只有約 3,200 個物件,主執行緒的樣式和排版加起來不到一秒,也沒有長任務。我另外試過把整篇文章設成 visibility: hidden(DOM 還在,只是不畫),還是卡。只改掉紙紋那一個 CSS 變數就順了,所以成本在「畫」,不在 DOM 的數量。

小結

卡的時候不要只盯著 JS。主執行緒很空、GPU 卻很忙,就往 CSS 的繪製成本找,像 feTurbulence、混合模式、遮罩這類東西都要懷疑。

排查的時候直接在手機 Console 注入 CSS,一類一類關掉,比改程式碼再部署快很多。只是我第一輪關的清單漏了 background-image、background-blend-mode 和 mask-border,紙紋就一直沒被抓到,清單要把相關的屬性都列進去。

還沒量化

我只確認了「拿掉紙紋後體感順很多」,沒有在修好後再錄一次 trace 比對 GPU 時間,所以這裡沒有「修好後降了多少」的數字。

丙午年 霜降

唯一 書

載入留言區中...