该用 <img> 还是 new Image()?前端图片加载的决策指南 😌😌😌

最近在出一个前端的体系课程,里面的内容非常详细,如果你感兴趣,可以加我 v 进行联系 yunmz777:

浪费你几秒钟时间,内容正式开始

要想知道什么时候改用这些,我们首先要这段浏览器到底是怎么拉图的。

浏览器加载图片的原理

<img>

元素 <img> 插入 DOM 后,浏览器会立即根据关键渲染路径发起请求,并可能结合预解析或预连接等优化策略。它会依据是否在视口中、是否启用懒加载、优先级提示以及 srcset/sizes 的配置,自动选择合适的资源分辨率与下载优先级。

同时,<img> 还具备语义和可访问性支持,alt 属性能为视觉障碍用户提供说明,并能被辅助技术读取。这使得 <img> 在性能和可访问性上都具有默认优化。

可以使用 alt 提供代替文本,便于无障碍支持:

html 复制代码
<img src="cat.jpg" alt="一只正在睡觉的猫" />

可以结合 srcsetsizes,让浏览器在不同的屏幕和视口条件下自动选择最合适的图片资源,从而兼顾清晰度与性能:

html 复制代码
<img
  src="cat-800.jpg"
  srcset="cat-400.jpg 400w, cat-800.jpg 800w, cat-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 600px"
  alt="一只正在睡觉的猫"
/>

可以通过 loading="lazy" 属性,让非首屏图片在滚动到视口时再加载,从而减少初始请求数、优化页面性能:

html 复制代码
<img src="cat.jpg" alt="一只正在睡觉的猫" loading="lazy" />

可以通过 decoding="async" 让图片解码在空闲时异步进行,避免阻塞渲染、提升页面交互流畅度:

html 复制代码
<img src="cat.jpg" alt="一只正在睡觉的猫" decoding="async" />

对于首屏关键图,使用 fetchpriority="high"(可配合 loading="eager")提示浏览器尽快抓取,降低 LCP:

html 复制代码
<img
  src="hero.jpg"
  alt="首页横幅"
  width="1200"
  height="600"
  loading="eager"
  decoding="async"
  fetchpriority="high"
/>

对于非首屏次要图,使用 fetchpriority="low" + loading="lazy" 减少首屏带宽竞争与请求压力:

html 复制代码
<img
  src="gallery-1.jpg"
  alt="相册缩略图"
  width="400"
  height="300"
  loading="lazy"
  decoding="async"
  fetchpriority="low"
/>

为消除布局偏移(CLS),显式声明固有尺寸 width/height 或使用 style="aspect-ratio" 来预留占位:

html 复制代码
<img
  src="cat.jpg"
  alt="一只正在睡觉的猫"
  width="800"
  height="600"
  decoding="async"
  loading="lazy"
/>

<!-- 或者 -->
<img
  src="cat.jpg"
  alt="一只正在睡觉的猫"
  style="aspect-ratio: 4 / 3; width: 100%; height: auto;"
  decoding="async"
  loading="lazy"
/>

<picture> 提供现代格式回退,并在各 source 与回退的 <img> 上同时配置响应式 srcset/sizes,让浏览器按视口选择最佳资源:

html 复制代码
<picture>
  <source
    type="image/avif"
    srcset="cat-400.avif 400w, cat-800.avif 800w, cat-1600.avif 1600w"
    sizes="(max-width: 600px) 100vw, 600px"
  />
  <source
    type="image/webp"
    srcset="cat-400.webp 400w, cat-800.webp 800w, cat-1600.webp 1600w"
    sizes="(max-width: 600px) 100vw, 600px"
  />
  <img
    src="cat-800.jpg"
    srcset="cat-400.jpg 400w, cat-800.jpg 800w, cat-1600.jpg 1600w"
    sizes="(max-width: 600px) 100vw, 600px"
    width="800"
    height="600"
    alt="一只正在睡觉的猫"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
  />
</picture>

在需要延迟解码但又希望尽快显示占位时,可以结合原生懒加载与 CSS 背景占位(或渐进式占位图 LQIP):

html 复制代码
<img
  src="cat-800.jpg"
  alt="一只正在睡觉的猫"
  width="800"
  height="600"
  loading="lazy"
  decoding="async"
  style="background: #f2f3f5;"
/>

若图片用于装饰且不承载语义,请将 alt 设为空字符串,避免冗余信息打扰读屏器;若图片可点击,应为可操作元素提供可聚焦与可见文本替代:

