预加载在资源的普通消费者发现它之前,先告诉浏览器这个资源可能很快需要使用。由 JavaScript 创建的图片,或必须等样式表加载并解析后才发现的字体,都可能因此更早开始获取。它提供的是提前下载的机会,不能推导成所有提示资源都获得全局最高优先级,更不能保证添加之后每个页面都一定变快。
发现、获取与使用是三个阶段
现代浏览器通常能够提前扫描 HTML 中的资源,因此图片写在后面,不代表必须等主解析器逐行走到那里才开始发现;图片位于首屏之外,也不直接证明发现时间很晚。添加提示之前,应查看真实请求发起者和时间。如果资源原本就及时开始获取,额外提示可能收益很小,却与更重要的工作竞争带宽。
rel=preload 且 as=image 的 link 被处理时,会按图片目的发起获取,但不会自动插入 img 元素。同样,预加载样式表不等于应用样式,预加载脚本也不会自动执行脚本。实际消费者仍需要通过正常机制请求或使用该资源。下载提前了,却没有后续使用,只会增加开销,不能算作页面已经完成渲染。
复用依赖请求信息兼容。地址、as 表达的资源用途,以及相关的 CORS 和凭据模式,需要与最终消费者一致。字体预加载通常即使位于同源,也要配置 crossorigin,使模式匹配实际字体请求。media 适合控制条件加载,却不是每一个字体提示都必填的属性。响应式图片还需要匹配真实选择结果,不能仅凭猜测预加载最大的一张,然后假定浏览器肯定使用它。
完整性元数据又增加一条复用条件。HTML 的消费算法会比较消费者的完整性元数据与预加载条目;不兼容的元数据可能阻止消费,即使两个哈希单独看都能描述该资源。更新资产时应同步审阅完整性配置。本图片实验没有覆盖这些检查,因此不能据此断言带完整性校验的脚本预加载必定具有相同复用结果;地址匹配不是全部条件。
坏掉的样子
设计调整后,旧提示很容易留下来:页面已经移除主图,head 中的 preload 却继续下载。另一个常见问题是消费者使用不同查询参数或请求模式,导致原本期待的复用没有发生。检查时既要数请求,也要核对最终真正使用的地址。图片能够显示,只说明某次加载成功,不能证明提前下载的那份资源被复用了。
预加载过多资源,还会竞争带宽、连接和处理能力。提前发现不是暂停浏览器其他优先级规则的命令。对可选交互资源的提示,也可能让从不点击该控件的用户承担流量成本。应明确这笔取舍,并在真实受众的测试中移除成本高于收益的提示,而不是把所有资产都提前下载当作默认优化。
很快的本地实验不能建立最大内容绘制或真实响应速度已经改善的结论。性能比较需要页面、缓存状态、设备和网络条件具备可比性。资源时间记录能够说明请求何时开始,但不能单独解释整个用户体验变化,更不能把两次非受控访问的耗时差全归因于一个属性。
快速验证
把下面代码保存为 preload-image.py,使用 Python 3 执行 python3 preload-image.py。命令通过 HTTP 检查三个页面变体和一张图片的可达性,并明确输出 browser_preload_tested_by_cli 为假。服务端检查 HTML 字符串,并没有运行浏览器的资源调度过程,不能据此宣称预加载已经发生。
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Event, Lock, Thread
from urllib.parse import urlsplit
import json
import sys
requests = []
lock = Lock()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlsplit(self.path).path
cache = 'no-store'
if path in ('/plain', '/hinted', '/unused'):
mode = path[1:]
asset = '/hero.svg?mode=' + mode
hint = f'<link rel="preload" as="image" href="{asset}">' if mode != 'plain' else ''
if mode == 'unused':
hint += '<link rel="preload" as="image" href="/unused.svg?mode=unused">'
script = '''document.getElementById('reveal').addEventListener('click',()=>{
if(document.getElementById('hero'))return;
const picture=new Image(480,160);picture.id='hero';picture.alt='Requested illustration';
picture.onload=()=>document.getElementById('state').textContent='Image displayed';
picture.src=ASSET;document.getElementById('slot').append(picture);
});'''.replace('ASSET', json.dumps(asset))
data = f'<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Preload {mode}</title><link rel="icon" href="data:,">{hint}<style>body{{font:18px sans-serif;margin:24px;max-width:700px}}button{{padding:12px 18px;font:inherit}}img{{max-width:100%;height:auto}}#slot{{margin-top:24px}}</style></head><body><h1>Preload: {mode}</h1><p>The image is inserted only after the button is pressed.</p><button id="reveal">Show image</button><p id="state">Waiting for input</p><div id="slot"></div><script>{script}</script></body></html>'.encode()
kind = 'text/html; charset=utf-8'
elif path in ('/hero.svg', '/unused.svg'):
with lock:
requests.append(self.path)
label = 'HERO' if path == '/hero.svg' else 'UNUSED'
data = f'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="160"><rect width="480" height="160" fill="#4338ca"/><text x="24" y="92" font-family="sans-serif" font-size="36" fill="white">{label}</text></svg>'.encode()
kind, cache = 'image/svg+xml', 'public, max-age=600'
elif path == '/events':
with lock:
data = json.dumps(requests).encode()
kind = 'application/json'
else:
self.send_error(404)
return
self.send_response(200)
self.send_header('Content-Type', kind)
self.send_header('Cache-Control', cache)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
if '--serve' in sys.argv:
print(json.dumps({'origin': f'http://127.0.0.1:{server.server_port}'}), flush=True)
try:
Event().wait()
except KeyboardInterrupt:
pass
else:
for path in ('/plain', '/hinted', '/unused', '/hero.svg?mode=plain'):
connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request('GET', path)
response = connection.getresponse()
data = response.read()
assert response.status == 200
if path == '/plain':
assert b'rel="preload"' not in data
if path == '/hinted':
assert data.count(b'rel="preload"') == 1
if path == '/unused':
assert data.count(b'rel="preload"') == 2
finally:
connection.close()
print(json.dumps({'three_page_variants_available': True, 'browser_preload_tested_by_cli': False}))
finally:
server.shutdown()
thread.join()
server.server_close()
要进行浏览器实验,另行运行 python3 preload-image.py --serve,在新的浏览器配置里依次打开打印源地址下的 /plain、/hinted 和 /unused。每个页面只有按下 Show image 后才插入主图。不同变体使用不同查询地址,避免无意间共用一次缓存结果。点击前后都检查 /events,按当前 mode 统计请求;保留正常缓存行为,结束后按 Ctrl-C 停止服务。
此前记录的 Chrome 152.0.7977.83 实测结果是:plain 在交互前没有图片请求,点击后有一次主图请求;hinted 在点击之前已经请求主图,显示图片之后累计仍然只有一次。unused 则提前请求主图和 unused 图片,最终页面只消费主图,但两张图片各发生了一次传输。通过服务端计数和实际元素显示相互核对,才能区分“提前下载”“成功复用”和“多下载了无用内容”。
这些结果只建立了本例的提前获取、兼容复用及额外传输事实,没有测出性能收益,也没有覆盖字体处理或不同浏览器的一致性。用于真实页面时,应先确认每个提示对应的消费者,再测量它准备改善的用户结果;只有两者都成立,保留提示才有充分依据。
本轮没有修改此运行实验,也没有重新执行。文中的预期输出和此前记录的观察,与本轮执行证据分别保留,不能据此声称真实集成已经再次通过。
来源
- MDN: rel=preload,资源提示、请求匹配与响应式条件,已核验 2026-10-15。
- HTML Standard: Link type preload,预加载处理及后续消费规则,已核验 2026-10-15。