页面大量图片如何优化加载 前端面试
方法
- 图片懒加载,在页面上的未可视区域可以添加一个滚动事件,判断图片位置与浏览器顶端的距离与页面的距离,如果前者小于后者,优先加载。
- 如果为幻灯片、相册等,可以使用图片预加载技术,将当前展示图片的前一张和后一张优先下载。
- 如果图片为
css图片,可以使用CSSsprite,SVGsprite,Iconfont、Base64等技术。 - 如果图片过大,可以使用特殊编码的图片,加载时会先加载一张压缩的特别厉害的缩略图,以提高用户体验。
- 如果图片展示区域小于图片的真实大小,则因在服务器端根据业务需要先行进行图片压缩,图片压缩后大小与展示一致
面试官想听什么
这道题面试官通常不满足于背清单,他要看你能不能分清「什么阶段的瓶颈用什么手段解」。图片加载慢,可以拆成三个正交的方向:
- 少请求 — 该加载的时候再加载(懒加载)、能合并的合并(雪碧图)
- 小体积 — 换格式、换尺寸、上压缩
- 快通道 — CDN、HTTP/2、预连接、优先级提示
答的时候按这三层组织,比平铺五六条更有说服力。下面把上面每条清单展开成能落地的写法。
懒加载:原生属性优先,别急着写滚动监听
上面提到的「监听滚动事件、比较图片位置」是懒加载最早的实现思路,但今天不该是首选,原因是滚动事件触发极其频繁,不做节流会直接把主线程打满,而 getBoundingClientRect() 每次调用都会强制浏览器重排。
现在浏览器原生就支持,一个属性搞定:
<img src="photo.jpg" loading="lazy" width="800" height="600" alt="示例图" />
loading="lazy" 让浏览器自己决定什么时候下载,时机由浏览器根据网络状况和滚动距离动态调整,比手写的固定阈值更聪明。注意两点:
- 必须写
width和height(或用 CSS 的aspect-ratio占好位)。不写的话图片加载前高度是 0,加载完撑开会把下面的内容顶下去,这就是 CLS(累积布局偏移),Core Web Vitals 里直接扣分。 - 首屏图片不要加
loading="lazy"。懒加载会让浏览器延后发请求,首屏的主图这么一延后,LCP(最大内容绘制)反而变差。首屏主图应该反过来加fetchpriority="high":
<!-- 首屏主图:抢优先级,不要懒加载 -->
<img src="hero.jpg" fetchpriority="high" width="1200" height="600" alt="主图" />
如果需要更精细的控制(比如懒加载的不是 img 而是背景图、或者要做骨架屏切换),用 IntersectionObserver,它由浏览器在合适的时机批量回调,不占用主线程做位置计算:
const io = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // 真实地址存在 data-src 上
observer.unobserve(img); // 加载过就取消观察,避免重复触发
});
},
{
// 提前 200px 开始加载,让用户滚到时图片已经就绪
rootMargin: '200px 0px',
},
);
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
rootMargin 是这里的关键参数:设成 0 意味着图片刚进视口才开始下载,用户会看到一片空白再刷出图。给 200px 左右的提前量,体验上就基本无感了。
小体积:格式和尺寸比压缩率更重要
换格式的收益最大。同样画质,WebP 比 JPEG 小 25%~35%,AVIF 能再小 20%~30%。但老浏览器不认,所以用 <picture> 做渐进降级,浏览器会从上往下挑第一个它支持的:
<picture>
<source srcset="photo.avif" type="image/avif" />
<source srcset="photo.webp" type="image/webp" />
<img src="photo.jpg" width="800" height="600" alt="示例图" />
</picture>
按屏幕尺寸给不同的图,这就是清单里「展示区域小于真实大小要在服务端压缩」的标准实现,用 srcset + sizes:
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
width="800"
height="600"
alt="示例图"
/>
srcset 里的 400w 是图片的真实像素宽度,不是显示宽度;sizes 告诉浏览器这张图在不同视口下会占多宽。浏览器拿这两个信息,再结合设备的 DPR,自己算出该下载哪一张。手机上下载 400w 那张,比下载 1600w 再缩放到 375px 显示,体积能差好几倍。
压缩要挑对工具。JPEG 用 mozjpeg,PNG 用 pngquant,SVG 用 svgo,批量处理接 sharp。质量给到 75~85 之间人眼基本看不出差别,再往下压才开始出现明显的块状伪影。
雪碧图今天还需要吗
清单里提到的 CSSsprite、Iconfont、Base64,本质都是「合并请求」,解决的是 HTTP/1.1 时代每个域名只能并发 6 个连接的问题。HTTP/2 之后同一连接可以多路复用,合并请求的收益大幅缩水,甚至有负作用:雪碧图改一个小图标就得让整张图的缓存失效。
所以现在的取舍是:
- 图标类 — 直接用 SVG,能内联进 HTML(省一个请求)、能用 CSS 改颜色、任意缩放不失真。Iconfont 的字体文件有 FOIT 白屏问题,且语义化和无障碍支持都不如 SVG,新项目不建议再用。
- Base64 — 只适合极小的图(1KB 以内)。Base64 编码会让体积涨约 33%,而且内联进 CSS/JS 后无法单独缓存,改一个图标整个文件的缓存都作废。
- 雪碧图 — 还在维护 HTTP/1.1 老项目、或者图标数量特别多且几乎不变时,仍然有用。
渐进式加载:先给个模糊的顶上
清单里「先加载压缩得特别厉害的缩略图」这个思路,工程上叫 LQIP(Low Quality Image Placeholder)。做法是把原图缩到 20~40px 宽、质量压到很低,编码成一个 1KB 上下的 Base64 内联在 HTML 里做背景,加 CSS 模糊;原图加载完再淡入替换。
<div
class="img-wrap"
style="background-image: url('data:image/jpeg;base64,...')"
>
<img
src="photo.jpg"
loading="lazy"
onload="this.classList.add('loaded')"
alt="示例图"
/>
</div>
.img-wrap {
background-size: cover;
filter: blur(8px); /* 遮住缩略图的马赛克 */
}
.img-wrap img {
opacity: 0;
transition: opacity 0.3s;
}
.img-wrap img.loaded {
opacity: 1;
}
另一条路是用渐进式 JPEG(mozjpeg -progressive),它把图片编码成由粗到细的多次扫描,浏览器边下边渲染,天然就是从模糊到清晰,不用额外写代码。代价是编码后体积略大一点,且解码耗时略高。
预加载与预连接
「幻灯片预加载前后一张」这个场景,用 <link rel="preload"> 声明式地做更稳:
<link rel="preload" as="image" href="slide-2.jpg" />
或者 JS 里造一个 Image 对象触发下载,图片进了 HTTP 缓存,真正显示时就是瞬间的:
function preload(urls) {
urls.forEach(url => {
const img = new Image();
img.src = url;
});
}
// 展示第 n 张时,预取 n-1 和 n+1
preload([list[index - 1], list[index + 1]].filter(Boolean));
如果图片放在独立的 CDN 域名下,还要提前把 TLS 握手做掉,不然第一张图会白等一个完整的 DNS + TCP + TLS 往返:
<link rel="preconnect" href="https://img.example.com" crossorigin />
怎么验证优化有没有效果
说到这一步面试基本就稳了,因为大部分人只答手段不答验证。三个层次:
- Chrome DevTools 的 Network 面板 — 勾上 Disable cache,用 Slow 4G 节流,看图片请求的瀑布图:有没有在首屏就发起大量请求、有没有排队等连接。
- Lighthouse — 直接给出 LCP、CLS 数值,以及「Properly size images」「Serve images in next-gen formats」「Defer offscreen images」这几项具体扣了多少分。
- 真实用户监控(RUM) — 用
web-vitals库上报线上真实的 LCP/CLS 分布。实验室数据和真实数据经常差很远,尤其是国内的网络环境。
最后一个容易被问到的追问:图片优化的收益上限在哪。答案是当图片不再是 LCP 元素、且总体积降到不影响首屏的量级时,继续压缩的边际收益就趋近于零,这时候瓶颈已经转移到 JS 执行或者接口耗时上了。能说出这句,说明你是在做性能优化,而不是在背图片优化。

还没有留言,来发表第一条吧~