html 复制代码
<!-- 装饰性图片:alt 为空,读屏器跳过 -->
<img src="decor-line.png" alt="" role="presentation" />

<!-- 可点击图片:给链接提供明确文本 -->
<a href="/cats">
  <img src="cat-card.jpg" alt="查看全部猫咪照片" width="600" height="400" />
</a>

浏览器在遇到 <img> 时会立即根据关键渲染路径调度资源请求,并结合 srcset/sizesloadingdecodingfetchpriority 等属性动态决定下载时机、分辨率与优先级,从而在保证性能的同时兼顾可访问性。

new Image()

通过 new Image() 创建的是一个独立的 HTMLImageElement,它默认并不会触发网络请求,只有在设置了 srcsrcset 时才会开始加载。由于此时该元素并未插入 DOM,浏览器通常会将其判定为较低的网络优先级,从而避免和关键渲染路径上的资源争抢带宽。

开发者可以利用这一特性,将图片在后台预加载,并在 onload 回调或 decode() 解析完成后再插入 DOM,这样可以减少首次渲染时的布局抖动和解码阻塞。同时,new Image() 加载的资源仍会进入缓存池,后续 <img> 或 CSS 使用同一资源时能直接复用,提升整体性能。

示例:

html 复制代码
<script>
  // 创建并预加载图片
  const img = new Image();
  img.src = "cat-800.jpg";

  // 等待加载完成再插入 DOM,避免解码阻塞
  img.onload = () => {
    document.body.appendChild(img);
  };

  // 或使用 decode(),确保解码完成再插入
  const preload = new Image();
  preload.src = "hero.jpg";
  preload.decode().then(() => {
    document.body.appendChild(preload);
  });
</script>

new Image() 在设置 srcsrcset 时才触发请求,因未挂载到 DOM 通常被赋予较低网络优先级,适合用于预加载;开发者可在 onloaddecode() 完成后再插入 DOM,以减少渲染与解码抖动,并复用缓存。

什么时候用 new Image() 更合适?

首先第一个就是我们需要精确控制何时加载的时候,我们可以将图片的下载放到首屏稳定之后或空闲时间,让关键资源先走:

js 复制代码
const onIdle = (fn) =>
  window.requestIdleCallback ? requestIdleCallback(fn) : setTimeout(fn, 0);

onIdle(() => {
  const img = new Image();
  img.src = "/next-section/banner.avif";
});

这种使用下一屏、下一步交互、轮播图的下一章、路由借还即即将展示的图。

第二个就是先加载并完成解码,再插入 DOM:

js 复制代码
function preload(src, { crossOrigin, srcset, sizes } = {}) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    if (crossOrigin) img.crossOrigin = crossOrigin; // canvas 用图需先设
    if (srcset) img.srcset = srcset;
    if (sizes) img.sizes = sizes;
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

async function mountDecoded(container, { src, width, height, ...rest }) {
  // 提前占位,避免 CLS
  if (width && height) container.style.aspectRatio = `${width} / ${height}`;
  const img = await preload(src, rest);
  // 等待位图解码,插入更丝滑
  if (img.decode) await img.decode();
  container.replaceChildren(img);
}

它的要点是写死 width/heightaspect-ratio,先占出布局位。再通过 await img.decode() 能显著降低插入瞬间主线程卡顿

第三个就是缓存预热,后续几乎能实现秒显,具体操作就是把相同 URL 再赋给真正的 <img>CSS background-image,大概率命中内存/磁盘缓存,甚至复用已解码位图:

js 复制代码
const imageWarmCache = new Map();

async function warm(src, options) {
  if (!imageWarmCache.has(src)) {
    imageWarmCache.set(
      src,
      preload(src, options).catch(() => null)
    );
  }
  return imageWarmCache.get(src);
}

async function show(selector, src) {
  await warm(src);
  const el = document.querySelector(selector);
  if (el) el.src = src; // 命中缓存 → 快速呈现
}

适用于列表到详情、缩略图到大图,以及分页/轮播的"下一张"等场景:提前预加载可在用户触发后几乎瞬时呈现内容,显著减少等待与卡顿。

如果想要处理更细致的错误处理与降级,那这个是必选项了,因为它能提前拿到 error 并做兜底,避免"破图"闪烁:

js 复制代码
async function safeInsert(container, primary, fallback) {
  try {
    const img = await preload(primary);
    if (img.decode) await img.decode();
    container.replaceChildren(img);
  } catch {
    const alt = document.createElement("img");
    alt.src = fallback;
    alt.alt = "fallback";
    container.replaceChildren(alt);
  }
}

