前端技术
#第三方脚本#Core Web Vitals#LCP#TBT#async/defer

第三方脚本如何阻塞主线程:解析器阻塞与长任务对 Core Web Vitals 的影响

本文以新闻门户页面为例,分析第三方脚本如何通过阻塞 HTML 解析、执行长任务和占用网络带宽,拖慢 LCP 与 TBT。文章将解释 async/defer 的局限,并比较按需加载、iframe 隔离、资源提示等缓解措施的收益与代价,帮助开发者做出有依据的工程决策。

新闻门户首页的 HTML 里通常嵌着十几个第三方脚本:广告竞价、访问分析、聊天客服、社交分享按钮。这些脚本不属于站点自身,却常常决定首屏何时出现。HTTP Archive 的 2022 年 Web Almanac 数据显示,几乎所有网站都至少使用一个第三方,接近一半的请求来自第三方。更关键的是,排名前 10 的热门第三方脚本平均阻塞渲染的中位时间达到 1.4 秒。这个数字意味着,在移动设备上,用户可能盯着空白页超过一秒,而这段时间里浏览器主线程正在执行别人的代码。

第三方脚本对性能的影响并非单一途径。它可能阻塞 HTML 解析,让浏览器无法继续发现后续资源;可能执行长任务,让主线程无法响应用户输入;也可能抢占网络带宽,延迟 LCP 图片的下载。要理解这些影响,需要先弄清楚浏览器解析 HTML 时发生了什么,以及脚本标签的默认行为如何打断这个过程。

解析器阻塞:默认脚本如何拖延首屏

浏览器收到 HTML 响应后,会启动解析器逐字节构建 DOM。遇到 <script src="..."> 时,如果没有 async 或 defer 属性,解析器会停下来,先获取并执行该脚本,然后才继续解析后续 HTML。这就是“解析器阻塞”。

为什么浏览器要这样设计?因为脚本可能使用 document.write() 写入新的 HTML,或者修改尚未解析的 DOM 结构。为了确保脚本能访问完整的 DOM,浏览器必须暂停解析,等待脚本执行完毕。这种机制保证了脚本的确定性,代价是首屏延迟。

在新闻门户场景中,如果 <head> 里有一个同步的广告脚本,浏览器会停止解析,等待脚本从广告服务器下载并执行。这段时间里,<body> 中的文章标题、首图等 LCP 元素不会被解析,更不会开始加载。即使 LCP 图片的 URL 就在 HTML 中,预加载扫描器也无法发现它,因为解析器被卡住了。

MDN 文档指出,默认脚本阻塞的是解析,而不是渲染。但解析被阻塞,意味着 DOM 构建停滞,后续的渲染自然无从谈起。因此,解析器阻塞直接推迟了首次内容绘制(FCP)和 LCP。

预加载扫描器:绕过解析器阻塞的机制

现代浏览器内置了预加载扫描器(preload scanner)。它不构建 DOM,而是快速扫描原始 HTML 标记,提前发现图片、样式表、脚本等资源,并立即发起请求。这样,即使主解析器被脚本阻塞,网络层仍能并行下载后续资源。

预加载扫描器能显著缓解解析器阻塞带来的资源发现延迟。但它的能力有限:只能发现 HTML 中直接出现的 URL。如果 LCP 图片的 URL 被隐藏在 data-src 属性中,或者由 JavaScript 动态创建,预加载扫描器就看不到它。web.dev 的文章指出,在 LCP 较差的页面中,35% 的 LCP 图片 URL 无法在初始 HTML 响应中被检测到,这直接导致资源加载延迟。

因此,即使预加载扫描器存在,同步脚本仍然会拖慢首屏。它只是让后续资源提前开始下载,但无法让 LCP 元素提前渲染。

async 与 defer:延迟执行的两种策略

asyncdefer 属性都能消除解析器阻塞。它们的区别在于执行时机。

defer 告诉浏览器:脚本在后台下载,但等到 HTML 解析完成后、DOMContentLoaded 事件触发前再执行。多个 defer 脚本按文档顺序执行。async 则让脚本下载完成后立即执行,不等待解析完成,也不保证执行顺序。

MDN 文档明确说明,async 允许脚本在解析的同时并行获取,并在可用时立即求值。这消除了解析器阻塞,因为浏览器不再需要暂停解析来等待脚本。

但 async/defer 并不能解决所有问题。它们只改变了脚本的执行时机,没有减少脚本本身的工作量。一个大型广告脚本即使延迟执行,一旦开始运行,仍然会占用主线程,可能产生长任务。此外,如果多个 async 脚本同时下载,它们会与 LCP 图片竞争网络带宽,延迟关键资源的加载。

