救命!我那加载慢到离谱的图片,终于被懒加载 “救活” 了

721 阅读7分钟

前言

你一定有这样的经历:打开一个图片超多的页面,进度条像蜗牛一样爬,屏幕上全是空白的占位框,你甚至怀疑自己的网是不是欠费了。

其实这不是你的网络问题,而是页面在一股脑加载所有图片,哪怕它们还在屏幕外面躺着。这就好比疯狂星期四你点了一份全家桶,结果服务员把一年的量都端了上来,你吃不完,还得花大力气搬回家。

好了,那么遇到问题就要解决问题。懒加载就是来解决这个问题的 “聪明服务员”—— 它只在你需要的时候,才把图片端到你面前。

除了图片,比如视频、音频、甚至是整个 DOM 模块(比如长列表里的卡片、分页内容)都可以用懒加载来优化,本文用图片做代表。

一、懒加载到底是什么?

简单来说,懒加载就是 “按需加载”。通俗来讲不就是 “懒得加载” 嘛。

具体实现如下:

  • 页面刚打开时,只加载当前屏幕里能看到的图片(或视频等等)
  • 等你往下滚动页面,其他图片才会在进入视野的瞬间加载出来
  • 这样一来,页面首次加载速度直接起飞,用户体验瞬间拉满。

就比如刷小红书刷快了,图片就会变成这样:

image.png

这可不能让网络背锅,这就是运用到了 -- 懒加载,既不耗太多性能,也不影响用户的体验。

二、传统懒加载:用滚动监听 + 节流实现

这是最经典的懒加载方案,核心思路是监听滚动事件,判断图片是否进入了可视区域

先上代码:第一个懒加载实现

// 可视区域高度
const visibleHeight = window.innerHeight;

// 懒加载函数
function lazyLoad() {
    const imgs = document.querySelectorAll('img[lazyload]');
    imgs.forEach(item => {
        const rect = item.getBoundingClientRect();
        // 判断图片是否在可视区域内
        if (rect.top < visibleHeight && rect.bottom >= 0) {
            item.src = item.dataset.origin;
            item.removeAttribute('lazyload');
        }
    })
}

// 节流函数:防止滚动事件触发太频繁
function throttle(fn, await) {
    let preTime = Date.now();
    return () => {
        const nowTime = Date.now();
        if (nowTime - preTime >= await) {
            preTime = nowTime;
            fn();
        }
    }
}

// 初始化和监听
lazyLoad();
window.addEventListener('scroll', throttle(lazyLoad, 200));

原理拆解:玩转懒加载

  1. window.innerHeight 先拿到当前屏幕的高度,这是我们的 “视野范围”
  2. getBoundingClientRect() 用来获取图片元素相对于视口的位置。
  3. 当图片的顶部进入视口rect.top < visibleHeight),并且底部还没完全离开视口rect.bottom >= 0)时,就把 data-origin 里的真实地址赋值给 src,图片就开始加载了。
  4. 最后别忘了用节流函数 throttle,不然滚动一下触发几十次 lazyLoad,性能开销会很大。

