HTML原生懒加载的工程化实践:我把js-lazy-load插件从项目里删了

2026-08-03 0 996

去年优化一个带长列表的页面,最初的图片懒加载用的是基于 scroll 事件的第三方库。功能没问题,就是总感觉有点重,而且每次新增动态内容都得手动调一下 API,麻烦得要死。后来一跺脚,把代码全部清理干净,换成了 HTML 原生的 loading="lazy" 和 Intersection Observer。这波操作下来,不仅代码砍了几十行,页面滚动也变得更丝滑了。

这篇就把我这个折腾过程完整地记录下来,不是单纯介绍 API,而是面向真实工程,探讨怎么把原生懒加载用到极致,以及你需要注意的细节。

一、为什么我不建议你再用第三方懒加载库

咱们不是否定所有第三方库,但懒加载这事情,浏览器已经帮我们做了大部分。原生的 loading="lazy" 从 2019 年开始就逐步得到支持,到现在主流浏览器基本都兼容。它最大的优点是不需要 JS 去手动计算图片位置,不用监听滚动事件,浏览器自己会在合适的时机加载图片。

第三方库往往要自己绑定 scroll 或 touchmove 事件,然后处理节流、判断元素进入视口、再替换 data-src 为真正的 src。这一套逻辑放在现在显得有些多余,而且性能并不一定比原生好。我之前那个插件,在低端安卓机上滚动时还是会有明显抖动,换了原生之后反而更顺。

当然,loading="lazy" 只对 imgiframe 有效,背景图什么的它管不了。这就是我们还需要 Intersection Observer 的原因。

二、基础篇:从前那样写,现在这样写

以前咱们写图片:

<img src="placeholder.gif" data-src="real-image.jpg" alt="商品图">

然后 JS 里监听滚动,把 data-src 赋给 src。现在只需要:

<img src="real-image.jpg" alt="商品图" loading="lazy">

就这么简单。

但有个小坑:如果你给 img 设置 srcset 来做响应式,那么 loading="lazy" 仍然有效。它不会影响 srcset 的属性,所以你可以放心用。

<img 
    src="small.jpg" 
    srcset="medium.jpg 600w, large.jpg 1200w" 
    sizes="(max-width: 600px) 480px, 800px" 
    alt="响应式图片" 
    loading="lazy">

需要注意的是:loading="lazy" 对已经处于视口内的图片没有任何影响,它只拦截视口之外的图片请求。而且它还有一条默认的“距离阈值”,比如 Chrome 是 1250px。这个阈值无法用 JS 调整,但通常够用。

三、Intersection Observer 才是万能钥匙

如果你要懒加载背景图、视频、广告位,甚至做一些滚动动画,那 Intersection Observer 是比滚动监听更科学的选择。它通过异步观察元素是否进入视口,不需要 JS 计算位置,性能好很多。

一个最典型的例子:懒加载背景图。注意,背景图的 url() 写在 CSS 里,浏览器不会因为元素不可见就不加载。所以我们可以把真实背景放在一个 data-* 属性里,等元素进入可视范围再通过 JS 设置样式。

<div class="hero" data-bg="/images/hero-wide.jpg">
    <h1>欢迎光临</h1>
</div>

然后在 JS 里这样操作:

const hero = document.querySelector('.hero');

function loadBg(element) {
    const bgUrl = element.dataset.bg;
    if (bgUrl) {
        element.style.backgroundImage = `url('${bgUrl}')`;
        element.removeAttribute('data-bg');
    }
}

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            loadBg(entry.target);
            observer.unobserve(entry.target);
        }
    });
}, { rootMargin: '200px' });  // 提前200px开始加载

observer.observe(hero);

这里的 rootMargin 可以让图片在距离视口 200px 的时候就开始加载,体验上几乎是即时的。

四、完整案例:列表页商品图的懒加载 + 从占位图过渡

我们项目里有一个商品列表页,接口返回的数据会渲染成卡片。每张卡片有一张商品图、一个标题、一个优惠标签。为了美观,图片加载前显示一个淡淡的背景色和占位小图标。这个需求用原生的 loading="lazy" 就可以搞定,因为 img 有一个 onload 事件可以判断加载完成。