在新闻门户中,常见的做法是给所有第三方脚本加上 async 或 defer。这确实能避免解析器阻塞,但首屏性能可能仍然不佳,因为脚本执行阶段的主线程竞争仍然存在。

长任务与主线程竞争:TBT 与 INP 的恶化

任务(task)是浏览器执行的一段离散工作,包括解析、布局、脚本执行等。当任务时长超过 50 毫秒,就被称为长任务(long task)。长任务会阻塞主线程,导致用户输入(点击、滚动、按键)无法及时处理。

Total Blocking Time(TBT)是 Lighthouse 等工具衡量页面加载期间主线程被长任务阻塞的总时间。它统计从 FCP 到交互时间(TTI)之间所有长任务的阻塞时间之和。TBT 是实验室指标,但它与真实用户指标 INP(Interaction to Next Paint)高度相关,因为长任务同样会延迟用户交互的响应。

第三方脚本是长任务的主要来源。广告脚本可能包含复杂的竞价逻辑、渲染逻辑,分析脚本可能遍历 DOM、发送大量请求。这些脚本执行时,主线程被占用,用户点击按钮后,浏览器无法立即处理点击事件,必须等待当前任务完成。

web.dev 的文章建议,开发者应经常使用 scheduler.yield() 来拆分长任务,让主线程有机会处理交互。但这是对第一方代码的建议,第三方脚本不受开发者控制。因此,减少第三方脚本对主线程的占用,需要从加载策略入手。

网络带宽竞争:第三方如何拖慢 LCP 图片

LCP 元素通常是首屏最大的图片。LCP 时间由资源加载延迟和资源加载时长组成。资源加载延迟是指从导航开始到浏览器发出图片请求的时间,资源加载时长则是下载图片本身所需的时间。

第三方脚本会同时影响这两个阶段。解析器阻塞会推迟图片的发现,增加加载延迟。而多个第三方脚本同时下载时,会与图片竞争网络带宽,尤其是在弱网环境下,这种竞争会显著延长图片的下载时间。

web.dev 的数据显示,在 LCP 较差的页面中,75 百分位的 LCP 图片加载延迟为 1290 毫秒,这超过了快速体验预算的一半。如果第三方脚本占用了大量带宽,这个数字还会进一步恶化。

资源提示(Resource Hints)可以缓解带宽竞争。<link rel="preload"> 可以提前加载 LCP 图片,fetchpriority="high" 可以提高图片的优先级。web.dev 建议,将 fetchpriority="high" 添加到 LCP 图片的 <img> 或预加载标签中,并移除 loading="lazy",以避免延迟。

但资源提示只能优化图片的加载,无法消除第三方脚本本身的主线程占用。

缓解措施:按需加载、iframe 隔离与资源提示

针对第三方脚本的影响,常见的缓解措施有三种:按需加载、iframe 隔离和资源提示。它们各有优劣,适用于不同场景。

按需加载(Lazy Loading)

按需加载是指延迟加载非关键第三方脚本,直到用户需要它们时才加载。例如,聊天客服脚本可以在用户点击“在线咨询”按钮后再加载,社交分享按钮可以在用户滚动到页面底部时再加载。

这种方式能显著减少初始加载时的脚本数量,降低主线程竞争和带宽占用。但实现起来需要修改页面逻辑,将脚本加载从 HTML 静态标签改为 JavaScript 动态注入。

iframe 隔离

iframe 可以将第三方脚本隔离在独立的浏览上下文中。由于 iframe 有自己的主线程(在 Chromium 中,iframe 与主文档共享主线程,但脚本执行是分离的),第三方脚本的长任务不会直接阻塞主文档的交互。

但 iframe 并非万能。它需要额外的网络请求和内存开销,而且 iframe 内部的脚本仍然会与主文档竞争网络带宽。此外,某些第三方功能(如需要访问父页面 DOM 的脚本)无法在 iframe 中工作。

资源提示

资源提示(preload、preconnect、fetchpriority)可以优化资源的加载顺序和优先级,但不能减少脚本的执行成本。preconnect 可以提前与第三方域名建立连接,减少握手延迟;preload 可以提前加载关键资源;fetchpriority 可以调整资源优先级。

这些提示实现简单,只需在 HTML 中添加标签,但效果有限。它们无法解决脚本执行时的长任务问题。

权衡对比

下表总结了三种措施的适用场景、收益和代价。

措施解决的问题实现成本适用场景局限性
按需加载减少初始脚本数量,降低主线程和带宽占用中,需要改造页面逻辑非关键脚本,如聊天、分享用户操作前功能不可用,需要设计加载状态
iframe 隔离隔离脚本执行,避免长任务阻塞主文档低,只需包裹 iframe广告、视频等独立功能增加内存和请求,部分功能受限,仍竞争带宽
资源提示优化资源加载顺序和优先级低,只需添加标签LCP 图片等关键资源不减少脚本执行成本,无法解决长任务

