图片是前端开发中最常见的资源类型,也是影响用户体验和页面性能的关键因素。一个密集的图集页面可能让加载时间翻倍、滚动卡顿、流量浪费。以下三个技术分别解决三个核心问题:何时加载(懒加载)、何时提前准备(预加载)、如何优雅排布(瀑布流)。
31.3.1 图片懒加载(Lazy Loading)
解决了什么问题
一个图文混排的长页面可能包含上百张图片,如果一口气全部加载,会阻塞关键资源请求、消耗大量带宽、延长首屏时间。懒加载的核心思想是:只加载用户当前视口内或即将进入视口的图片,其余推迟加载。
传统实现:监听滚动事件
通过对比元素位置与视口高度,判断图片是否“露头”。
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
img.src = img.dataset.src; // 真正加载
img.removeAttribute('data-src'); // 避免重复加载
}
});
}
window.addEventListener('scroll', lazyLoad);
注意事项:
- 监听滚动事件必须加防抖或节流,否则每滚一像素都触发计算,性能堪忧。
getBoundingClientRect()会触发回流,对大量图片同时计算仍可能卡顿。
现代方案:Intersection Observer API
浏览器原生提供的交叉观察器,异步监听目标元素与祖先或视口的交叉状态,性能远优于滚动事件。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img); // 加载后取消观察
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
优势:
- 异步执行,不阻塞主线程。
- 不需要手动计算位置,无需防抖。
- 支持设定阈值(
threshold)提前加载。
更进一步:loading="lazy" 属性
从 Chrome 76 开始,HTML 原生支持图片懒加载:
<img src="photo.jpg" loading="lazy" alt="描述" />
不需要写任何 JavaScript,浏览器自动判断图片是否接近视口。但行为细节由浏览器决定,开发者无法精确控制提前量,且旧浏览器可能不支持。实际工程中通常用在不需要精细控制的首屏图片上。
31.3.2 图片预加载(Preloading)
解决了什么问题
有些图片不是用户当前想看的,但马上就会要用到。例如轮播图的下一张、hover 时的悬停大图、页面切换后需要即刻展示的背景图。如果等到需要时才去请求,用户会看到明显的白块或延迟。预加载就是在空闲时间提前把这些图片下载到浏览器缓存,用到时直接从缓存取,实现“即点即出”。
实现方式一:JavaScript 动态创建 Image 对象
function preloadImages(urls) {
urls.forEach(url => {
const img = new Image();
img.src = url; // 开始加载到缓存
});
}
// 使用时机:页面 load 后或核心内容渲染完成后
window.addEventListener('load', () => {
preloadImages([
'banner-next.jpg',
'avatar-hover.png',
'modal-bg.jpg'
]);
});
这种方式不插入 DOM,只是利用 Image 对象发起 HTTP 请求,不影响当前页面布局。
实现方式二:<link rel="preload">
在 HTML 的 <head> 中使用预加载提示,告诉浏览器这个资源会立即需要,优先级更高。
<link rel="preload" as="image" href="hero-bg.jpg" />
可以在 JavaScript 中动态添加:
function addPreloadLink(href) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = href;
document.head.appendChild(link);
}
适用场景与注意事项
- 预加载适量关键图片,不要一次性预加载几十张大图,否则会与当前页面资源抢夺带宽,得不偿失。
- 结合用户行为预测:如鼠标悬停到缩略图时,提前预加载对应的大图。
- 利用 Webpack 的魔法注释:在单页应用中,路由切换到新页面时需要的封面图可以配合代码分割的
/ webpackPrefetch: true /进行预加载。
31.3.3 瀑布流布局(Masonry Layout)
场景描述
多个图片卡片高度不一(例如用户上传的图片、商品列表),需要以一种紧密拼贴的方式排列:每一列从上到下依次堆放卡片,新卡片追加到当前高度最小的那一列。这就是瀑布流布局,Pinterest、花瓣网是其典型代表。
核心算法
无论用 JS 还是 CSS 实现,逻辑都一样:
- 确定列数(比如根据屏幕宽度每列宽 300px 算出 4 列)。
- 维护一个长度为列数的数组,存储每列的当前累计高度。
- 每次添加一个新卡片时,找到数组中高度最小的那一列,将卡片定位到该列的顶部(top = 该列当前高度),然后将该列高度更新为加上了卡片高度后的新值。
- 为卡片设置
position: absolute,用 JavaScript 动态设置left和top。
纯 JavaScript 实现示例
function waterfallLayout(container, columns = 4) {
const items = Array.from(container.children);
const columnHeights = new Array(columns).fill(0);
const itemWidth = container.offsetWidth / columns;
items.forEach(item => {
item.style.width = `${itemWidth}px`;
// 找到最短列
const minIndex = columnHeights.indexOf(Math.min(...columnHeights));
item.style.position = 'absolute';
item.style.left = `${minIndex * itemWidth}px`;
item.style.top = `${columnHeights[minIndex]}px`;
// 更新列高度
columnHeights[minIndex] += item.offsetHeight;
});
// 容器高度设为最高列的高度,防止重叠
container.style.height = `${Math.max(...columnHeights)}px`;
}
注意:如果图片还没有加载完成就执行布局函数,offsetHeight 会是 0,导致卡片全部堆叠在一起。解决方案:等待所有图片加载完毕后再调用布局函数,或者监听每一张图片的 load 事件,动态触发一次重新布局。
图片加载完毕后再布局
function waitForImages(container) {
const images = container.querySelectorAll('img');
const promises = Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise(resolve => { img.onload = resolve; });
});
return Promise.all(promises);
}
// 使用
waitForImages(document.getElementById('waterfall'))
.then(() => waterfallLayout(container, 4));
CSS 方案:column 布局
简单场景可以直接用 CSS 多列布局,但会有内容流被打断的问题(左列读完才到右列)。
.waterfall {
column-count: 4;
column-gap: 10px;
}
.item {
break-inside: avoid; /* 防止卡片内部断列 */
}
优点是不需要 JavaScript 计算,但无法做到“逐列追加并始终填充最短列”的瀑布流效果,因为 CSS 多列是逐列填充整体高度。
实际工程优化
- Resize 监听:窗口大小改变时列数可能变化,需要重新计算布局。使用防抖 + 重新执行布局函数。
- 虚拟列表结合:当图片数量达到几千张时,DOM 数量本身就成瓶颈,反而需要配合虚拟滚动(只渲染可视区域的卡片),瀑布流只是视觉排列,核心还是控制 DOM 量。
- 懒加载 + 瀑布流:先设置占位高度(可以通过预估图片宽高比提前留出空间),图片懒加载进入视口后再显示,避免加载时的布局抖动。
31.3.4 三者结合:图片密集型页面的最佳实践
一个完整的图片墙页面(如摄影社区、商品橱窗)通常同时使用这三种技术:
- 首屏用
loading="lazy"或 Intersection Observer 懒加载,避免非首屏图片拖慢加载速度。 - 用户悬停在某张缩略图时,预加载对应大图,保证后续点击查看时瞬间展现。
- 整个容器使用瀑布流布局,并监听图片加载以及窗口尺寸变化,动态重新排列卡片。
这些技术的共同目标,是用最小的资源消耗,带给用户最流畅、最即时的视觉体验。掌握它们的原理和组合方式,是前端处理图片类需求的基础能力。