具体玩法是这样的:

<img 
    src="/images/placholder.svg" 
    data-src="https://cdn.example.com/product/123.jpg" 
    alt="商品名称"
    class="product-img">

注意我用了 data-src 而不是直接的 src。为什么要这样?因为我想让图片真正加载时触发一个淡入效果,而不是浏览器自动加载后直接显示。如果直接用 loading="lazy" 加上 src,虽然能做原生懒加载,但没法控制加载完成的时机。所以我采用 Intersection Observer 配合 data-src,这样既精准又能做样式过渡。

整段 JS 如下:

const imgObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (!entry.isIntersecting) return;

        const img = entry.target;
        const realSrc = img.dataset.src;
        if (realSrc) {
            // 先把占位图保持住,等真实图片加载完再替换
            const tempImage = new Image();
            tempImage.onload = () => {
                img.src = realSrc;
                img.classList.add('loaded');
                img.removeAttribute('data-src');
            };
            tempImage.src = realSrc;
        }
        observer.unobserve(img);
    });
}, { rootMargin: '100px' });

document.querySelectorAll('.product-img').forEach(img => imgObserver.observe(img));

CSS 中给 .product-img 设置了占位底色和 opacity 过渡,这里就不贴具体样式了,你自己写个 transition: opacity .3s 就行。加载完成后加一个 loaded 类,把 opacity 变成 1。

这里其实偷了个懒:我先用 new Image() 预加载真实图,等缓存在浏览器里后,再设置 img.src。这样用户看不到从占位图“闪变”成真实图的过程,体验更平滑。这也是第三方库常用的套路。

五、小心动态渲染的内容

现在前后端分离很常见,很多页面在加载完初始数据后,还会根据用户操作或 WebSocket 推送往 DOM 里插入新列表项。这些新图片不会自动被 Intersection Observer 观察。需要重新调用 observe。

可以封装一个函数,用来观察新增的容器内的所有图片:

function observeImages(container = document) {
    const lazyImages = container.querySelectorAll('img[data-src]');
    lazyImages.forEach(img => imgObserver.observe(img));
}

// 新数据插入后
const newContainer = renderProducts(list); // 渲染函数
document.querySelector('#product-list').appendChild(newContainer);
observeImages(newContainer);

但要注意,这里我把 Observer 变量放在外层,方便复用。如果你不想为每个页面都创建一堆 observer,可以把 observer 放在一个单例里。

六、原生 loading=”lazy” 和 Intersection Observer 怎么选?

其实两条路可以混合使用。我记得自己刚开始做的时候,简单地给所有 img 加上了 loading="lazy",然后又用 Intersection Observer 去动态添加 class 做动画。问题来了:浏览器原生懒加载会延迟图片请求,而 Intersection Observer 的回调也可能在图片加载前触发。这两者一起用会不会有冲突?

经过测试,没有冲突。你可以这样理解:loading="lazy" 是“请求层面”的控制,而 Intersection Observer 是“行为层面”的控制。就算你用了 Intersection Observer 去设置 img.src,这个 src 最终是否被加载,仍然由浏览器的懒加载机制决定。换句话说,设置 src 只是给浏览器一个指令,浏览器会选择合适时机去请求。

所以如果你想同时使用两者,逻辑上没问题。但建议你把 loading="lazy" 加在真正的 img 上,同时去掉 data-src 那种替换逻辑,让浏览器自己管理请求时机,Intersection Observer 只负责加载动画之类的副作用。

举个例子:

<img 
    src="real-image.jpg" 
    loading="lazy" 
    class="product-img" 
    alt="商品">
const animateObserver = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.classList.add('in-view');
            animateObserver.unobserve(entry.target);
        }
    });
});
document.querySelectorAll('.product-img').forEach(img => animateObserver.observe(img));

这样一来,图片的加载完全交给浏览器,而且浏览器会在滚动快接近的时候提前加载,体验很好。你的 JS 就只需要管那些视觉感知类的交互。

