← 全部指南

菜单与 LCP:导航如何阻塞你最大内容绘制

渲染阻塞菜单脚本:如何延迟加载 JavaScript 而不破坏导航

Async vs defer vs type=module——在不阻塞 LCP 的情况下加载菜单 JS 的策略,包含 Shopify 特定示例。

你有一个不错的菜单。客户喜欢它。它能正常工作。但每次你用 PageSpeed Insights 测试你的店铺时,Google 都会标记一个特定文件:你的菜单脚本。标签说”渲染阻塞 JavaScript”,文件大小 120KB,它给你的移动端 LCP 增加了 600 毫秒。你知道需要修复这个问题,但上次有人建议使用 defer,菜单就坏了。一半的下拉菜单停止工作,移动切换按钮没有反应,你在一小时内就回滚了。

这是一个常见问题。渲染阻塞脚本是 Shopify 店铺中影响最大的性能瓶颈之一,菜单是经常出现的罪魁祸首。但延迟加载 JavaScript 不仅仅是在脚本标签中添加一个属性。如果做错了,菜单就无法工作。如果做对了,你可以在不改动菜单逻辑的情况下,从移动端 LCP 中剪掉半秒钟。

本文解释了渲染阻塞的真正含义,为什么 asyncdefer 表现不同,以及如何加载菜单脚本而不破坏功能。

快速要点
  • 渲染阻塞脚本会暂停所有页面渲染,直到脚本下载、解析和执行完成。
  • defer 并行下载脚本,但将执行延迟到 HTML 解析完成。
  • async 立即下载和执行,这可能会破坏依赖 DOM 顺序的菜单。
  • type="module" 默认延迟执行,并支持现代 JavaScript 功能。
  • 延迟加载后菜单破坏的原因通常是初始化时序问题,而不是 defer 属性本身。

渲染阻塞的真正含义

当浏览器遇到没有加载属性的 <script> 标签时,它会停止一切。它暂停 HTML 解析,下载脚本,解析脚本,执行脚本,然后才继续构建页面。这称为同步或渲染阻塞行为。

这种行为的逻辑很古老但合理:JavaScript 可以用 document.write() 修改 DOM,所以浏览器假设每个脚本都可能改变页面的结构,因此必须在继续之前运行。

这就是问题所在。你的菜单脚本不使用 document.write()。它不需要在页面其余部分解析之前运行。但浏览器不知道这一点,所以它还是会阻塞。

根据 Chrome 自己关于优化最大内容绘制的文档,消除渲染阻塞 JavaScript 是你能做的影响最大的改进之一。当文档顶部的脚本阻塞 400 毫秒时,DOM 中它下方的每个元素——你的英雄图片、你的标题、你的产品网格——都要多等 400 毫秒才能绘制。

三种加载策略:默认、Defer 和 Async

浏览器有三种方式可以加载脚本。

默认(同步)

<script src="menu.js"></script>

浏览器停止解析 HTML,下载 menu.js,解析它,执行它,然后恢复。这会在整个持续期间阻塞渲染。

Defer

<script src="menu.js" defer></script>

浏览器并行下载 menu.js 和 HTML 解析,但直到整个 HTML 文档解析完成才执行它。多个延迟脚本按它们在 HTML 中出现的顺序执行。

Async

<script src="menu.js" async></script>

浏览器并行下载 menu.js 和 HTML 解析,一旦下载完成就立即执行它,即使 HTML 解析未完成。如果多个脚本使用 async,执行顺序是不可预测的。

这是一个总结行为的表格:

属性 下载 执行 阻塞 HTML 解析 执行顺序
无(默认) 阻塞解析 立即 顺序执行
defer 并行 HTML 解析后 顺序执行
async 并行 就绪后立即 是(执行期间) 不可预测

对于大多数导航菜单,defer 是正确的选择。它保持脚本顺序,当一个脚本依赖另一个时很重要(例如,依赖工具库的菜单脚本)。它保证 DOM 在脚本运行前已准备好,这对任何查询元素的代码至关重要。

为什么 Defer 会破坏某些菜单(以及如何修复)

你在菜单脚本标签中添加 defer,重新加载页面,菜单无法工作。下拉菜单无法打开。移动切换不起作用。你移除 defer,一切都正常了。发生了什么?

问题几乎总是初始化时序。这是三个最常见的原因。

问题 1:脚本在主题 JavaScript 准备好前运行

许多 Shopify 主题有其他脚本所依赖的全局 JavaScript 对象或初始化函数。如果你的菜单脚本在该对象创建前运行,它就会失败。

示例:

// theme.js(首先加载)
window.Theme = { utils: { ... } };

// menu.js(第二个加载,期望 Theme 存在)
Theme.utils.initDropdown();

如果 menu.js 延迟但 theme.js 没有,menu.js 可能会在 theme.js 完成执行前运行,Theme 将是 undefined。

修复方法: 延迟两个脚本,或确保 menu.js 在初始化前检查依赖。

if (typeof Theme !== 'undefined') {
  Theme.utils.initDropdown();
}

问题 2:内联脚本在延迟脚本前运行

内联脚本(直接在 HTML 中编写的 JavaScript,而不是从外部文件加载)按遇到的顺序在文档中执行。延迟脚本在整个 HTML 解析后执行,这意味着在所有内联脚本之后。

如果你有内联脚本尝试初始化菜单,而菜单库延迟加载,初始化将失败。

修复方法: 将初始化逻辑移到延迟脚本中,或用 DOMContentLoaded 监听器包装内联初始化。

document.addEventListener('DOMContentLoaded', function() {
  if (typeof MenuApp !== 'undefined') {
    MenuApp.init();
  }
});