了解原理我们直接加上html代码,并且找10张图片实践一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        img{
            display: block;
            height: 200px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/OG4Cnt2SgXAuTj-Vv77ASGszUj1BwOhUXtBCplSlBfQmAAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/OceYG4p3E6SGFqFBN500JOHSiO4recmt6S1_fml1rlgUEAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://img1.baidu.com/it/u=2631091293,484496842&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="">
    <img src="" lazyload="true" data-origin="https://img2.baidu.com/it/u=3107839948,3570219976&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O98tXzU2bQgRJ5nRiiJQx_6uwzkB_rsF9e6TA7kcXr058AA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Oz13eqQ6iXnUgh-eQxuXT8VmWvogpk66DvRM52Oe0QbNQAA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Otvjht3x4wA58ZkSIxSo88NZTsyD6ZdUyiLMHZmGFoHokAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/O7ZsQ9IrSfcWAWLPeaRcfeEt5FdyeTfnFYrSGmDSKlU0sAA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O_2D2lSAfCS8o9_fGMsuNxOArO_rS1j9Nc2vsYaEaq2PwAA/641" alt="">
    <script>
        // 可视区域的高度
        const visibleHeight = window.innerHeight;

        // 懒加载函数
        function lazyLoad(){
            const imgs = document.querySelectorAll('img[lazyload]');
            // console.log(imgs);

            // 判断哪些 img 在可视区域内
            imgs.forEach(item => {
                console.log(123);
                const rect = item.getBoundingClientRect();
                if(rect.top < visibleHeight && rect.bottom >= 0){
                    item.src = item.dataset.origin;
                    item.removeAttribute('lazyload');
                }
            })
        }
        lazyLoad();
        // 监听滚动事件
        window.addEventListener('scroll', throttle(lazyLoad, 200));

        // 节流函数
        function throttle(fn, await){  // 在规定时间内只执行一次
            let preTime = Date.now();
            return () => {
                const nowTime = Date.now();
                if (nowTime - preTime >= await){
                    preTime = nowTime;
                    fn();
                }
            }
        }
    </script>
</body>
</html>

可以看到我刚打开浏览器就出现了4张图片,并且这4张图片已经加载好了。

image.png

很好,那当我们往下滑时,你会发现图片只要在可视区域就加载出来:

image.png

这个大家可以自己copy代码去试试因为图片看不出效果😭。

三、现代懒加载:用 IntersectionObserver 躺赢

传统方案需要自己监听滚动、计算位置,有点像手动开车。而 IntersectionObserver 就像自动驾驶,它会自动告诉你元素什么时候进入了视口。

看看这个更优雅的实现:

const io = new IntersectionObserver(
    () => {
        console.log('hello');
    },
    {
        threshold: [0, 0.25, 0.5, 0.75, 1.0]
    }
)
io.observe(document.getElementById('box'));

首先要明确:threshold 配置的核心作用是指定元素可见比例的触发节点,你配置的 [0, 0.25, 0.5, 0.75, 1.0],意味着当 box 元素的可见度达到 0%25%50%75%100% 时,都会触发一次回调函数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            height: 2000px;
        }
        #box{
            height: 100px;
            width: 100px;
            background-color: brown;
            margin: 100px;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <script>
        const io = new IntersectionObserver(
            () => {
                console.log('hello');
            },
            {
                threshold: [0, 0.25, 0.5, 0.75, 1.0]
            }
        )
        io.observe(document.getElementById('box'));
    </script>
</body>
</html>

也就是说,我们定义一个棕色的小方块,当这个方块的可见度0%25%50%75%100%时,就会打印一次hello

  • 最开始的时候,可见度100%所以直接打印一次hello

image.png

  • 当经过一半时,可见度分别经过了25%50%,算上最开始的共打印3次

image.png

