主題: Web platform

Speculation Rules API:預載不是免費加速,先處理副作用

Speculation Rules 能讓 MPA 的下一頁更快,但 prefetch 與 prerender 的成本不同。先排除有副作用的 URL、保留正常導航,再以 activation 與命中率決定是否擴大。

動態迷因(展開/收合)
不是每個連結都值得先打開:登出、結帳與會改變狀態的 URL,要先排除在 speculative loading 之外。 · 來源:GIPHY

第一次看到 Speculation Rules API,很容易想把它當成「讓整站變快的 JSON」。把所有站內連結都預載,看起來不是最有效率嗎?

我現在反而會先問:這個 URL 就算使用者沒有真的前往,先載入會不會做了不該做的事?

這個 API 的目標是未來的整頁導航,不是替 SPA router 偷跑 API 資料。它讓支援的瀏覽器猜下一頁,選擇預先取得文件,或把整個頁面放到隱藏環境跑起來;瀏覽器仍可依記憶體、網路與使用者設定略過這個提示。正常點連結的路徑因此必須一直可用。

先分清:拿一頁,還是先跑一頁

動作 瀏覽器先做什麼 適合的起點
prefetch 取得目標文件的 response body,不載入它引用的子資源 常見、低成本、可預測的下一頁
prerender 在隱藏頁面中取得、render、執行 JavaScript 與載入子資源 命中率很高,且副作用已處理的少數頁面

兩者的差距不只是「快一點」或「快很多」。prerender 的成本更接近真的開了一個看不見的分頁:它會消耗網路、記憶體與 CPU;使用者沒有前往時,這些成本多半白費。它預設也只適合 same-origin 文件。

所以對一般 MPA,我會先用 prefetch 驗證假設;不要因為 API 名字裡有 prerender,就把它當成第一步。

URL 是 GET,不代表可以隨便猜

Speculation Rules 會發出未來導航的請求。若你的站把狀態變更藏在 GET URL,或頁面載入本身會做事,猜錯一次就可能讓使用者遇到奇怪結果。

先排除登出、切換語系、加入購物車、確認操作、個人化流程等路徑。下例只考慮文章頁,並把高風險路徑明確留在外面:

<script type="speculationrules">
  {
    "prefetch": [
      {
        "where": {
          "and": [
            { "href_matches": "/article/*" },
            { "not": { "href_matches": "/logout" } },
            { "not": { "href_matches": "/checkout/*" } }
          ]
        },
        "eagerness": "moderate"
      }
    ]
  }
</script>

這段不是每個瀏覽器都會執行,也不是按下部署鍵就保證每個連結都被預載。它是支援瀏覽器的漸進增強;不支援時,使用者仍是普通、正確地導航。

若站點有嚴格 Content Security Policy,這個內嵌 JSON script 也要被 script-src 明確允許,例如使用 hash、nonce 或 inline-speculation-rules。這個細節比「在 Chrome 看起來很快」更值得先確認。

prerender 會先跑 JavaScript,副作用要等 activation

prefetch 的風險相對小;prerender 則會讓頁面在使用者真正看到之前執行 JavaScript。自訂 analytics、廣告 impression、寫入 local storage、依登入或購物車狀態 render 的元件,都要重新檢查。

Chrome 的指南指出,Google Analytics 等部分服務已能延後到 activation;但這不是自訂 script、tag manager 或第三方 widget 的通行證。自己的程式應清楚決定什麼可以先跑、什麼必須等使用者真的進頁。

function whenActivated() {
  return new Promise((resolve) => {
    if (document.prerendering) {
      document.addEventListener("prerenderingchange", resolve, { once: true });
    } else {
      resolve();
    }
  });
}

async function initSideEffects() {
  await whenActivated();
  initCustomAnalytics();
  refreshCartBadge();
}

void initSideEffects();

延後所有 script 是安全的起點,卻不一定是終點:activation 一次跑太多程式,可能把 INP 問題搬到使用者剛進頁的瞬間。比較好的下一步是逐項檢查:可安全 render 的內容保持預先載入,會記錄、寫入或依即時狀態決定內容的工作再等 activation。

動態迷因(展開/收合)
真正值得慶祝的是使用者真的啟用已準備好的頁面;猜測本身不應先被算成一次造訪或一次轉換。 · 來源:GIPHY

從一兩個高機率頁面開始量

適合測試的候選通常有兩個特徵:下一步很可預測,且頁面很便宜。例如文章列表到熱門文章、固定流程的下一步,或使用者已明確 hover 的單一連結。低命中率的全站規則只是在替使用者多花流量。

我會一起看三件事:

  • 預載要求與實際 activation 的比例,判斷猜測是否命中。
  • 真的被啟用的頁面,LCP/INP 與導覽時間是否改善。
  • 手機網路、server load 與未命中時的浪費是否仍在可接受範圍。

Chrome DevTools 的 Application → Background services → Speculative loads 能顯示規則、候選 URL 與失敗原因;這比只看首頁變快更能回答「這條 rule 到底有沒有生效」。對已 prerender 後被啟用的頁面,navigation timing 的 activationStart 也能協助分開量測。

Speculation Rules API 目前不是 Baseline。這不是拒用理由,卻是設計提醒:把它放在正常導航之上,別讓它成為正確性前提。

我學到什麼

  • 我把「下一頁很快」看成 document lifecycle 的優化,不把它混成 SPA route 或一般資源 preload。
  • prefetch 先驗證使用者旅程;prerender 只留給高命中、無副作用且可處理 activation 的頁面。
  • 速度改善必須同時看命中率、互動指標與資源浪費;一次猜對的 demo 不等於適合全站開啟。

外部參考資料