新闻门户的优化实践:一个贯穿场景

假设一个新闻门户首页,LCP 元素是头条新闻的大图。页面中包含以下第三方脚本:

  • 广告脚本(同步,位于 <head>
  • 分析脚本(async)
  • 聊天客服脚本(defer)
  • 社交分享按钮(async)

初始状态:同步广告脚本阻塞解析,LCP 图片发现延迟;async 分析脚本与图片竞争带宽;defer 聊天脚本在解析完成后执行,可能产生长任务。

优化步骤:

  1. 将广告脚本改为异步加载,或使用 iframe 隔离。广告通常需要展示,但可以延迟到首屏渲染后。
  2. 为 LCP 图片添加 fetchpriority="high",并确保其 URL 出现在 HTML 中(使用 <img src> 而非 data-src)。
  3. 将聊天客服脚本改为按需加载,用户点击按钮后再注入。
  4. 对社交分享按钮使用 defer 或按需加载。

优化后的资源加载流程可以用下图表示:

flowchart TD
    A[HTML 响应到达] --> B[预加载扫描器发现 LCP 图片]
    B --> C[发起图片请求,高优先级]
    A --> D[解析 HTML,遇到 async 脚本]
    D --> E[并行下载脚本,不阻塞解析]
    E --> F[脚本下载完成后执行]
    F --> G{执行时长是否超过 50ms?}
    G -- 是 --> H[产生长任务,阻塞主线程]
    G -- 否 --> I[继续执行]
    C --> J[图片下载完成]
    J --> K[渲染 LCP 元素]
    H --> L[用户交互延迟]
    I --> M[主线程空闲,响应交互]

图中显示,预加载扫描器能提前发现图片,但脚本执行仍然可能产生长任务。优化目标是让脚本执行发生在 LCP 渲染之后,或减少其执行时长。

可观测信号与诊断路径

要定位第三方脚本的性能影响,需要观察以下信号:

  • LCP:如果 LCP 时间较长,检查资源加载延迟和资源加载时长。在 DevTools 的 Performance 面板中,查看 LCP 图片的请求开始时间。
  • TBT:Lighthouse 报告会显示 TBT 值。在 Performance 面板中,查看长任务的来源,通常是第三方脚本的 URL。
  • 主线程活动:Performance 面板的 Main 火焰图可以显示每个任务的耗时和调用栈。
  • 网络瀑布图:WebPageTest 的瀑布图可以显示第三方请求与 LCP 图片请求的时间关系。

诊断步骤:

  1. 在无第三方脚本的页面副本上运行 Lighthouse,对比 TBT 和 LCP。
  2. 使用 Chrome DevTools 的 Coverage 工具,检查第三方脚本的代码使用率。
  3. 在 Performance 面板中,记录长任务的开始时间和持续时间,关联到对应的脚本。

失败模式与边界条件

第三方脚本优化并非总是有效,以下情况可能导致方案失效:

  • 脚本依赖:某些第三方脚本在加载时立即执行,且依赖 DOM 结构。如果延迟加载,可能错过初始化时机,导致功能异常。
  • A/B 测试脚本:A/B 测试脚本通常需要尽早执行,以决定渲染哪个版本。如果延迟,可能导致页面闪烁或测试失效。
  • 广告竞价:广告脚本需要尽早参与竞价,延迟可能导致广告收入下降。
  • iframe 限制:某些第三方脚本需要访问父页面数据,iframe 隔离会破坏这种能力。

此外,asyncdefer 的兼容性良好,但 blocking="render" 属性是较新的特性,浏览器支持有限。MDN 文档指出,只有 <head> 中的脚本可以阻塞渲染,且需要显式设置 blocking="render"

结论

第三方脚本对 Core Web Vitals 的影响是多维的:阻塞解析、执行长任务、竞争带宽。asyncdefer 能消除解析器阻塞,但无法解决执行时的主线程占用。按需加载、iframe 隔离和资源提示各有侧重,需要根据脚本的功能和页面场景组合使用。

对于新闻门户而言,最有效的策略是:确保 LCP 图片在 HTML 中可发现并具有高优先级,将非关键脚本延迟加载,对无法避免的脚本使用 iframe 隔离。这些措施能显著降低第三方脚本对 LCP 和 TBT 的影响,但需要权衡功能完整性和实现成本。

资料来源

  1. Web Almanac: Third-party scripts
  2. MDN: Async and defer
  3. Google Developers: Optimize third-party scripts
  4. 提高 Core Web Vitals 最有效的方法  |  Articles  |  web.dev