了解原理后我们一样用上面的10张图片来举例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片懒加载演示</title>
    <style>
        img{
            display: block;
            height: 200px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <!-- 所有图片先不填真实src,用data-origin存真实地址,加lazyload标记 -->
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/OG4Cnt2SgXAuTj-Vv77ASGszUj1BwOhUXtBCplSlBfQmAAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Os3eJ8u3SgB3Kd-zrRRhgfR5hUvdwcVPKUTNO6O7sZfUwAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/OceYG4p3E6SGFqFBN500JOHSiO4recmt6S1_fml1rlgUEAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://img1.baidu.com/it/u=2631091293,484496842&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" alt="">
    <img src="" lazyload="true" data-origin="https://img2.baidu.com/it/u=3107839948,3570219976&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O98tXzU2bQgRJ5nRiiJQx_6uwzkB_rsF9e6TA7kcXr058AA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Oz13eqQ6iXnUgh-eQxuXT8VmWvogpk66DvRM52Oe0QbNQAA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/Otvjht3x4wA58ZkSIxSo88NZTsyD6ZdUyiLMHZmGFoHokAA/641" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/news_bt/O7ZsQ9IrSfcWAWLPeaRcfeEt5FdyeTfnFYrSGmDSKlU0sAA/1000" alt="">
    <img src="" lazyload="true" data-origin="https://inews.gtimg.com/om_bt/O_2D2lSAfCS8o9_fGMsuNxOArO_rS1j9Nc2vsYaEaq2PwAA/641" alt="">

    <script>
        // 1. 实例化 IntersectionObserver,传入回调函数
        const io = new IntersectionObserver(
            (entries) => {
                // entries 是被观察元素的状态数组
                entries.forEach(item => {
                    // isIntersecting 为 true 表示元素进入了视口
                    if(item.isIntersecting){
                        // 把 data-origin 里的真实地址赋值给 src,触发图片加载
                        item.target.src = item.target.dataset.origin;
                        // 移除 lazyload 标记,避免重复处理
                        item.target.removeAttribute('lazyload');
                        // 加载完成后停止观察该元素,节省性能
                        io.unobserve(item.target);
                    }
                })
            }
        )

        // 2. 获取所有需要懒加载的图片,逐个进行观察
        const imgs = document.querySelectorAll('img[lazyload]');
        imgs.forEach(item => {
            io.observe(item);
        })
    </script>
</body>
</html>

逻辑拆解:了解代码核心

  • HTML 部分:图片的 src 留空(避免默认加载),用 data-origin 自定义属性存储真实的图片地址,同时添加 lazyload="true" 作为筛选标记,方便后续获取需要懒加载的图片。

  • 实例化 IntersectionObserver:创建一个观察者实例,传入的回调函数会在被观察元素的可见状态变化时触发。

  • isIntersecting 判断:这是最关键的属性,它直接告诉我们元素是否进入了视口,不用自己手动计算位置,省去了大量冗余代码。

  • 加载图片并停止观察:当图片进入视口后,把 data-origin 赋值给 src,图片就会开始加载;加载完成后,用 io.unobserve(item.target) 停止观察该图片,避免后续滚动时重复触发回调,优化性能。

  • 批量观察图片:通过 querySelectorAll 获取所有带 lazyload 标记的图片,循环调用 io.observe(item) 让观察者 “盯紧” 这些图片。

进阶优化:配置 threshold 提前加载

如果你觉得 “进入视口才加载” 有点慢,想让图片提前一点点加载(比如元素露出 10% 就开始加载),可以添加 threshold 配置,让用户体验更流畅:

// 实例化时添加配置项
const io = new IntersectionObserver(
    (entries) => {
        entries.forEach(item => {
            if (item.isIntersecting) {
                item.target.src = item.target.dataset.origin;
                item.target.removeAttribute('lazyload');
                io.unobserve(item.target);
            }
        })
    },
    {
        threshold: 0.1 // 元素可见度达到 10% 时触发回调
    }
)

优势总结

  • 不用监听 scroll 事件,也不用手动计算元素位置,代码量少且易维护。
  • 浏览器原生支持,内部做了性能优化,比手动节流的滚动监听更高效。
  • 支持灵活配置,比如提前加载、指定观察根元素等,满足不同场景需求。

四、HTML 原生懒加载:一行代码搞定

如果你觉得 JavaScript 方案还是有点麻烦,那 HTML 原生的懒加载了解一下?

<img src="placeholder.jpg" loading="lazy" alt="图片">

只需要给img标签加个loading="lazy"属性,浏览器就会自动帮你实现懒加载。不过它的兼容性还不是 100%,所以目前项目中还是用JavaScript方案更稳妥。

五、总结

懒加载是解决 “图片多导致页面加载慢” 的实用技巧,核心是 “按需加载”—— 只在图片进入用户视野时才加载,既提升页面速度,又优化用户体验。

本文列举了3种方法

1. 传统方案:靠scroll监听 +getBoundingClientRect()计算位置,搭配节流函数避免性能浪费,但代码繁琐、需手动处理逻辑。

2. 现代方案(推荐) :用IntersectionObserver“自动驾驶” 式监听元素可见状态,通过isIntersecting判断是否加载,代码简洁、性能更优,还能通过threshold配置提前加载。

3. 原生方案:HTML 的loading="lazy"属性一行代码搞定,但兼容性不足,暂不适合全场景。

结语

从手动计算位置的 “原始人” 方案,到 IntersectionObserver“自动驾驶”,再到 HTML 原生的 “躺平” 方案,懒加载的进化史,就是前端开发者追求 “更懒、更高效” 的历史。无论你用哪种方案,核心思想都是一样的:不要让用户为他们还没看到的内容买单

下次再遇到图片加载慢的问题,别再骂网络了,试试懒加载,让你的页面飞起来!

IntersectionObserver原文文档