無閃爍的用戶端 A/B 測試

網頁功能: blocking="render"

問題說明

用戶端 A/B 測試工具的運作方式,是在瀏覽器開始建構網頁後,載入修改 DOM 的指令碼。如果沒有介入,使用者會先短暫看到原始內容,然後畫面會閃爍或閃現實驗變化版本。測試平台過去會使用「防網頁閃爍程式碼片段」來解決這個問題,也就是使用 opacity: 0 隱藏整個網頁,直到實驗指令碼完成或任意逾時 (通常為 4 秒) 為止。這種做法會犧牲漸進式算繪、允許意外點擊隱形內容,並產生不必要的繪製週期。

解決方法

blocking=render 屬性可讓放置在 <head> 中的 <script> 或 <link> 元素在擷取並執行資源前,封鎖轉譯 (但不會封鎖剖析)。這樣一來,實驗指令碼就會與樣式表一樣,預設為禁止轉譯,確保瀏覽器在套用實驗變化版本前,不會繪製網頁。不需調整不透明度、任意設定逾時時間,也不會出現閃爍。

導入策略

  1. 必要:將實驗指令碼放在文件 <head> 中,並新增 blocking="render"。
  2. 必要:請確認指令碼含有 type="module" (內嵌指令碼建議使用) 或 async (外部指令碼建議使用,且值為 src)。
  3. 建議做法:保持實驗指令碼精簡快速。由於轉譯作業會遭到封鎖,直到指令碼執行完畢為止,因此大型或緩慢的指令碼會直接延遲首次繪製。
  4. 請勿在不需於首次繪製前執行的指令碼上使用 blocking="render"。僅適用於輸出內容必須在初始算繪中顯示的指令碼。
  5. 請勿將 blocking="render" 套用至 <head> 以外的指令碼。只有 <head> 中的指令碼可以封鎖轉譯。

導入指南

基本設定

必要:使用 async 和 blocking="render" 載入實驗指令碼。async 屬性可確保指令碼不會阻擋 HTML 剖析 (瀏覽器在擷取指令碼時會繼續建構 DOM)。blocking="render" 屬性可確保瀏覽器在指令碼執行完畢前,不會繪製任何內容。

<head>
  <!--
    MANDATORY: Both `async` and `blocking="render"` are required.
    - `async`: Prevents parser-blocking, so the DOM is built in parallel.
    - `blocking="render"`: Holds rendering until the script executes,
      ensuring experiment changes are applied before the user sees anything.
  -->
  <script
    src="https://cdn.example.com/experiment-sdk.js"
    async
    blocking="render"
  ></script>
</head>

載入實驗專屬樣式

如果實驗需要子類樣式表,請在 <link> 元素上使用 blocking="render"。根據預設,<head> 中的樣式表已會阻斷轉譯,但動態插入或透過指令碼新增的樣式表則不會。如果動態新增樣式表,或想明確表達意圖,請明確使用 blocking="render"。

<head>
  <!--
    DO: Use blocking="render" on experiment stylesheets that are
    dynamically inserted or conditionally loaded by the experiment SDK.
    This ensures variant styles are applied before first paint.
  -->
  <link
    rel="stylesheet"
    href="https://cdn.example.com/experiment-variant-b.css"
    blocking="render"
  >
</head>

內嵌實驗指令碼

如果實驗邏輯夠輕量,可內嵌,請使用內嵌模組指令碼搭配 blocking="render"。如果實驗邏輯會擷取設定並直接套用 DOM 變更,這個方法就很有用。

<head>
  <!--
    DO: Use an inline module script when the experiment logic is small.
    Module scripts are deferred by default (non-parser-blocking),
    and blocking="render" ensures rendering waits for execution.
  -->
  <script type="module" blocking="render">
    // Fetch the experiment configuration from your testing platform.
    const config = await fetch('/api/experiment?id=homepage-cta')
      .then(res => res.json());

    // Apply the variant by setting a data attribute on <html>.
    // CSS rules keyed to this attribute will style the variant.
    document.documentElement.dataset.variant = config.variant;
  </script>

  <style>
    /* Default styles (control group) */
    .cta-button {
      background-color: blue;
    }

    /* Variant B styles, activated by the data attribute */
    [data-variant="b"] .cta-button {
      background-color: green;
    }
  </style>