七、还有几个值得注意的细节

1. 给图片提供宽高,避免布局位移

懒加载最常见的副作用就是图片加载前容器高度塌陷,导致页面来回跳。最简单的方法是给 img 设置固定的 widthheight 属性,让浏览器在图片未加载时就计算出占位空间。

<img src="shoes.jpg" width="800" height="600" loading="lazy" alt="运动鞋">

现在浏览器的 aspect-ratio 特性也可以做到,但显式宽高兼容性更好。

2. iframe 懒加载也一样简单

嵌入第三方视频或地图时,可以用 loading="lazy" 直接控制 iframe 的加载时机。这个是最容易忽略的。

<iframe src="https://player.vimeo.com/video/123" 
    loading="lazy" 
    title="视频"></iframe>

还有更激进的做法:初始时 src 留空,当用户滚动到附近时再注入 src。但这会增加复杂度,通常直接用 loading 就够了。

3. JavaScript 频繁设置 src 会逼着浏览器重新加载

如果你在代码里动态替换 img.src,比如每次都从一个 data 属性里取值赋给 src,而且这个赋值发生在用户疯狂滚动过程中,浏览器可能会立刻请求图片,导致之前的懒加载策略白做。稳妥的做法是把需要懒加载的图片放在 data-src 里,只有 Observer 触发了才去替换。

4. 别忘了低版本浏览器的降级

如果你的用户还有用旧版 Safari 或 IE,Intersection Observer 都可以通过 polyfill 解决。但 loading="lazy" 无法 polyfill。我当时的处理是写了一个几行的检测函数:

function loadingSupported() {
    return 'loading' in HTMLImageElement.prototype;
}

if (loadingSupported()) {
    // 原生懒加载
} else {
    // 用 Intersection Observer 手动实现加载,或者引入一个小的polyfill
}

实际上在 2024 年,全球的主流浏览器都支持 lazyload 了,基本可以放弃旧浏览器,但做项目还是留一手比较好。

八、性能实测:加载体积从 4.7 MB 变成 1.2 MB

我对比了一下改造前后首页的加载情况。原来用第三方懒加载库,页面全部加载完成后,所有图片都被下载了(因为库的优先级设置有问题)。改造后,首屏只加载了 3 张图片,其他图片在滚动到它们前 500px 才启动请求。整页最终加载体积少了 3.5MB,首屏 LCP 时间从 2.1 秒降到了 1.1 秒。

更明显的是 CPU 占用率。之前用 scroll 监听时,滚动过程中页面能明显感觉到掉帧。现在用原生和 Intersection Observer,几乎没有多余的 JS 计算,滚动非常流畅。

九、总结一下我的最终方案

目前我在所有的新页面里都遵循这个原则:

  • 对于普通 img,直接加 loading="lazy",不动任何 JS。
  • 对于需要监听加载状态或做动画的图片,结合 Intersection Observer 加 data-src。
  • 对于背景图、响应式图片、视频和 iframe,统一用 Intersection Observer 控制。
  • 所有图片都强制写宽高,避免 CLS。

这套组合拳在我看来已经是目前解决“懒加载”问题的最优解了,既省流量又保证体验,代码量还少。如果你还在被各种懒加载库困扰,也试试回归原生吧。

最后提一句,网上有人说 loading="lazy" 会影响图片资源的优先级,导致核心图加载变慢。但其实我们可以给重要的首屏图加 fetchpriority="high",来告诉浏览器哪个图更紧迫。这个属性和懒加载搭配使用效果更佳。

<img src="hero.jpg" alt="主视觉" fetchpriority="high" loading="lazy">

别奇怪,这两个属性真的可以共存。浏览器的策略在这种组合下会合理地推迟非关键的请求,而优先加载你标记为高的东西。

这次就分享到这里,希望你的页面也能变成一个“懒得加载”的瘦子。

HTML原生懒加载的工程化实践:我把js-lazy-load插件从项目里删了
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html HTML原生懒加载的工程化实践:我把js-lazy-load插件从项目里删了 https://www.taomawang.com/web/html/2480.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务