问题
客户端 A/B 测试工具的工作原理是加载一个脚本,该脚本会在浏览器开始构建网页后修改 DOM。如果不进行干预,用户会先短暂看到原始内容,然后内容会闪烁或闪现为实验变体。过去,测试平台通过“防闪烁代码段”来解决此问题,这些代码段会使用 opacity: 0 隐藏整个网页,直到实验脚本完成或任意超时(通常为 4 秒)过去。这种方法会牺牲渐进式渲染,导致用户误点击不可见的内容,并引入不必要的绘制周期。
解决方案
blocking=render 属性允许放置在 <head> 中的 <script> 或 <link> 元素在提取并执行资源之前阻止渲染(但不阻止解析)。这样,实验脚本就会具有与样式表默认情况下相同的渲染阻塞行为,从而确保浏览器在应用实验变体之前绝不会绘制网页。没有不透明度黑客攻击、没有任意超时,也没有闪烁。
实施策略
- 强制性要求:将实验脚本放在文档
<head>中,并添加blocking="render"。 - 强制要求:确保脚本具有
type="module"(首选用于内嵌脚本)或async(首选用于具有src值的外部脚本)。 - 务必确保实验脚本小巧且快速。由于在脚本执行之前渲染会被阻止,因此大型或缓慢的脚本会直接延迟首次绘制。
- 请勿在无需在首次绘制之前运行的脚本上使用
blocking="render"。它仅适用于其输出必须在初始渲染中可见的脚本。 - 请勿将
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');
}
其他注意事项
- 性能影响:
blocking="render"会牺牲首次绘制速度来换取视觉正确性。监控 Largest Contentful Paint (LCP) 和 First Contentful Paint (FCP),确保实验脚本不会造成过度延迟。 - 第三方脚本可靠性:如果实验 SDK 托管在第三方 CDN 上,CDN 服务中断可能会导致完全无法呈现。浏览器会应用自己的超时启发法(可能超过 4 秒),但
blocking="render"没有开发者控制的超时。确保第三方提供商能够提供可靠的正常运行时间保证。 - 服务器端替代方案:对于对性能要求极高的网页,请考虑使用服务器端 A/B 测试(由服务器直接呈现正确的变体),而不是客户端测试。服务器端方法可完全消除闪烁,而不会产生任何渲染阻塞成本。仅当无法进行服务器端测试时,才使用客户端
blocking="render"。