新闻门户首页的 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:延迟执行的两种策略
async 和 defer 属性都能消除解析器阻塞。它们的区别在于执行时机。
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 聊天脚本在解析完成后执行,可能产生长任务。
优化步骤:
- 将广告脚本改为异步加载,或使用 iframe 隔离。广告通常需要展示,但可以延迟到首屏渲染后。
- 为 LCP 图片添加
fetchpriority="high",并确保其 URL 出现在 HTML 中(使用<img src>而非data-src)。 - 将聊天客服脚本改为按需加载,用户点击按钮后再注入。
- 对社交分享按钮使用
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 图片请求的时间关系。
诊断步骤:
- 在无第三方脚本的页面副本上运行 Lighthouse,对比 TBT 和 LCP。
- 使用 Chrome DevTools 的 Coverage 工具,检查第三方脚本的代码使用率。
- 在 Performance 面板中,记录长任务的开始时间和持续时间,关联到对应的脚本。
失败模式与边界条件
第三方脚本优化并非总是有效,以下情况可能导致方案失效:
- 脚本依赖:某些第三方脚本在加载时立即执行,且依赖 DOM 结构。如果延迟加载,可能错过初始化时机,导致功能异常。
- A/B 测试脚本:A/B 测试脚本通常需要尽早执行,以决定渲染哪个版本。如果延迟,可能导致页面闪烁或测试失效。
- 广告竞价:广告脚本需要尽早参与竞价,延迟可能导致广告收入下降。
- iframe 限制:某些第三方脚本需要访问父页面数据,iframe 隔离会破坏这种能力。
此外,async 和 defer 的兼容性良好,但 blocking="render" 属性是较新的特性,浏览器支持有限。MDN 文档指出,只有 <head> 中的脚本可以阻塞渲染,且需要显式设置 blocking="render"。
结论
第三方脚本对 Core Web Vitals 的影响是多维的:阻塞解析、执行长任务、竞争带宽。async 和 defer 能消除解析器阻塞,但无法解决执行时的主线程占用。按需加载、iframe 隔离和资源提示各有侧重,需要根据脚本的功能和页面场景组合使用。
对于新闻门户而言,最有效的策略是:确保 LCP 图片在 HTML 中可发现并具有高优先级,将非关键脚本延迟加载,对无法避免的脚本使用 iframe 隔离。这些措施能显著降低第三方脚本对 LCP 和 TBT 的影响,但需要权衡功能完整性和实现成本。