首屏那张大图不该懒加载,这是 Shopify 主题性能里最值钱的一条规则,也是最常被违反的一条。执行顺序比结论本身更容易出错:先确认哪张图是真正的 LCP 元素,再改它的加载方式,最后才谈优先级和尺寸。顺序反了,常见结果是把 fetchpriority="high" 加在一张并不参与 LCP 的图上,改完也说不清有没有效果。
先确认哪张图才是 LCP 元素
凭设计稿猜不可靠。轮播的第一张、产品页主图这两个位置确实经常是 LCP 元素,但移动端和桌面端经常不是同一张,集合页上更可能是首屏卡片里的某一张。没确认之前不要动手。

先确认哪个元素是 LCP,再动手(光算 · 示意图)
Shopify 官方性能文档给的确认方法是用 DevTools:Performance 面板的 Insights 标签能直接指出 LCP 元素和它的资源加载延迟;Network 面板的瀑布图看这张图的请求什么时候开始,打开 Priority 列确认实际优先级(参考 Shopify 关于 fetchpriority 的文档)。动手前先录一次基线,改完用同样的页面、设备与网络条件再跑一次,否则两组数字没法比。
懒加载是怎么把首屏图片推后的
Liquid 用 image_tag 把 src 直接渲染进 HTML 之后,浏览器的预加载扫描器在解析 HTML 的过程中就能发现这张图的地址,那时 CSS 还没解析完、JavaScript 还没执行、布局还没开始。首屏图片能提前下载,靠的就是这个时间差。
两种写法会把它抵消掉。给 LCP 候选图加 loading="lazy",下载会被推到浏览器处理完一个隐藏的 IntersectionObserver 事件之后;用 lazysizes、lozad、vanilla-lazyload 这类 JavaScript 懒加载库,它们把 src 换成非标准的 data-src,图片地址在脚本执行之前对预加载扫描器完全不可见。Shopify 官方文档把这两种做法直接称为主题里最常见也最有害的性能反模式(Never lazy-load the LCP image)。
具体怎么改
改动位置通常在 sections/ 下渲染首屏的那几个 section,或者被它们引用的 snippets/。别在线上主题直接改,先复制一份主题再动手,确认没问题再发布。步骤按这个顺序做:

