概念定义
首屏以下的图片在加载期抢占关键资源的带宽。loading 属性把推迟策略交给浏览器:元素进入引擎选定的视口边距内才抓取,阈值随连接速度与设备级别自适应。
工作机制
取值 eager(默认)、lazy 与 auto(交给浏览器启发式)。属性应配合显式尺寸,预留空间防止布局偏移;fetchpriority 会交互:lazy 图片临近视口后仍可转为紧急。各引擎的边距阈值不同,但契约都是“接近视口即抓取”。
使用场景
首屏以下媒体与嵌入默认 lazy;LCP 候选保持 eager 并显式声明尺寸。原生加载消除了观察器脚本、布局读取,以及自定义加载器在旧浏览器上的失败模式。
常见误区
原生懒加载不需要任何 JavaScript,它是 HTML 属性。把主视觉图标 lazy 会让每页 LCP 倒退;推迟也会让图片晚于预加载扫描器的发现时机,关键图必须 eager。大多数引擎会在打印与页内查找时强制加载,但依赖这一点很脆弱;iframe 内的脚本在抓取后照常执行。
代码示例
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible confirmation dialog</title>
<style>
:root {
color-scheme: light dark;
font-family: system-ui, sans-serif;
}
body {
margin: 0;
padding: 2rem;
}
button {
font: inherit;
padding: 0.6rem 1rem;
border: 1px solid currentColor;
border-radius: 0.4rem;
background: canvas;
color: canvastext;
cursor: pointer;
}
button:focus-visible {
outline: 3px solid #4f46e5;
outline-offset: 2px;
}
dialog {
max-width: 32rem;
border: 1px solid gray;
border-radius: 0.75rem;
padding: 1.5rem;
}
dialog::backdrop {
background: rgb(0 0 0 / 55%);
}
.actions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<main>
<h1>Delete project</h1>
<p>Deleting a project removes its deployments and environment variables.</p>
<button type="button" id="open-dialog">Delete project…</button>
<p id="status" role="status" aria-live="polite"></p>
</main>
<dialog id="confirm-dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<form method="dialog">
<h2 id="dialog-title">Delete “Atlas”?</h2>
<p id="dialog-desc">
This permanently removes the project and its 12 deployments. This action cannot be undone.
</p>
<div class="actions">
<button value="cancel" autofocus>Cancel</button>
<button value="confirm">Delete permanently</button>
</div>
</form>
</dialog>
<script>
const dialog = document.querySelector('#confirm-dialog');
const openButton = document.querySelector('#open-dialog');
const status = document.querySelector('#status');
openButton.addEventListener('click', () => {
dialog.returnValue = '';
dialog.showModal();
});
dialog.addEventListener('close', () => {
status.textContent = dialog.returnValue === 'confirm'
? 'Project deleted.'
: 'Deletion cancelled.';
openButton.focus();
});
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
dialog.close('cancel');
}
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Signup with an accessible error summary</title>
<style>
body {
margin: 0;
padding: 2rem;
font-family: system-ui, sans-serif;
}
form {
max-width: 28rem;
display: grid;
gap: 1.25rem;
}
.field {
display: grid;
gap: 0.35rem;
}
input {
font: inherit;
padding: 0.5rem;
border: 1px solid gray;
border-radius: 0.35rem;
}
input:focus-visible,
button:focus-visible,
.error-summary:focus-visible {
outline: 3px solid #4f46e5;
outline-offset: 2px;
}
.error-summary {
border: 2px solid #b91c1c;
border-radius: 0.5rem;
padding: 1rem;
}
.error-summary h2 {
margin-top: 0;
font-size: 1.15rem;
}
.error-summary a {
color: inherit;
}
.field-error {
margin: 0;
color: #b91c1c;
}
input[aria-invalid="true"] {
border-color: #b91c1c;
}
fieldset {
border: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
</style>
</head>
<body>
<main>
<h1>Create your account</h1>
<form id="signup" novalidate>
<div class="error-summary" id="error-summary" role="alert" tabindex="-1" hidden>
<h2>There is a problem</h2>
<ul id="error-list"></ul>
</div>
<div class="field">
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required aria-describedby="email-error">
<p class="field-error" id="email-error" hidden></p>
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="new-password" minlength="8" required aria-describedby="password-error">
<p class="field-error" id="password-error" hidden></p>
</div>
<fieldset aria-describedby="plan-error">
<legend>Plan</legend>
<label><input type="radio" name="plan" value="starter" id="plan-starter" required> Starter</label>
<label><input type="radio" name="plan" value="pro" id="plan-pro"> Pro</label>
<p class="field-error" id="plan-error" hidden></p>
</fieldset>
<div class="field">
<label for="terms">
<input id="terms" name="terms" type="checkbox" required aria-describedby="terms-error">
I accept the terms of service
</label>
<p class="field-error" id="terms-error" hidden></p>
</div>
<button type="submit">Create account</button>
<p id="status" role="status" aria-live="polite"></p>
</form>
</main>
<script>
const form = document.querySelector('#signup');
const summary = document.querySelector('#error-summary');
const errorList = document.querySelector('#error-list');
const status = document.querySelector('#status');
const email = document.querySelector('#email');
const password = document.querySelector('#password');
const planInputs = [...document.querySelectorAll('input[name="plan"]')];
const terms = document.querySelector('#terms');
const fields = [
{
input: email,
error: document.querySelector('#email-error'),
validate() {
const value = email.value.trim();
if (!value) return 'Enter your email address.';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Enter an email address in the format [email protected].';
}
return '';
}
},
{
input: password,
error: document.querySelector('#password-error'),
validate() {
if (!password.value) return 'Enter a password.';
if (password.value.length < 8) return 'Use at least 8 characters for your password.';
return '';
}
},
{
inputs: planInputs,
input: planInputs[0],
error: document.querySelector('#plan-error'),
validate() {
return document.querySelector('input[name="plan"]:checked') ? '' : 'Choose a plan.';
}
},
{
input: terms,
error: document.querySelector('#terms-error'),
validate() {
return terms.checked ? '' : 'Accept the terms to continue.';
}
}
];
function targetsFor(field) {
return field.inputs ?? [field.input];
}
function clearError(field) {
field.error.textContent = '';
field.error.hidden = true;
for (const target of targetsFor(field)) {
target.removeAttribute('aria-invalid');
}
if (!fields.some((candidate) => !candidate.error.hidden)) {
summary.hidden = true;
errorList.replaceChildren();
}
}
for (const field of fields) {
for (const target of targetsFor(field)) {
target.addEventListener('input', () => clearError(field));
target.addEventListener('change', () => clearError(field));
}
}
form.addEventListener('submit', (event) => {
event.preventDefault();
errorList.replaceChildren();
const invalid = [];
for (const field of fields) {
const message = field.validate();
field.error.textContent = message;
field.error.hidden = message === '';
for (const target of targetsFor(field)) {
if (message) target.setAttribute('aria-invalid', 'true');
else target.removeAttribute('aria-invalid');
}
if (message) invalid.push({ field, message });
}
if (invalid.length > 0) {
for (const { field, message } of invalid) {
const item = document.createElement('li');
const link = document.createElement('a');
link.href = '#' + targetsFor(field)[0].id;
link.textContent = message;
link.addEventListener('click', (clickEvent) => {
clickEvent.preventDefault();
targetsFor(field)[0].focus();
});
item.append(link);
errorList.append(item);
}
summary.hidden = false;
summary.focus();
status.textContent = '';
return;
}
summary.hidden = true;
status.textContent = 'Account created. Check your inbox to confirm your email.';
form.reset();
});
</script>
</body>
</html>