</head>

最佳做法

  • 強制規定:實驗指令碼必須快速執行。指令碼速度緩慢會延遲所有轉譯作業。為會阻礙轉譯的指令碼設定效能預算 (例如執行時間不超過 100 毫秒)。
  • 務必將繁重的實驗邏輯與會阻斷轉譯的指令碼分開。載入會套用變化版本的小型轉譯阻斷虛設常式,然後使用 async 或 defer (不含 blocking="render") 分別載入較大的追蹤或數據分析指令碼。
  • 務必在 <html> 或 <body> 上使用資料屬性來表示有效變化版本,並使用以該屬性為索引鍵的 CSS 選取器來設定變化版本樣式。這樣可以避免在轉譯阻斷指令碼中直接操控 DOM。
  • 請勿在數據分析、追蹤或其他非視覺指令碼上使用 blocking="render"。只有必須變更使用者在首次繪製時所見內容的指令碼,才應阻斷轉譯。
  • 請勿將 blocking="render" 與舊版防網頁閃爍程式碼片段合併。兩者解決的是同一個問題,同時使用會造成不必要的延遲。

備用策略

並非所有瀏覽器都支援 blocking 屬性。如果瀏覽器不支援,則會忽略該屬性,並以預設行為載入指令碼 (如上例中的 async),這可能會導致閃爍。必須設定備用方案,以免不支援的瀏覽器發生閃爍。

備用方案:防網頁閃爍程式碼片段

正確做法:只有在不支援 blocking="render" 時,才使用輕量級防網頁閃爍程式碼片段做為備用方案。偵測功能支援,並在原生處理該功能的瀏覽器中略過備用方案。

<head>
  <!--
    DO: Load the experiment script with blocking="render" for
    browsers that support it. This is the preferred approach.
  -->
  <script
    src="https://cdn.example.com/experiment-sdk.js"
    async
    blocking="render"
  ></script>

  <script>
    // DO: Only apply the anti-flicker fallback in browsers
    // that do not support blocking="render".
    if (!Object.hasOwn(HTMLScriptElement.prototype, 'blocking')) {
      // Hide the page until the experiment script runs.
      document.documentElement.classList.add('ab-loading');

      // DO: Set a timeout to reveal the page if the experiment
      // script takes too long. This prevents an indefinitely
      // blank page on slow connections. Adjust the timeout
      // to match your experiment SDK's expected load time.
      setTimeout(() => {
        document.documentElement.classList.remove('ab-loading');
      }, 4000);
    }
  </script>

  <style>
    /*
      DO: Use opacity to hide content during experiment loading.
      This is only applied when blocking="render" is unsupported.
    */
    .ab-loading {
      opacity: 0 !important;
    }
  </style>
</head>
// DO: In your experiment SDK's initialization callback,
// remove the fallback class to reveal the page.
function onExperimentReady() {
  document.documentElement.classList.remove('ab-loading');
}

其他考量

  1. 效能影響:blocking="render" 會犧牲首次繪製速度,以確保視覺正確性。監控 Largest Contentful Paint (LCP) 和 First Contentful Paint (FCP),確保實驗指令碼不會造成過度延遲。
  2. 第三方指令碼可靠性:如果實驗 SDK 託管於第三方 CDN,CDN 服務中斷可能會導致網頁完全無法顯示。瀏覽器會套用自己的逾時經驗法則 (可能超過 4 秒),但開發人員無法控制 blocking="render" 的逾時時間。確保第三方供應商提供可靠的運作時間保證。
  3. 伺服器端替代方案:對於效能至關重要的網頁,建議採用伺服器端 A/B 測試 (由伺服器直接顯示正確的變化版本),而非用戶端測試。伺服器端方法可完全消除閃爍,且不會造成任何轉譯阻斷成本。只有在無法進行伺服器端測試時,才使用用戶端 blocking="render"。