什么时候优先用 <img>(甚至只用 <img>)?

当图片是首屏关键内容(LCP)、需要语义/可访问性或不希望依赖 JS 参与渲染时,应优先直接在 DOM 里写 <img>。这样浏览器能最早、最高优先级地调度下载与解码,你也能用语义属性(如 alt)服务 SEO 与无障碍。

可以使用 alt 提供替代文本,便于无障碍支持;width/height(或 aspect-ratio)用于占位防 CLS:

html 复制代码
<img src="/cats/cat.jpg" alt="一只正在睡觉的猫" width="1200" height="800" />

<!-- 或用 CSS 占位(不如写 width/height 稳妥) -->
<style>
  .hero {
    aspect-ratio: 3 / 2;
  }
</style>
<img class="hero" src="/cats/cat.jpg" alt="一只正在睡觉的猫" />

结合 srcsetsizes,让浏览器在不同屏幕和视口条件下自动选择最合适的图片资源,获得清晰度与性能的双赢:

html 复制代码
<img
  src="/cats/cat-800.jpg"
  srcset="
    /cats/cat-400.jpg   400w,
    /cats/cat-800.jpg   800w,
    /cats/cat-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, 600px"
  alt="一只正在睡觉的猫"
  width="800"
  height="533"
/>

对非首屏图片,直接使用原生懒加载 loading="lazy",让图片在接近视口时再下载,减少首屏请求、优化总体性能:

html 复制代码
<img
  src="/gallery/thumb-1.jpg"
  alt="画廊缩略图 1"
  loading="lazy"
  width="400"
  height="300"
/>

将解码放到空闲时异步进行,避免解码阻塞渲染;对大量非关键图片特别有用(LCP 图建议保持默认 auto,让浏览器自行权衡):

html 复制代码
<img
  src="/gallery/photo-1.jpg"
  alt="旅行照片 1"
  decoding="async"
  loading="lazy"
  width="1200"
  height="800"
/>

对于首屏关键图(LCP),请直接放 DOM,并提升优先级;可选地配合 <link rel="preload"> 进一步提前抓取(注意:使用响应式资源时要带上 imagesrcsetimagesizes,避免重复下载):

html 复制代码
<!-- 1) 预加载(可选但推荐) -->
<link
  rel="preload"
  as="image"
  href="/hero/hero-1600.avif"
  imagesrcset="/hero/hero-800.avif 800w, /hero/hero-1200.avif 1200w, /hero/hero-1600.avif 1600w"
  imagesizes="(max-width: 768px) 92vw, 1200px"
/>

<!-- 2) 关键图本体:高优先级 + 明确尺寸 -->
<img
  src="/hero/hero-1600.avif"
  srcset="
    /hero/hero-800.avif   800w,
    /hero/hero-1200.avif 1200w,
    /hero/hero-1600.avif 1600w
  "
  sizes="(max-width: 768px) 92vw, 1200px"
  alt="首页横幅"
  fetchpriority="high"
  loading="eager"
  width="1200"
  height="675"
/>

若需要格式回退/美术分发(art direction),用 <picture> 在仍然保留 <img> 语义的前提下提供多格式/多裁剪版本(依然建议写宽高占位与高优先级)。这同样适合 LCP 图:

html 复制代码
<picture>
  <!-- 大屏用横裁剪,小屏用竖裁剪,仅示意 -->
  <source
    type="image/avif"
    media="(min-width: 1024px)"
    srcset="/hero/desktop-1200.avif 1200w, /hero/desktop-1600.avif 1600w"
    sizes="(min-width: 1280px) 1200px, 90vw"
  />
  <source
    type="image/avif"
    media="(max-width: 1023px)"
    srcset="/hero/mobile-640.avif 640w, /hero/mobile-960.avif 960w"
    sizes="(max-width: 600px) 92vw, 600px"
  />
  <!-- Fallback 到 JPEG/WebP -->
  <img
    src="/hero/desktop-1200.jpg"
    alt="首页横幅"
    fetchpriority="high"
    loading="eager"
    width="1200"
    height="675"
  />
</picture>

当你需要兼顾 SEO 与可访问性时,还可以用 figure/figcaption 提供更完整的语义包装;注意 alt 是给屏幕阅读器与加载失败的替代文本,figcaption 是对所有用户可见的说明:

html 复制代码
<figure>
  <img
    src="/article/cover-1200.jpg"
    alt="日落下的海岸线"
    width="1200"
    height="800"
  />
  <figcaption>日落下的海岸线(拍摄于青岛石老人)</figcaption>
</figure>

最后,为防止 JS 失败时"白板",关键内容务必用纯 <img> 就能渲染;如果你的页面有 JS 主导的图片渲染路径,建议加一个 noscript 兜底:

html 复制代码
<noscript>
  <img
    src="/hero/hero-1200.jpg"
    alt="首页横幅(JS 关闭时的兜底)"
    width="1200"
    height="675"
  />
</noscript>

首屏/LCP 与需要语义的内容图像,优先直接写 <img>,并通过 width/height(或 aspect-ratio)预留空间避免 CLS,必要时加 fetchpriority="high" 与(可选)<link rel="preload">

非首屏图片用 loading="lazy"decoding="async" 降低主线程压力。响应式场景务必提供 srcset/sizes(或 <picture>)以减少过大资源浪费。无论是否配合 JS 优化,只要是关键渲染路径,都不要把显示逻辑完全寄托在脚本执行之上。

总结

在浏览器的关键渲染路径里,<img> 一旦进入 DOM 就会立刻参与调度并发起下载,天然具备语义与可访问性(如 alt 被读屏器识别),还能配合 srcset/sizesfetchpriority 让浏览器按视口与优先级选择最合适的资源;

new Image() 只有在你设置 src/srcset 时才开始请求,且因不在 DOM 通常被赋予更低网络优先级,便于把下载推迟到空闲/交互之后,并可在 await img.decode() 后再插入以避免插入卡顿,同时起到缓存预热的作用。基于此,首屏/LCP 级的关键可见图像请优先使用 <img>,把 new Image() 留给"下一步就会出现"的图片、背景图或需要精细调度的预加载场景(切勿把 LCP 图放到 new Image() 里)。

具体什么时候该用什么,可以参考下表:

场景 推荐 写法要点 避坑提醒
首屏关键图(LCP) <img> 写明 width/heightaspect-ratiofetchpriority="high";必要时 <link rel="preload">;配 srcset/sizes<picture> 不要用 new Image() 预拉 LCP,JS 后才请求会拖慢 LCP
非首屏/数量多 <img> loading="lazy" decoding="async";同样写尺寸占位;提供 srcset/sizes 懒加载也要占位,避免插入时 CLS
需要语义/SEO <img> 正确的 alt;可用 figure/figcaption 不要把语义图像仅放在 JS 里
精确控制时机/顺序 new Image() 空闲/交互后设置 src;插入前 await img.decode();先占位 预拉过多会占带宽、影响当前体验
列表 → 详情 / 缩略图 → 大图 / 轮播"下一张" new Image() 预热缓存实现"几乎秒显";必要时也配置 srcset/sizes 预拉与展示复用同一 URL,否则二次下载
背景图/Canvas new Image() 不能用 <img> 时先拉后用;Canvas 需像素操作 在设 src 之前设置 img.crossOrigin,避免画布污染
JS 可能失败/延迟 <img> 关键渲染路径可直接渲染;可加 noscript 兜底 关键图像不要依赖 JS 才能出现
低网速/带宽敏感 二者结合 关键图走 <img>;次要图懒加载;近未来图用 new Image() 热身并控制数量 管理预拉缓存并按需释放 Image 引用,避免内存上涨
相关推荐
小楓12014 小时前
MySQL數據庫開發教學(四) 後端與數據庫的交互
前端·数据库·后端·mysql
Mike_jia4 小时前
SSM平台:Ansible与Docker融合的运维革命——轻量级服务器智能管理指南
前端
yinuo4 小时前
Uni-App跨端开发实战:编译微信小程序跳转全平台终极指南(01)
前端
小流苏生4 小时前
或许,找对象真的太难了……
前端·后端·程序员
前端小巷子4 小时前
Vue 3 模板编译器
前端·vue.js·面试
江城开朗的豌豆4 小时前
为什么在render里调setState,代码会和你“翻脸”?
前端·javascript·react.js
江城开朗的豌豆4 小时前
子组件改状态,父组件会“炸毛”吗?
前端·javascript·react.js
晓得迷路了4 小时前
栗子前端技术周刊第 96 期 - Rspack v1.5、ESLint v9.34.0、Bun v1.2.1...
前端·javascript·bun
Sapphire~4 小时前
重学JS-004 --- JavaScript算法与数据结构(四)JavaScript 表单验证
前端·javascript·数据结构·算法