关于 loading-sequence
面向 Web 开发者的页面加载顺序优化指南,说明如何协调 CSS、字体、图片及一方和第三方 JavaScript,以支持 Core Web Vitals。
主要功能
- 01按 FCP、LCP 和交互需求安排资源
- 02内联关键 CSS 并提前连接字体来源
- 03延后非关键脚本和首屏以下图片
- 04考虑第三方脚本与浏览器优先级行为
适用场景
- 01排查第三方脚本拖慢页面加载
- 02调整关键 CSS、字体和首屏图片的次序
- 03优化页面的 Core Web Vitals
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill loading-sequence安装后在 Claude Code 中输入/loading-sequence确认已加载。
详细介绍
优化加载顺序
目录
每次成功加载网页时,一些关键组件和资源都会在恰当的时机就绪,从而带来流畅的加载体验。这能确保用户认为应用性能出色。良好的用户体验通常也意味着能够通过 Core Web Vitals 测评。
适用场景
- 在优化页面加载性能、提升 Core Web Vitals 表现时使用
- 需要协调加载 1P JS、3P JS、CSS、字体和图片时,此方法很有帮助
- 第三方脚本影响加载性能时使用
说明
- 内联关键 CSS 和字体 CSS;使用 preconnect 预连接外部字体
- 安排资源加载顺序,确保 FCP 先于 LCP 触发,LCP 先于 FID 触发
- 在网络上优先于 ATF 图片开始获取第一方 JS
- 对非关键脚本使用
async或defer属性 - 延迟加载首屏以下的图片和非必要的第三方资源
详情
用于衡量性能的关键指标(例如 First Content Paint、Largest Contentful Paint、First Input Delay 等)直接取决于关键资源的加载顺序。例如,如果主视觉图片等关键资源尚未加载,页面就无法触发 LCP。本文将介绍资源加载顺序与 Web Vitals 之间的关系。我们的目标是明确说明如何优化加载顺序,从而改善 Web Vitals 表现。
在制定理想的加载顺序之前,我们先来了解一下为什么要正确安排加载顺序如此困难。
为什么难以实现最佳加载顺序?
我们有幸为许多合作伙伴的网站进行性能分析,并发现不同合作伙伴网站的页面高效加载都受到多个相似问题的影响。
开发者的预期与浏览器对页面资源的优先级排序方式之间,往往存在明显差距。这常常会导致性能评分不理想。我们进一步分析了造成这一差距的原因,以下几点概括了分析的核心内容。
加载顺序欠佳
优化 Web Vitals 不仅需要充分了解各项指标的含义,还要了解它们的触发顺序,以及它们与不同关键资源之间的关系。FCP 先于 LCP 触发,而 LCP 又先于 FID 触发。因此,实现 FCP 所需的资源应优先于 LCP 所需的资源,而后者又应优先于 FID 所需的资源。
资源的加载顺序和流水线安排往往不正确。这可能是因为开发者不了解指标对资源加载的依赖关系。因此,有时相关资源未能在相应指标触发时及时就绪。
示例:
a) FCP 触发时,主视觉图片应已就绪,以便触发 LCP。 b) LCP 触发时,JavaScript (JS) 应已下载、解析并就绪(或已开始执行),以解除对交互(FID)的阻塞。
网络/CPU 利用率
资源的流水线安排也可能不够合理,无法充分利用 CPU 和网络。这会导致进程受网络限制时,CPU 出现“空闲时间”,反之亦然。 一个很好的例子是可能并发或顺序下载的脚本。并发下载时,带宽会被分摊,因此下载所有脚本所需的总时间在顺序下载和并发下载的情况下相同。如果并发下载脚本,下载期间 CPU 会未得到充分利用。但如果按顺序下载脚本,第一个脚本一下载完成,CPU 就可以开始处理它。这样可以更充分地利用 CPU 和网络。
第三方(3P)产品
网站通常需要 3P 库来添加常见功能。第三方产品包括广告、分析、社交组件、实时聊天,以及其他为网站提供支持的嵌入内容。第三方库自带 JavaScript、图像、字体等资源。
3P 产品通常没有动力针对消费者网站的加载性能进行优化或提供支持。它们可能需要大量 JavaScript 执行,从而延迟交互,或妨碍其他关键资源的下载。
引入 3P 产品的开发者往往更关注它们带来的功能价值,而不是对性能的影响。因此,有时会在没有充分考虑其与整体加载顺序如何配合的情况下,随意添加 3P 资源。这让它们难以控制和安排。
平台特性
不同浏览器在请求优先级处理方式和提示实现方面可能有所不同。深入了解平台及其特性有助于简化优化。特定浏览器的行为会让人难以始终如一地实现预期的加载顺序。
一个例子是 Chromium 平台上的 预加载 bug。可以使用 Preload(<link rel=preload>)指令,告知浏览器尽快下载关键资源。只有确信当前页面会用到该资源时,才应使用此指令。Chromium 中的这个 bug 会导致通过 <link rel=preload> 发出的请求,始终早于预加载扫描器发现的其他请求开始,即使后者的优先级更高。这类问题会打乱优化计划。
HTTP2 优先级处理
协议本身没有提供太多用于调整资源顺序和优先级的选项或控制项。即使可以使用更好的优先级处理原语,底层的 HTTP2 优先级处理问题仍会让最佳排序难以实现。主要问题在于,我们无法预测服务器或 CDN 会以什么顺序处理各个资源的请求。有些 CDN 会重新调整请求优先级,另一些则只实现了部分优先级处理、存在缺陷的优先级处理,或完全不处理优先级。
资源级优化
要有效安排加载顺序,就需要让待安排的资源以最佳方式提供,从而能够快速加载。关键 CSS 应内联,图像应设置正确尺寸,JS 应拆分代码并逐步提供。
框架本身缺少支持代码拆分以及逐步提供 JS 和数据的机制。用户必须依靠以下方式之一来拆分大型的 1P JS 代码块:
- Modern React(Suspense / Concurrent mode / Data Fetching)——目前仍仅供实验使用
- 使用动态导入进行延迟加载——这种方式不够直观,开发者需要手动确定代码拆分边界。
拆分代码时,开发者需要找到恰当的代码块粒度,因为粒度和性能之间存在权衡。
较高的粒度更理想,因为它:
- 尽量减少单个路由以及后续用户交互所需的 JS。
- 支持缓存常见依赖项。这样一来,库发生更改时就无需重新获取整个捆绑包。
与此同时,代码拆分粒度过细也可能有害,因为过多的小块会降低各个代码块的压缩率,并影响浏览器性能。
资源优化还需要消除无效或未使用的代码。现代浏览器往往会加载不必要或过时的 JS,从而影响性能。转译为 ES5 且捆绑了 polyfill 的 JS 对现代浏览器来说没有必要。库和 npm 包通常不会以 ES 模块格式发布,这使打包工具难以进行 tree shaking 和优化。
你可能已经注意到,这些问题并不局限于某一组特定资源或平台。要解决这些问题,需要了解整个技术栈,以及如何整合不同资源以实现最佳指标。
深入了解资源:关系、约束和优先级
在上一节中,我们举了一些例子,说明某些资源是触发 FCP 或 LCP 等特定事件所必需的。在讨论如何处理这些依赖关系之前,我们先来了解所有此类依赖关系。以下是按资源整理的建议、约束和注意事项,定义理想加载顺序前需要考虑这些内容。
关键 CSS
关键 CSS 是指 FCP 所需的最少 CSS。最好将此类 CSS 内联到 HTML 中,而不是从另一个 CSS 文件导入。任何时候都只应下载该路由所需的 CSS,并相应拆分所有关键 CSS。
如果无法内联,应预加载关键 CSS,并从与文档相同的源提供。避免从多个域名提供关键 CSS,或直接使用 Google Fonts 等第三方关键 CSS。也可以由自己的服务端充当第三方关键 CSS 的代理。
延迟获取 CSS 或 CSS 获取顺序不正确,都可能影响 FCP 和 LCP。为避免这种情况,应在网络加载优先级中,将非内联 CSS 排在 1P JS 和首屏图片之前。
内联过多 CSS 会导致 HTML 臃肿,并延长主线程上的样式解析时间,从而损害 FCP。因此,识别关键内容并进行代码拆分至关重要。
内联 CSS 无法缓存。一种变通方法是对 CSS 发起重复请求,以便缓存。但请注意,这可能导致多次整页布局,从而影响 FID。
字体
与关键 CSS 一样,关键字体的 CSS 也应内联。如果无法内联,应使用指定了 preconnect 的脚本加载。延迟获取字体,例如 Google Fonts 或来自其他域名的字体,可能会影响 FCP。Preconnect 会告知浏览器提前与这些资源建立连接。
内联字体会显著增大 HTML,并延迟发起其他关键资源的获取。字体回退可用于解除 FCP 阻塞并显示文本。不过,字体回退可能会因为字体切换而影响 CLS。真实字体到达时,主线程上可能会出现较大的样式和布局任务,也可能因此影响 FID。
首屏图片(ATF)
这是指页面加载时位于视口内、用户最初可见的图片。首屏图片的一种特殊情况是页面的主视觉图。所有首屏图片都应指定尺寸。未指定尺寸的图片会因完全渲染时发生布局偏移而损害 CLS 指标。首屏图片的占位图应由服务端渲染。 延迟加载主视觉图片或使用空白占位符都会导致 LCP 延后。此外,如果占位符尺寸与实际主视觉图片的固有尺寸不匹配,且没有在替换时将图片叠加显示,LCP 就会重新触发。理想情况下,首屏图片不应影响 FCP,但实际中图片可能触发 FCP。
折叠线以下(BTF)图片
这些图片在页面加载时不会立即显示给用户,因此非常适合延迟加载。这可以确保它们不会与 1P JS 或页面所需的重要 3P 资源争抢资源。如果在 1P JS 或重要的 3P 资源之前加载 BTF 图片,FID 就会延迟。
1P JavaScript
1P JS 会影响应用的交互就绪状态。它可能在网络上排在图片和 3P JS 之后延迟加载,也可能在主线程上排在 3P JS 之后延迟执行。因此,它应在网络上先于首屏图片开始加载,并在主线程上先于 3P JS 执行。在服务器端渲染的页面中,1P JS 不会阻塞 FCP 和 LCP。
3P JavaScript
HTML head 中的 3P 同步脚本可能会阻塞 CSS 和字体解析,从而影响 FCP。head 中的同步脚本也会阻塞 HTML body 解析。3P 脚本在主线程上执行时,可能会延迟 1P 脚本执行,并推迟水合和 FID。因此,需要更好地控制 3P 脚本的加载。
无论技术栈和浏览器如何,这些建议和限制通常都适用。请注意,建议有时也会成为限制。例如,内联字体和 CSS 很有用,但内联过多会造成臃肿。关键在于找到「太少、太晚」与「太多、太早」之间的平衡。
以下是 Chrome 资源优先级表中的要点:
- CSS 和字体会以最高优先级加载。这有助于我们优先处理关键 CSS 和字体。
- 脚本的优先级取决于它们在文档中的位置,以及它们是 async、defer 还是阻塞脚本。在第一张图片之前(或文档中较早的位置)请求的阻塞脚本,其优先级高于第一张图片获取之后请求的阻塞脚本。无论 async、defer 还是注入的脚本位于文档中的什么位置,它们的优先级都最低。因此,我们可以使用 async and defer 的相应属性来调整不同脚本的优先级。
- 可见且位于视口中的图片优先级较高(网络:中),而不在视口中的图片优先级较低(网络:最低)。这有助于我们优先处理首屏图片,而非 BTF 图片。
现在来看看如何将以上信息结合起来,制定最佳加载顺序。
理想的加载顺序是什么
了解这些背景后,我们可以提出一种加载顺序,以优化 1P 和 3P 资源的加载。下面提出的顺序以 Next.js 服务端渲染(SSR)作为优化参考。
当前状态
根据我们的经验,以下是我们观察到的 Next.js SSR 应用在优化前的典型加载顺序。
| 资源 | 状态 |
|---|---|
| CSS | CSS 会在 JS 之前预加载,但不会内联 |
| JavaScript | 1P JS 会预加载。3P JS 不受管理,因此仍可能在文档中的任何位置阻塞渲染。 |
| 字体 | 字体既不会内联,也不会使用 preconnect。字体通过外部样式表加载,导致加载延迟。字体可能会阻塞显示,也可能不会。 |
| 图片 | 主视觉图片不会被优先处理。首屏和 BTF 图片都没有经过优化。 |
不含 3P 的建议加载顺序
以下加载顺序考虑了前面讨论的所有约束。我们先来讨论不含 3P 的加载顺序,然后再看看如何在此顺序中穿插 3P 资源。请注意,这里我们将 Google Fonts 视为 1P。
| #(主线程) | 事件 | #(网络) | 请求 |
|---|---|---|---|
| 1 | 解析 HTML | 1 | 小型内联 1P 脚本 |
| 2 | 执行小型内联 1P 脚本 | 2 | 内联关键 CSS(若为外部资源则预加载) |
| 3 | 内联关键字体(若为外部资源则预连接) | ||
| 3 | 解析 FCP 资源(关键 CSS、字体) | 4 | LCP 图片(若为外部资源则预连接) |
| FCP | 5 | 字体(由内联字体 CSS 触发,预连接) | |
| 4 | 渲染 LCP 资源(主视觉图片、文本) | 6 | 非关键 CSS(异步) |
| 7 | 用于交互的第一方 JS | ||
| 8 | 首屏以上的图片(预连接) | ||
| LCP | 9 | 首屏以下的图片 | |
| 5 | 渲染重要的首屏以上图片 | ||
| 视觉完整 | |||
| 6 | 解析非关键 CSS(异步) | ||
| 7 | 执行 1P JS 并进行水合 | 10 | 延迟加载的 JS 块 |
| FID |
此顺序中的一些部分可能直观易懂,以下几点将进一步说明其合理性。
- 我们建议尽可能避免预加载,因为它会迫使你手动预加载每个前置资源,还需要手动整理加载顺序。尤其应避免预加载字体,因为很难判断哪些字体是关键字体。
- 字体 CSS 理想情况下应内联。来自其他源的字体应使用预连接获取。
- 建议对所有来自其他源的资源使用预连接。这样可以提前建立连接,以便下载这些资源。
- 应在用户开始交互(FID)之前获取非关键 CSS。这样可以避免后续渲染此类 CSS 时出现样式问题。
- 应在网络上开始获取第一方 JS,然后再获取首屏以上的图片。下载和解析 JS 需要一些时间。
- 在解析 1P JS 的同时,主线程上的 HTML 解析和首屏以上图片的下载可以并行进行。
使用 3P 的建议加载顺序
最后,我们可以为现代 Web 应用中通常加载的所有关键资源提出一种加载顺序。下面展示了在考虑 3P 资源的情况下,主浏览器线程上的事件和网络请求的顺序。
| # | 主线程事件 | 网络请求 |
|---|---|---|
| 1 | 解析 HTML | 阻塞 FCP 的 3P 资源 |
| 2 | ||
| 3 | 小型内联 1P 脚本 | |
| 4 | 执行小型内联 1P 脚本 | 内联关键 CSS(若为外部资源,则预加载) |
| 5 | 解析阻塞 FCP 的 3P 资源 | 内联关键字体(若为外部资源,则预连接) |
| 6 | 解析 FCP 资源(关键 CSS、字体) | LCP 所需的个性化首屏 3P 图片 |
| 7 | FCP | LCP 图片(若为外部资源,则预连接) |
| 8 | 渲染 LCP 所需的个性化首屏 3P 图片 | 字体(由内联字体 CSS 触发,预连接) |
| 9 | 非关键(异步)CSS | |
| 10 | 渲染 LCP 资源(主视觉图片、文本) | 首次用户交互前必须执行的 3P 脚本 |
| 11 | 用于交互的第一方 JS | |
| 12 | LCP | |
| 13 | 渲染重要的首屏图片 | 默认 3P JS |
| 14 | 解析非关键(异步)CSS | |
| 15 | 执行首次用户交互所需的 3P 脚本 | 首屏以下的图片 |
| 16 | 执行 1P JS 并完成水合 | 延迟加载的 JS 代码块 |
| FID | 次要的 3P JS |
这里主要需要关注的是,如何确保 3P 脚本以最佳方式下载,并按所需顺序执行。
由于脚本请求会发送到另一个域名,建议对以下 3P 请求进行预连接:
- #1 - 阻塞 FCP 的 3P 资源
- #5 - LCP 所需的个性化首屏 3P 图片
- #9 - 首次用户交互前必须执行的 3P 脚本
- #12 - 默认 3P JS
为实现预期的加载顺序,我们建议在 Next 中使用 ScriptLoader 组件。此组件旨在“优化关键渲染路径,确保外部脚本不会成为实现最佳页面加载速度的瓶颈”。与本文讨论最相关的功能是加载优先级。它允许我们在不同里程碑安排脚本,以满足不同的使用场景。以下是可用的加载优先级:
After-Interactive:在下一次水合后加载指定的 3P 脚本。可用于加载标签管理器、广告或分析脚本;我们希望这些脚本尽早执行,但要在 1P 脚本之后。
Before-Interactive:在水合前加载指定的 3P 脚本。适用于希望 3P 脚本在 1P 脚本之前执行的情况。例如:polyfill.io、机器人检测、安全与身份验证、用户同意管理(GDPR)等。
Lazy-Onload:让其他所有资源的优先级都高于指定的 3P 脚本,并延迟加载该脚本。适用于 Google Feedback 等 CRM 组件,或用于分享按钮、评论等功能的特定社交网络脚本。
因此,预连接、脚本属性和 Next.js 的 ScriptLoader 配合使用,可以帮助我们为所有脚本实现预期的加载顺序。
结论
优化应用的责任不仅落在所用平台的创建者肩上,也落在使用这些平台的开发者肩上。我们需要解决常见问题。我们的目标是从内到外简化加载顺序的管理。经过实践验证、针对不同使用场景的建议,以及 Script Loader 等方案,有助于 React-Next.js 技术栈实现这一目标。下一步是确保新应用符合上述建议。