很多人做图片性能优化,第一反应是压体积、换 WebP、上懒加载。这些都对,但有一类问题压缩救不了:图片本身不大,首屏那张主图却迟迟不出来,Lighthouse 里 LCP(Largest Contentful Paint,最大内容绘制)红得刺眼。追下去会发现,问题不在「图多大」,而在「浏览器决定先下哪张」。
浏览器不是傻乎乎地从上到下顺序下载资源。它内部有一套资源优先级(priority)调度模型,谁先谁后、谁抢带宽、谁排队等着,都是这套模型算出来的。而图片,恰恰是这套模型里最容易被「误判」的一类资源。本文不谈压缩、不谈懒加载的实现(那是另一面),只把「优先级 / 预加载」这一条线讲透:浏览器怎么给图片排优先级、fetchpriority 怎么给 LCP 图插队、preload / prefetch 各管什么、它们怎么和懒加载配合、以及最容易踩的「优先级踩踏」坑。
一、先搞懂:浏览器给资源排的那几档优先级
打开 Chrome DevTools 的 Network 面板,右键表头把 Priority 列勾出来,你会看到每个请求后面挂着一档优先级。Chromium 内部大致分五档:Highest、High、Medium、Low、Lowest(有些文档里叫 VeryHigh / VeryLow,是同一套东西的不同叫法)。这档位决定两件事:一是请求进入网络队列的排队顺序,二是在 HTTP/2、HTTP/3 下多路复用时带宽怎么分。
不同资源的默认档位大致是这样的:
- HTML 文档本身:Highest。
- 关键 CSS、
<head>里同步的 JS:Highest / High。 - 字体(被 CSS 引用到的):High。
- 异步 / defer 的脚本:Low。
- 图片:这里是重点------默认是 Low。
图片默认 Low,是浏览器一个刻意的保守策略:一个页面往往有几十上百张图,如果每张都高优先级去抢带宽,关键的 CSS 和 JS 反而被挤到后面,首屏渲染更慢。所以浏览器先把图片一律压到 Low,等布局(layout)算出来、知道哪些图落在视口内,再把「视口内可见」的图提级到 High。
问题就出在这个「等布局算出来」上。等 CSS 下载解析完、布局计算完,才知道你的 LCP 主图在首屏------这一等,主图的高优先级来得太晚,前面几百毫秒它一直以 Low 的身份在队列里排着。对首屏体验最关键的那张图,反而起跑最慢。这就是 LCP 飘红最常见的一类根因。
二、fetchpriority:给 LCP 图一张「插队卡」
fetchpriority 是专门用来手动干预这套优先级的属性,写在标签上就行。取值三个:high、low、auto(默认)。
对首屏那张 LCP 主图,直接标 high:
html
<!-- 首屏 Hero 主图:这就是 LCP 元素,明确告诉浏览器优先拉 -->
<img
src="/img/hero-1600.webp"
srcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
sizes="100vw"
width="1600"
height="900"
fetchpriority="high"
alt="产品首屏主视觉"
/>
加上 fetchpriority="high" 后,浏览器不再等布局算完,在预扫描(preload scanner)阶段解析到这张 <img> 时就直接给它 High,提前进队。实测在图文首屏场景,这一个属性经常能把 LCP 拉快几百毫秒。
反过来,fetchpriority="low" 用来给「明显不重要、但又不想懒加载」的图降级。比如首屏里有个装饰性的背景纹样、一个次要的 logo 墙,它们在视口内会被浏览器提到 High,跟你的 LCP 主图抢带宽。这时候手动把它们标 low,让路给主图:
html
<!-- 首屏内但不重要的装饰图:主动降级,别跟 LCP 图抢带宽 -->
<img src="/img/decoration.webp" width="120" height="120"
fetchpriority="low" alt="" />
fetchpriority 不只作用于 <img>,<link>、<script>、以及 JS 里的 fetch() 都支持:
js
// 关键接口,希望它比默认 fetch 更早拿到带宽
fetch('/api/first-screen-data', { priority: 'high' });
// 埋点上报这种,不急,降级让路
fetch('/api/track', { priority: 'low' });
三、preload:让关键图「提前被发现」
fetchpriority 解决的是「档位」问题,preload 解决的是另一个问题------「发现时机」。
浏览器要下载一张图,前提是先在 HTML 里解析到那个 <img> 标签。但有些关键图不是写死在 HTML 里的:可能是 CSS background-image、可能是 JS 动态插入、可能藏在 <picture> 深处。这些图要等 CSS 下载解析完、或 JS 执行完才被发现,发现得晚,下载自然晚。
<link rel="preload"> 就是提前在 <head> 里「打个招呼」,让浏览器一拿到 HTML 就开始拉,不用等到解析到真正用它的地方:
html
<head>
<!-- CSS 背景图作为 LCP 元素时,preload 提前拉,as 必须写对 -->
<link rel="preload" as="image" href="/img/hero-bg-1600.webp"
fetchpriority="high" />
<!-- 响应式背景图:用 imagesrcset / imagesizes,让浏览器按屏幕挑 -->
<link rel="preload" as="image"
imagesrcset="/img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
imagesizes="100vw" />
</head>
几个必须记住的点:
as属性必须写 。as="image"告诉浏览器这是图片,好按图片的规则设优先级、做同源判断。漏写as,浏览器可能重复下载两次,反而更糟。- preload 和 fetchpriority 可以叠加。preload 管「早发现」,fetchpriority 管「高优先级」,两者正交,关键图两个一起上。
- 响应式背景图用
imagesrcset/imagesizes,别只 preload 一个固定尺寸,否则大屏小屏拉的是同一张,白费带宽。
一个反面典型:把 <img> 里已经写好的首屏图又拿去 preload 一遍。<img> 本来就在 HTML 里,预扫描器早就发现了,再 preload 属于重复劳动,还占用了 <head> 的解析时间。preload 只留给「浏览器自己晚发现」的关键资源 ------CSS 背景图、JS 动态图、字体、被 @import 套娃的样式等。
四、prefetch:赌用户下一步会去哪
preload 是「这个页面马上要用」,prefetch 是「下个页面可能要用」。它优先级最低(Lowest),只在浏览器空闲、带宽富余时才悄悄拉,拉到的资源进 HTTP 缓存,等真跳过去时秒开。
典型场景:详情页的「下一张 / 下一篇」、分页列表的第二页封面图、漏斗里用户大概率会点的下一步:
html
<!-- 赌用户会翻到下一页,空闲时把下一页首图先缓存好 -->
<link rel="prefetch" as="image" href="/img/next-page-cover.webp" />
也可以用 JS 在用户「露出意图」时再动态 prefetch,比 HTML 里写死更省------比如鼠标悬停在「下一篇」链接上、或图片滚动到接近底部时:
js
// 鼠标悬停下一篇链接,才预取它的封面,避免无脑预取浪费流量
function prefetchImage(url) {
// 同一张只预取一次
if (document.querySelector(`link[rel="prefetch"][href="${url}"]`)) return;
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'image';
link.href = url;
document.head.appendChild(link);
}
document.querySelector('.next-post')?.addEventListener(
'mouseenter',
(e) => prefetchImage(e.currentTarget.dataset.cover),
{ once: true }
);
prefetch 是「赌」,赌对了秒开,赌错了白下一张图。所以别对拿不准的链接一股脑 prefetch,尤其移动端,用户流量是真金白银。只对转化路径清晰、跳转概率高的地方用。
五、和懒加载配合:一头一尾,别打架
懒加载(loading="lazy")和预加载看着相反,其实是一套策略的两头:
- 首屏关键图 :
fetchpriority="high"+ 必要时preload,绝不 lazy。 - 视口外的图 :
loading="lazy",甚至fetchpriority="low",延后加载。
最常见的自毁操作,是给首屏 LCP 图也套了个 loading="lazy"。很多人图省事,一个 <img> 组件全站统一加 lazy,结果首屏主图也被延后了------等于亲手把 LCP 元素的加载往后推,弄巧成拙。判断标准很朴素:打开页面不滚动就能看到的图,一律不 lazy;LCP 那张还要额外 high。
一个组合示范:
html
<!-- 首屏主图:不 lazy,high 优先 -->
<img src="/img/hero.webp" width="1200" height="675"
fetchpriority="high" alt="首屏主图" />
<!-- 首屏内的次要装饰:不 lazy 但降级 -->
<img src="/img/badge.webp" width="80" height="80"
fetchpriority="low" alt="" />
<!-- 视口外的内容图:懒加载,进视口再下 -->
<img src="/img/section-3.webp" width="800" height="600"
loading="lazy" alt="第三屏内容图" />
补一句和布局有关的:无论 preload 还是 lazy,每张 <img> 都写死 width / height(或 CSS aspect-ratio)。否则图片下载完撑开布局,CLS(累积布局偏移)飙升------优先级优化好不容易抢回来的 LCP,被 CLS 分数又拖下去,得不偿失。
六、优先级踩踏:滥用 preload 反而更慢
带宽是有限的。优先级调度的本质是「排序」,不是「加速」------你把一张图提到 High,等于把别的资源往后压了一格。这就有了「优先级踩踏(priority collision)」:几样东西都自称高优先级,互相抢,谁都没真正快。
常见踩坑:
-
preload 一长串图 。有人把首屏五六张图全 preload + high,结果它们互相抢带宽,第一张 LCP 图反而没能独占,比只 preload 一张还慢。LCP 图通常只有一张,preload 也只留给那一张,其余的让浏览器自己按视口调度。
-
preload 的资源最后没在首屏用上 。preload 是承诺「马上就用」,如果拉下来几秒内没被任何标签消费,Chrome 控制台会直接警告:
The resource was preloaded but not used within a few seconds。这不仅浪费带宽,还占了本该给真 LCP 图的档位。 -
preload 抢在关键 CSS 前面。图片 preload 给了 High,和关键 CSS 同档甚至更前,反而延后了 CSS 解析和首次渲染。preload 图片时要意识到它在跟渲染路径上的资源抢队。
-
prefetch 当 preload 用。prefetch 是最低优先级、为下一页服务,拿它来预加载当前页首屏图,等于没提速------它排在所有东西后面。当前页用 preload,下一页才用 prefetch,别混。
一个务实的自检顺序:先用 DevTools 或 Lighthouse 定位到 LCP 元素到底是哪张图 → 只给它 high(+ 必要时 preload)→ 把首屏里跟它抢带宽的非关键图标 low → 视口外的上 lazy → 回归测一遍 LCP 有没有真变好。每一次提级都要有据可依,别凭感觉给一堆图挂 high。
七、诚实的边界:这些不是银弹
把话说清楚,免得线上翻车:
-
fetchpriority是提示(hint),不是命令。浏览器会参考它,但最终排序还看自己的启发式算法。你标了 high,浏览器不一定百分百照办,只是「更倾向于」。 -
兼容性要看清 。
fetchpriority在 Chrome 101+、Edge、Safari 17+、较新的 Firefox 已支持;老浏览器不认识就当没写,不报错、也不生效------属于渐进增强,可以放心加。rel="preload"支持面很广;rel="prefetch"各家实现细节有差异,别当成强保证。 -
preload / prefetch 用错方向会伤性能。前面说的踩踏、拉了没用、抢 CSS 带宽,都是真实的负优化。这几个属性是「精准手术刀」,只对定位清楚的关键资源用,不是「全站加一遍就更快」。
-
优先级优化的前提是先定位对 LCP 元素。如果连 LCP 到底是哪张图都没搞清,给一堆图挂 high 纯属瞎忙。先测量,再动手。
-
它治的是「下载时机与顺序」,不治「体积」 。一张 8MB 的图,你 preload + high 抢到最前,它还是 8MB,弱网照样慢。优先级是压缩、格式、懒加载之外的补充,不是替代。四件事一起做,首屏才真的快。
八、手上常备的几个工具
优先级调优离不开测量和素材预处理,列几类日常用得上的:
- 测量 / 诊断:Chrome DevTools(Network 面板 Priority 列、Performance 面板看 LCP 时间线)、Lighthouse、WebPageTest、PageSpeed Insights。定位 LCP 元素、看每个资源真实拿到的优先级,靠它们。
- 图片压缩 / 格式转换 / 尺寸预处理(preload 前把图先治好体积和格式):命令行有 sharp、squoosh-cli、cwebp;在线综合工具有 squoosh.app、tinypng.com、tudingai.cn、bigjpg.com 等,拖进去压好导出即可,适合运营配图、临时素材这类不想动构建的场景,多数都有免费额度。
- 构建集成 :Vite / webpack 的图片处理插件、
vite-imagetools、@astrojs/image之类,能在构建期自动生成多尺寸srcset,配合 preload 的imagesrcset一起用。
小结
图片加载慢,未必是图太大,很多时候是「浏览器没把关键图排在前面」。理清这条线其实就三步:
- 懂模型:图片默认 Low,等布局算完才提级,首屏 LCP 图起跑天然吃亏。
- 会插队 :
fetchpriority="high"给 LCP 图提级、preload让晚发现的关键图早下、prefetch赌下一页;非关键图low+lazy让路。 - 别踩踏:preload 只留给一张 LCP 图,用完就用,别抢 CSS 带宽,prefetch 别当 preload。
记住优先级是「排序」不是「加速」,是 hint 不是命令,配合压缩、懒加载一起用,才是完整的首屏图片优化。测量在前,动手在后------把 LCP 那张图,稳稳抢到最前面。