一个页面给"下一屏的图片"加了 <link rel="preload">,期望"滚动到时已经加载好"。上线后首屏 LCP 反而变差——因为预加载与首屏关键资源抢带宽,这个"有限几档"很关键:同优先级内按"发现顺序"排队,于是"预加载"改变的是"发现顺序"——它能让你想加载的东西更早,也能让关键资源更晚。
而浏览器的优先级只有"有限几档"。
预加载不是"提前加载就好",而是优先级竞争:带宽有限,你提前加载的东西,正在挤占"首屏真正需要"的那部分。
1. 三种"预加载"与它们的代价
| 手段 | 代价 |
|---|---|
rel=preload |
立即开始下载,抢带宽 |
rel=prefetch |
空闲时下载,低优先级 |
rel=preconnect |
只建连(DNS/TCP/TLS),不下载 |
第一项最激进:它在 HTML 解析时就开始下载,与 CSS/JS 竞争第一项的"优先级"其实可以由 fetchpriority 调整:high 让它更早,low 让它退后——于是"preload + low"可以表达"提前但让路",而多数人只用了默认值。
——于是"你以为在加速,实际在插队"。
第二项的"不确定性"在于"空闲"的定义:有些浏览器把它放在"load 之后",有些放在"主线程空闲"——于是"我 prefetch 了但没生效"可能只是"一直没空闲"。
第二项是"空闲时":浏览器在空闲时才下载,因此它不会挤首屏——但"空闲"可能一直不出现。
第三项最安全:只提前建连(省 1–2 RTT),不占带宽,适合"确定要用但还不到时候"的第三方域名。
2. 坑一:与"关键资源"抢带宽
| 情形 | 结果 |
|---|---|
| 预加载大图(>200KB) | 首屏字体/CSS 变慢 |
| 预加载多个资源 | 带宽被切碎 |
| 预加载低优先级资源 | 关键资源排队 |
第一项在"图片大"时最明显:图片预加载会与"字体文件"竞争第一项的"量化"方式是"看瀑布图里"关键资源的开始时间"":如果加了 preload 之后"字体开始时间"推后了,就是抢带宽的证据。
,而字体阻塞文字渲染——于是 LCP 变差。
修法是"只预加载"首屏需要但发现太晚"的资源":关键 CSS、首屏字体、LCP 图片——而不是"下一屏的东西"。
3. 坑二:as 属性写错等于没预加载
| 写法 | 结果 |
|---|---|
<link rel=preload href=x.js> |
类型不匹配,可能被丢弃 |
<link rel=preload as=script href=x.js> |
正确 |
<link rel=preload as=font href=x.woff2 crossorigin> |
必须 crossorigin |
第一项是"常见错误":缺 as 或写错,浏览器会"下载了但不用",于是"又一次请求"(浪费一次)。
第三项的"坑"在"隐式模式":同域字体可以不带 CORS,但很多字体托管在 CDN(跨域)——于是"本地好好的、线上白下"。
第三项的 crossorigin 是硬要求:字体请求必须是 CORS 模式,否则"预加载的那份"与"实际用的那份"不匹配,等于白下。
<!-- 三种正确的预加载 -->
<link rel="preload" as="font" type="font/woff2" href="/f.woff2" crossorigin>
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<link rel="preload" as="style" href="/critical.css">
<!-- 反例:缺 as / 字体缺 crossorigin → 下载了但复用不到 -->
"第三方域名"的处理最容易被忽略:你早知道会用某个域名,但不确定用哪个资源——此时 preconnect 是唯一正解。
<!-- 三种"对第三方"的准备方式 -->
<link rel="dns-prefetch" href="//cdn.example.com"> <!-- 只解析 DNS -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin> <!-- 建连(省 1–2 RTT) -->
<link rel="preload" as="image" href="https://cdn.../a.webp"> <!-- 直接下资源(占带宽) -->
<!-- 建议:首屏后才用到的第三方 → preconnect;首屏就要 → preload -->
4. 坑三:预加载了"用不到的东西"
| 情形 | 后果 |
|---|---|
| 移动端也预加载桌面大图 | 浪费流量与带宽 |
| 预加载"可能点击"的页面 | 大概率不点 |
| 预加载后又改了 URL | 双份下载 |
第一项要用"媒体查询"限制:**media="(min-width: 1024px)"**第一项的"收益"很直接:移动端少下几百 KB,而它只需要一个属性——所以"预加载清单"里应当给每个资源标"哪个断点需要"。
,于是移动端不下载。
第三项的"防法"是"预加载与引用共用同一个 URL 常量"(构建时注入)——而"手写两处"几乎必然会不一致。
第三项最坑:预加载的是 /a.js,而实际引用的是 /a.js?v=2——两次下载,且"预加载的那份"白费。
"预加载错配"可以用一段两行的检查脚本在 CI 里发现,而不必等控制台警告:
// 检查:HTML 里的 preload href 是否都被实际引用
grep -oE 'rel="preload"[^>]*href="[^"]+"' index.html | grep -oE 'href="[^"]+"' | cut -d'"' -f2 > preloaded.txt
grep -oE '(src|href)="[^"]+\.(js|css|woff2|webp)"' index.html | grep -oE '"[^"]+"' | tr -d '"' | sort -u > used.txt
comm -23 <(sort preloaded.txt) used.txt # 预加载了但没引用 → 候选删掉
5. 正确的预加载清单
| 资源 | 是否预加载 |
|---|---|
| LCP 图片 | 是(fetchpriority=high) |
| 首屏字体 | 是(带 crossorigin) |
| 关键 CSS | 是(若被 JS 引入而发现晚) |
| 下一屏图片 | 否(用 loading=lazy) |
| 可能点击的页面 | 否(用 prefetch) |
| 第三方域名 | preconnect |
这张表的逻辑是"只提前加载"首屏确实需要、但浏览器发现太晚"的东西这个"发现太晚"的判据可以量化:在瀑布图里,这个资源的"开始时间"是否明显晚于"它被需要的时间"。晚,才值得 preload。
——其余的交给别人(lazy、prefetch、preconnect)。
"优先级"要显式写,因为它决定了"谁先开始":
<!-- 三个显式优先级:关键图片抢前,次要资源让路 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<link rel="preload" as="image" href="/below-1.webp" fetchpriority="low">
<img src="/hero.webp" fetchpriority="high" alt=""> <!-- 与 preload 同一个 URL -->
<!-- 关键:preload 与 img 必须同 URL,否则两份下载 -->
6. 排错:五种"预加载没效果/更差"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| LCP 变差 | 抢了关键资源带宽 | 减少预加载 |
| 控制台"未使用预加载" | as/crossorigin 不匹配 |
修正属性 |
| 双份下载 | URL 不一致(带版本号) | 对齐 URL |
| 移动端流量涨 | 未加 media 限制 | 加媒体查询 |
| 预加载了但仍慢 | 服务器/网络是瓶颈 | 见"带宽与延迟" |
第五项(预加载了但仍慢)的"分流"很重要:如果"服务器响应慢"或"网络差",预加载只会让"排队更早"——它不解决"总量"问题。
第二项最直接:浏览器会在控制台提示"资源被预加载但未在几秒内使用"——看到这条就说明"预加载写错了或时机不对"。
最后一个"验收"方式:对比"加预加载前后"的 LCP 与"关键资源开始时间"。而"只看 LCP"不够——因为 LCP 可能"没变差但也没变好",而瀑布图能看出"资源顺序变了"。
// 三个验收指标(同设备、同网络条件)
LCP 应下降或不变(变差 → 抢带宽)
关键字体开始时间 应提前(不变 → 预加载没生效)
预加载未使用警告数 应为 0(>0 → as/crossorigin/URL 错配)
7. 边界:什么情况不必预加载
| 场景 | 为什么 | 做法 |
|---|---|---|
| 首屏资源已足够早 | 无"发现晚"问题 | 不预加载 |
| 页面很小(< 10 个请求) | 竞争不激烈 | 不必要 |
| 资源在 HTML 里直接引用 | 已被发现 | 不必要 |
| 移动端弱网为主 | 预加载浪费配额 | 谨慎 |
判断口径一句话:这个资源"是不是首屏需要、而且浏览器发现得太晚"。最后一条与"演进"有关的边界:预加载是"补丁",不是"架构"。它修的是"发现太晚",而"关键资源太多"应当靠"减少资源"解决——两者的方向不同。
不是(下一屏、可选),就不要 preload;是(LCP 图、字体、被 JS 引入的关键 CSS),才值得。
8. 自测
- 三种预加载手段的代价分别是什么?为什么
preload会"插队"? - 为什么"预加载大图"会让 LCP 变差?它抢了谁的带宽?
as写错会怎样?字体为什么必须加crossorigin?- "双份下载"是怎么发生的?怎么避免?
- "预加载清单"的判据是什么?为什么"下一屏图片"不该 preload?
- 判断"是否需要预加载"的那个问题是什么?