懒加载把暂时用不到的工作推迟到更合适的时机。对于 HTML 图片,loading="lazy" 允许浏览器按照自己的策略,等图片接近视口时再发起请求。长文章因此可能减少初始传输量,但这个属性只是调度输入,并不保证每一张尚未进入屏幕的图片都绝对不会下载。能否节省请求,需要观察实际浏览器行为。
让浏览器调度普通图片
原生方式直接使用真实的 src 或 srcset,不必把地址隐藏在自定义属性里,也不必仅为基础懒加载编写滚动处理器。加载距离由浏览器决定,还可能随实现及连接条件调整。作者不能通过 loading 属性直接配置原生距离阈值。使用 IntersectionObserver 的应用方案有另一套控制方式,同时也增加了程序需要承担的调度和兼容责任。
首屏必需的图片应保持及时加载,尤其是主要展示图。如果对决定最大可见内容的图片使用延迟加载,真正有用的画面可能更晚出现。反过来,为刻意延迟的图片添加 preload,也可能提前触发下载,抵消预期节省。应根据图片角色和实测结果选择,而不是给页面所有资源统一贴上同一种策略。
明确填写图片原始 width 和 height,可以让浏览器在下载之前预留宽高比例。响应式样式仍然能够缩小实际宽度,并自动保持高度比例。缺少有效尺寸时,图片最初可能几乎不占空间,既影响哪些内容被认为接近视口,也可能在图片到达后把正文突然推开。图片无法显示时,准确的替代文字仍然是阅读体验的一部分,不能只顾请求数量。
坏掉的样子
只等页面 load 事件结束,可能漏掉尚未完成的懒加载图片。浏览器可以在远处图片未下载完时结束页面加载事件,因此应分别检查图片完成状态、自然尺寸、服务端请求及滚动后的真实显示。请求已经失败与请求尚未开始是不同状态;两者最初都可能没有解码后的像素,不能只凭一项尺寸判断根因。
没有编写页面脚本,不等于在浏览器中禁用了脚本。MDN 说明,作为防跟踪措施,图片的这种延迟行为需要浏览器启用脚本。普通静态 HTML 无须额外编写 JavaScript 就可以使用,但把浏览器脚本整体关闭之后,不能继续假定请求时机完全相同。应按站点实际支持的环境验证,不宣称所有配置都能获得一样的节省。
loading 属性只作用于支持它的元素,不能直接控制任意 CSS 背景图。content-visibility 主要用于跳过部分渲染工作,也不承诺相关网络请求全部延后。快速滚动与慢连接组合时,原生调度仍可能来不及让图片及时到达。评估中需要查看空白占位持续多久以及布局是否稳定,不能把初始请求减少当成用户体验必然改善。
快速验证
本轮复核没有重新运行 HTTP 服务或浏览器滚动检查;上述 Chrome 结果属于此前记录的运行。
将下面代码保存为 lazy-images.py,运行 python3 lazy-images.py。Python 3 命令验证本地页面和两张图片都能返回成功响应,但输出中的 native_loading_tested_by_cli 明确为假。普通 HTTP 客户端读取 HTML,并不会执行浏览器图片调度器,因此该命令本身不能证明已经发生原生懒加载。
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Event, Lock, Thread
import json
import sys
requests = []
lock = Lock()
page = '''<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Native image loading</title><link rel="icon" href="data:,"><style>body{font:18px sans-serif;margin:24px;max-width:700px}img{max-width:100%;height:auto}.gap{height:60000px}</style></head><body><h1>Native image loading</h1><p>The first image loads eagerly. Scroll down to reach the reserved lazy image.</p><img id="first" src="/eager.svg" width="480" height="160" alt="Eager example"><div class="gap"></div><h2>Deferred image</h2><img id="late" src="/late.svg" loading="lazy" width="480" height="160" alt="Lazy example"><p>Both image dimensions were reserved before loading.</p></body></html>'''
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
data, kind = page.encode(), 'text/html; charset=utf-8'
elif self.path in ('/eager.svg', '/late.svg'):
with lock:
requests.append(self.path)
color = '#2563eb' if self.path == '/eager.svg' else '#047857'
label = 'EAGER' if self.path == '/eager.svg' else 'LAZY'
data = f'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="160"><rect width="480" height="160" fill="{color}"/><text x="24" y="92" font-family="sans-serif" font-size="36" fill="white">{label}</text></svg>'.encode()
kind = 'image/svg+xml'
elif self.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', 'no-store')
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 ('/', '/eager.svg', '/late.svg'):
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 == '/':
assert b'loading="lazy"' in data and b'height:60000px' in data
finally:
connection.close()
print(json.dumps({'page_and_assets_available': True, 'native_loading_tested_by_cli': False}))
finally:
server.shutdown()
thread.join()
server.server_close()
随后运行 python3 lazy-images.py --serve,在使用独立新配置且启用脚本的浏览器里打开打印的源地址。页面自身没有 JavaScript:标题附近放一张及时加载的图片,第二张懒加载图片则放在刻意拉长的六万像素间隔之后。滚动前访问 /events 检查请求记录,再滚动到 Deferred image 标题附近,重新检查记录与显示。结束时按 Ctrl-C 关闭实验服务。
在 Chrome 152.0.7977.83 中,1440×1000 桌面视口和 390×844 手机视口最初都只请求 eager.svg。远处图片的 naturalWidth 为零,但分别预留了 480×160 和 342×114 的实际空间。滚动之后 late.svg 各请求一次,自然宽度变成 480,两种视口的预留尺寸均未改变,图片正常显示。这些观察同时确认了延迟请求与尺寸保留,而不是仅凭属性存在得出结论。
超长间隔是为了清楚展示调度顺序,不代表真实文章布局,也不是性能基准。本次没有测量禁用脚本的额外情况。用于实际站点时,需要在真实布局、设备和网络条件下重新检查滚动过程,再判断这个属性是否带来了用户可见的改善。
来源
- MDN: Lazy loading,延迟非关键资源及不同加载机制,已核验 2026-09-13。
- MDN: The Image Embed element,图片加载行为、尺寸与脚本启用条件,已核验 2026-09-13。