问题 3:多个延迟脚本的竞态条件

如果你的菜单依赖工具库,两者都延迟加载,它们应该按在 HTML 中出现的顺序执行。但如果工具库异步加载(使用 async 而不是 defer),执行顺序变得不可预测。

修复方法: 对所有相互依赖的脚本使用 defer,而不是 async。如果必须使用 async,添加显式依赖检查或使用动态导入。

Type=”module”:现代替代方案

现代 JavaScript 支持 ES6 模块,它们有内置的延迟加载。

<script type="module" src="menu.js"></script>

带有 type="module" 的脚本自动延迟。它们并行下载,在 HTML 解析完成后执行,就像 defer 一样。它们也支持 importexport,这使依赖管理显式。

如果你的菜单脚本是作为模块编写的,你可以直接导入依赖:

import { initDropdown } from './dropdown.js';
initDropdown();

这消除了传统 defer 导致破坏的时序问题,因为浏览器确切知道哪些脚本依赖哪些。

缺点:旧浏览器(Internet Explorer、非常旧的 Safari 版本)不支持模块。但根据 Can I Use,截至 2025 年,全球超过 95% 的浏览器流量支持 ES6 模块。对于大多数 Shopify 店铺,这是安全的。

Shopify 特定考虑

Shopify 主题根据主题架构以不同的方式加载脚本。以下是延迟加载在最常见模式中的工作方式。

Theme.liquid 脚本标签

如果你的菜单脚本像这样在 theme.liquid 中加载:

{{ 'menu.js' | asset_url | script_tag }}

script_tag 过滤器默认不添加 defer。你需要用手动 <script> 标签替换它:

<script src="{{ 'menu.js' | asset_url }}" defer></script>

应用嵌入块

如果你的菜单作为应用嵌入安装(第三方菜单应用很常见),应用控制其脚本如何加载。你无法自己添加 defer。你需要与应用开发者联系或切换到已经异步加载脚本的菜单应用。

像 Navi+ 这样的工具默认用 defer 加载它们的 JavaScript,这是它们在 LCP 测试中性能比传统菜单应用更好的原因之一。

区块渲染和延迟脚本

Shopify 2.0 主题动态加载区块。如果区块包含延迟脚本,并且区块在初始加载后添加到页面(例如,通过 AJAX 进行快速查看或无限滚动),脚本可能不会按预期执行。

修复方法: 只对初始页面加载使用 DOMContentLoaded。对于动态添加的区块,使用自定义事件或 mutation observer 手动触发初始化。

如何测试 Defer 是否有效

在将延迟脚本加载部署到你的在线店铺之前,彻底测试它。这是一个检查清单。

  1. 桌面 Chrome: 打开你的店铺,打开 DevTools,转到”网络”标签,重新加载,验证菜单脚本与其他资源并行下载,不阻塞文档。
  2. 移动模拟: 在 DevTools 中,切换到移动仿真(iPhone 或 Android),将网络限流到”快速 3G”,然后重新加载。菜单应该仍然正确初始化。
  3. 跨浏览器: 在 Safari、Firefox 和 Edge 中测试。模块脚本在 Safari 中的行为略有不同。
  4. 真实设备: 在真实移动连接下在实际手机上打开店铺。仿真很有用,但真实世界的延迟暴露了桌面测试错过的时序错误。
  5. 功能检查: 打开每个下拉菜单,测试移动菜单切换,点击每个链接,验证没有任何破坏。

推送前在启用 defer 前后对同一页面运行 PageSpeed Insights 测试。你应该看到 LCP 有明显下降,渲染阻塞资源也减少。如果 LCP 没有改进,脚本可能不是瓶颈,或者可能还有其他要解决的阻塞资源。

何时 Async 有意义(何时没有)

async 适用于完全独立的脚本,不依赖 DOM 顺序或其他脚本。示例包括分析追踪器、聊天小部件和某些广告脚本。

对于导航菜单,async 是有风险的。菜单需要 DOM 准备好,它通常依赖主题工具,它通常需要在客户与页面交互前运行。如果脚本在 HTML 仍在解析时执行,它可能尝试查询不存在的元素。

如果你在考虑对菜单脚本使用 async,脚本必须:

  • 不依赖任何其他脚本
  • 不查询 DOM 元素(或用 DOMContentLoaded 检查包装所有查询)
  • 足够小,其执行时间不阻塞解析

大多数菜单脚本不符合这些标准。除非你有特定原因使用 async,否则坚持使用 defertype="module"

良好的菜单脚本加载看起来像什么

Shopify 店铺上适当优化的菜单脚本看起来像这样:

<script src="{{ 'menu.js' | asset_url }}" defer></script>

或者,如果使用模块:

<script type="module" src="{{ 'menu.js' | asset_url }}"></script>

脚本与页面其余部分并行下载,在 DOM 准备好后执行,不阻塞英雄图片或任何其他 LCP 元素绘制。

在 Chrome DevTools 性能时间线中,你应该看到英雄图片在菜单脚本执行前绘制。如果菜单脚本首先运行,它仍在阻塞。

大多数店铺仅通过从同步切换到延迟脚本加载,就可以从移动端 LCP 中切掉 300 到 600 毫秒。菜单仍然工作。客户不会注意到区别。但 Google 会,更重要的是,浏览器会。页面变得更快可见,这就是要点。

本文是关于更大指南的一部分 菜单与 LCP:导航如何阻塞你最大内容绘制

分享 Facebook X

开始使用 Navi+ AI Menu Builder

选择您的平台 — 免费安装,几分钟内上线。