滥用会拖慢别的资源(光算 · 示意图)
- 把 LCP 图上的
loading="lazy"改成eager,或者干脆删掉这个属性,立即加载本来就是浏览器的默认行为。 - 如果主题用了 JavaScript 懒加载库,按官方给的三步迁移:先把所有图片的
data-src换回src,再给视口外的图片补上loading="lazy",最后删掉库的 script 标签。 - 判断哪张图在第一屏,优先用
section.index,不要把图片序号写死。 - 改完在副本主题的预览里逐屏看一遍,移动端单独看一遍。
{{ section.settings.image
| image_url: width: 1000
| image_tag: loading: 'eager'
}}
删掉懒加载库之后 data-sizes="auto" 就不能再用了,浏览器没法靠脚本算宽度,每张图都得自己写 sizes。这一步在迁移时最容易漏,漏了的后果是浏览器挑了偏大的源。
section.index 的默认行为,以及 nil 这个坑
image_tag 过滤器自己会做位置判断:第 1 到第 3 个 section 自动用 loading="eager",第 4 个及之后用 loading="lazy"。另外在 section.index0 为 nil 时它也用 eager,而这类场景就是主题编辑器、静态 section 和 Section Rendering API 的响应。依赖这个默认值时,不要再自己设一遍 loading 属性。
要覆盖默认值,测试条件必须写成正向比较:
{% if section.index == 1 %}
{{ section.settings.image
| image_url: width: 1000
| image_tag: loading: 'eager', fetchpriority: 'high'
}}
{% elsif section.index > 3 %}
{{ section.settings.image
| image_url: width: 1000
| image_tag: loading: 'lazy'
}}
{% else %}
{{ section.settings.image
| image_url: width: 1000
| image_tag: loading: 'eager'
}}
{% endif %}
写成 {% if section.index <= 3 %} 会出错:Liquid 里 nil 与数字比较的结果为假,主题编辑器、静态 section 和 Section Rendering API 这些场景恰恰会被送进 else 分支,而它们正是内置默认要保持 eager 的地方。官方文档的建议是先测懒加载那一支,也就是正向的 section.index > 3,其余情况包括 nil 都落到 eager 分支。
集合页:一个 section 里几十张图
集合页的一个 section 可能渲染几十个产品卡片,它们都在同一个 section 里,但只有前几张在视口内,这时 section.index 判断不出差别。用 forloop.index 只让前几张热情加载:
{% for product in collection.products %}
{%- liquid
if forloop.index <= 4
assign image_loading = 'eager'
else
assign image_loading = 'lazy'
endif
-%}
{{ product.featured_image
| image_url: width: 400
| image_tag: loading: image_loading
}}
{% endfor %}
具体留几张视你的栅格定,一般是一行加上第二行的第一张。留多了等于把首屏的资源预算分给看不见的图片。
fetchpriority 的范围要卡死
默认情况下所有图片都以低优先级被发现,等 HTML 解析完、布局完成,浏览器才知道哪些在视口内,然后才把它们升到高优先级。从「本可以开始下载」到「真的开始下载」之间的这段延迟,就是 fetchpriority="high" 要消掉的东西。
- 只加在确认过的 LCP 图上,一个页面只加一张。
- 必须和
loading="eager"一起用,或者干脆不写loading;绝不能和lazy同时出现。 - 前提是这张图在首屏可见,通常是 hero 图或主产品图,并且最好位于第一个 section。
- 滥用会干扰浏览器自己的优先级启发式,反而拖慢别的资源。官方文档在这里有一句提醒:只有当你掌握浏览器还不掌握的重要性信息时才用它。
轮播和画廊是最容易加错的地方:第一张是 LCP 候选,第二张之后的都在视口之外,给它们也加 fetchpriority="high",等于把带宽和下载优先级从真正需要的那张图上分走。如果轮播会在首屏预加载后面几张,先确认这层预加载是主题自带还是某个脚本库加的,再决定动不动它。
如果你只改加载方式和优先级,尺寸先不动,实测往往也能看到改善,但那是把问题从「下载晚」换成「下载后布局再抖一次」,两件事最好一起做。
尺寸与比例:LCP 和 CLS 在这里交叉
加载方式解决的是「什么时候开始下载」,尺寸属性解决的是「下载完之后画面稳不稳」。图片没有已知尺寸时浏览器无法为它预留位置,后续资源异步到达或者内容被动态插入,就会把已经画出来的东西推走。web.dev 在讲 Cumulative Layout Shift 时,把「尺寸未知的图片或视频」列为常见来源。
用 image_tag 时给足 widths 与 sizes,让浏览器自己挑源,别交给脚本去算:
{{ product.featured_image
| image_url: width: 1000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '400, 600, 800, 1000',
sizes: '(min-width: 1000px) 900px, calc(100vw - 2rem)'
}}
容器这一层同样要给比例,常见做法是给包裹元素写死 aspect-ratio,不要让容器的最终高度依赖图片加载完成后撑开。这里有个很隐蔽的坑:如果主题给首屏图套了一层 opacity: 0 的淡入动画,浏览器会一直等到元素以非零透明度重绘才记录 LCP,图片其实早就下完了。官方有一篇文档专门讲这个(Don't hide the LCP image behind animations),页面切换动画、.animate-reveal 一类淡入类名都属于「下载没问题,但画不出来」的情况。用 display: none 隐藏更糟:元素被移出布局,浏览器不为它留空间,脚本再翻回 block 时周围内容要重新排一遍。
改完怎么复测
回到 Performance 面板的 Insights 看 LCP 元素和资源加载延迟有没有变,Network 瀑布图确认这张图的优先级是 High、请求出现的位置足够靠前,这张图的请求应该出现在瀑布图靠前的位置,是被预加载扫描器发现的,而不是等 JavaScript 执行完才出现——如果它排在脚本后面,说明还有懒加载逻辑没清干净。然后带上和去掉属性各跑一次做对照。这里有个口径问题得提前说清:实验室里单次跑出来的分数,和后台基于真实用户数据的汇总本来就不是一回事,数字对不上不代表哪边错了,按后台速度报告与实验室数据的差别那篇的思路,先把条件对齐再比较。
怎么快速判断主题里有没有踩到这些坑
在主题代码里全文搜三个特征就够了:搜 loading: 'lazy' 与 loading="lazy",看首屏那几个 section 有没有命中;搜 lazysizes、data-src、data-sizes,判断有没有 JavaScript 懒加载库;看首屏图用的是 image_tag 还是手写的 img 标签,手写标签拿不到 section 位置的默认判断,加载属性得自己定。
同时要摸清这张图是谁渲染出来的。主题 section 输出的,你改得动;应用注入的区块,比如应用嵌入的横幅和推荐位,你只能控制它出现的位置和占位,图片上的属性归应用方。这条分界决定了哪些问题该写进自己的待办,哪些只能找对方处理,也影响你评估改动工作量时的心态。
还有一个和 LCP 直接相关但常被略过的变量:源图实际输出多大。image_url 的 width 参数决定输出尺寸,参数给得比实际显示宽很多,等于让浏览器下载用不上的像素。不需要精确到像素去算,按容器最宽的情况配一组 widths 就够用。
哪些不属于主题该动的范围
CDN、传输压缩、图片格式与尺寸处理是平台侧默认提供的,不要把这些算成自己的工作量,也别在主题里重复实现一遍。同样,image_tag 对 section 位置的默认判断也是平台行为,自己再设一次 loading 只会把默认覆盖掉,还把主题编辑器里的预览搞坏。这也解释了为什么把站点从 Shopify 换到自建方案并不会自动解决 LCP 问题,两边的可控范围压根不同,见Shopify 与 WordPress 的 SEO 差别。
删懒加载库之前要先确认没有别的功能依赖它。有些主题的轮播、弹窗、延迟加载区块共用同一套库,直接删脚本会出现空白区块。这类改动和自定义 section 引入的资源要一起进性能预算,思路可以参考自定义 Section 的 schema 与复用。
还有一个容易忽略的反转:Chrome、Safari、Firefox 都不会加载 display: none 且带 loading="lazy" 的图片,但用 opacity: 0 隐藏的图片照常加载。也就是说,用 display: none 做「先藏后加载」会连预期中的请求都停掉,而 opacity: 0 不会。这类行为差异只能实测确认,不要照抄别人的写法。
图片加载方式之外,布局偏移里更大的一部分来自字体切换和应用注入的横幅、弹窗,那部分要按应用注入与字体切换的排查顺序单独处理;两者的共同基础是先分清平台负责什么、主题负责什么,见Core Web Vitals 基线的建法。如果整站的速度、抓取与收录口径需要一起拉平,这属于谷歌 SEO 的技术整改范围,我们在谷歌 SEO 服务里按同一套基线流程推进,不承诺具体的分数改善。
最后两条边界要说清楚:fetchpriority="high" 加错位置会让别的关键资源变慢,所以不值得在没确认 LCP 元素的情况下先用为快;单次 PageSpeed 分数也不能当作性能提升的证据。还有一点,改完不要当天就下结论,CDN 缓存和用户本地缓存会让一部分访问仍走旧资源,真实用户数据也要时间积累,隔一段时间按同一口径回看才有意义。