性能优化之图片的预加载和懒加载

性能优化之图片的预加载和懒加载

前言

在现代 Web 开发中,图片是提升用户体验的重要元素,但过多的图片可能导致页面加载缓慢,影响性能。为了解决这一问题,开发者常采用 预加载懒加载 两种技术来优化图片加载策略。本文将从原理、实现方式及使用场景等方面深入探讨这两种技术。


图片的预加载

什么是图片的预加载?预加载是指在用户实际需要查看图片之前,提前将图片资源加载到浏览器缓存中。当用户后续需要访问这些图片时,可以直接从缓存中快速读取,减少等待时间。

预加载的实现方式

css的方式

通过隐藏的 CSS 元素(如背景图)触发图片加载,但不渲染到页面上:

javascript 复制代码
.hidden-preload {
  background: url('image1.jpg') no-repeat -9999px -9999px;
}

我们通过选择器应用html元素中,我们便可通过CSS的background属性将图片预加载到屏幕外的背景上。只要这些图片的路径保持不变,当它们在Web页面的其他地方被调用时,浏览器就会在渲染过程中使用预加载(缓存)的图片。简单、高效,不需要任何JavaScript。但是现实当中往往不止是图片,还会搭配其他内容,这样回增加整体的页面加载时间。这时候我们可以稍微的优化一些

javascript 复制代码
function preloader() {  
    if (document.getElementById) {  
        document.getElementsByClassName("hidden-preload")[0].style.background = "url(http://domain.tld/image-01.png) no-repeat -9999px -9999px";  
    }  
}  
function addLoadEvent(func) {  
    // 用于在页面及其所有资源完全加载后执行代码。
    var oldonload = window.onload;  
    if (typeof window.onload != 'function') {  
        window.onload = func;  
    } else {  
        window.onload = function() {  
            if (oldonload) {  
                oldonload();  
            }  
            func();  
        }  
    }  
}  
addLoadEvent(preloader);

window.onload 会在页面及其所有资源完全加载后执行代码。当页面及其所有资源加载完毕后触发 addLoadEvent 方法,我们使用addLoadEvent()函数来延迟preloader()函数的加载时间,直到页面加载完毕。通过合理使用window.onload,可以确保在页面完全加载后执行必要的初始化操作,从而提高用户体验和代码的可靠性。

css实现方式优点:

  1. 简单快速:无需编写 JavaScript,适合快速实现。
  2. 无侵入性:纯 CSS 实现,不依赖脚本逻辑。
  3. 兼容性好:支持所有浏览器,无需考虑 JavaScript 兼容性问题。

css实现方式缺点:

  1. 不可控性:无法监听加载状态(成功/失败),也无法触发后续逻辑。
  2. 资源浪费:可能预加载未使用的资源(如媒体查询未触发的图片)。
  3. 性能影响:过多预加载可能阻塞关键资源加载。

当需要监听每张图片的加载状态时,CSS 的方式便显得力不从心。此时,借助 Promise 实现图片加载便成为了一种更灵活且高效的解决方案。下面我们就来介绍一下Promise实现预加载。

基于Promise的图片预加载

loadImage 这是一个根据图片链接加载图片的函数,它返回一个Promise对象。当创建Promise对象时,我们创建一个Image对象,并设置它的src属性为我们提供的图片链接img。然后我们为Image对象添加两个事件处理器:onload和onerror。当图片加载成功时,onload事件处理器会被调用,然后调用resolve函数将Promise状态改为已解决,并且返回图片的链接。如果图片加载失败,onerror事件处理器会被调用,然后调用reject函数将Promise状态改为已拒绝,并返回图片链接。

javascript 复制代码
// 预加载组件
loadImage(img) {
   return new Promise(function(resolve, reject) {
     // 创建一个新的图片标签
     const image = new Image()
     // 将传进来的src赋值给新的图片
     image.src = img
     // 图片加载完成调用成功状态
     image.onload = function() {
       resolve(image.src)
     }
     // 图片加载完成调用失败状态
     image.onerror = function() {
       reject(image.src)
      }
   })
 },

loadNextImages 这是一个加载一批图片的函数。首先,我们从全部图片列表oldImgList中切割出前5个或剩下的所有图片,存入nextImagesList。然后,我们为nextImagesList中的每一个图片链接创建一个Promise(通过调用loadImage(img)函数),并将这些Promise存入newImgList数组。然后我们调用Promise.all(newImgList)。Promise.all()方法返回一个Promise,它在所有给定的Promise都被解决后解决,或在任意一个Promise被拒绝后拒绝。当所有图片都加载成功,我们将nextImagesList中的图片链接添加到需要显示的图片列表imageList中,并判断是否还有更多的图片需要加载,如果有,我们调用loadNextImages()继续加载下一批图片。如果有任何图片加载失败,我们在控制台打印错误信息。

javascript 复制代码
loadNextImages() {
  // 每次截取最前面的五个
  const nextImagesList = this.oldImgList.splice(
    0,
    Math.min(5, this.oldImgList.length)
  )
  const newImgList = []
  for (const img of nextImagesList) {
    newImgList.push(this.loadImage(img))
  }
  Promise.all(newImgList)
    .then(() => {
      this.imageList.push(...nextImagesList)
      if (this.oldImgList.length > 0) {
        this.loadNextImages()
      }
    })
    .catch(e => {
      console.error('Error images:', e)
    })
},

实现效果

从上面可以明显的看出来图片是分批一批一批的加载出来。我是为了直观的展示图片是一批一批加载出来的。现实当中一般是没有感觉的,首屏的图片是一次性加载出来的。看不见的地方才是这种分批加载出来的。

基于Promise实现方式优点:

  1. 精确控制:可监听每张图片的加载状态(成功/失败)。
  2. 异步管理:支持 Promise.all、async/await等异步流程控制。
  3. 动态灵活:可根据条件动态决定加载哪些资源。
  4. 错误处理:能捕获加载失败并执行回退逻辑。

基于Promise实现方式缺点:

  1. 代码复杂度:需要编写 JavaScript,对新手有一定门槛。
  2. 兼容性依赖:需 Polyfill 支持 Promise(如兼容 IE11)。
对比总结
特性 CSS 方式 Promise 方式
实现难度 简单 中等
可控制性 高(加载状态、错误处理)
资源管理 静态 动态
兼容性 所有浏览器 需 Polyfill(如 IE11)
适用规模 少量图片 中到大量图片

选择建议:

css实现方式适用场景:

  • 需要预加载少量图片且不关心加载结果。
  • 静态页面或对脚本依赖较低的项目。
  • 兼容老旧浏览器的场景(如 IE9 以下)。

基于Promise实现方式适用场景:

  • 需要确保关键图片加载完成后执行后续逻辑。
  • 动态加载大量图片且需管理加载优先级。
  • 需要错误处理和加载进度反馈的复杂应用。

图片的懒加载

什么是图片的懒加载?懒加载其实也叫做延迟加载、按需加载,在比较长的网页或应用中,如果图片有很多,一下子之间把所有的图片都加载出来的话,耗费很多性能,而且用户不一定会把图片全部看完。只有当图片出现在浏览器的可视区域内时,让图片显示出来,这就是图片懒加载。

懒加载的实现方式

img标签设置loading属性方式

loading="lazy" 是 HTML 中的一个属性,通常用于 <img> 标签中。它的作用是实现延迟加载(Lazy Loading),即当用户滚动到接近图片的位置时,才开始加载资源。这样可以减少页面初始加载时间,节省带宽,并提升用户体验。

javascript 复制代码
 <img src="https://images.unsplash.com/photo-1541963463532-d68292c34b19?w=50&h=50" loading="lazy" />

举个列子

javascript 复制代码
   class Exam extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          imgTotalList: [
            "https://images.unsplash.com/photo-1541963463532-d68292c34b19?w=50&h=50",
            "https://images.unsplash.com/photo-1507679799987-c73779587ccf?w=50&h=51",
          ], 
        };
      }
      render() {
        const { imgTotalList } = this.state;
        return (
          <div className="book-list" style={{ width: "100%", textAlign: "center" }}>
			// 通过高度将图片顶到可视窗口外
            <div style={{ textAlign: "center", height: '2000px' }}>
              下面有两张图片
            </div>
            {imgTotalList.map((img, index) => (
              <div>
                <img
                  key={index}
                  src={img}
                  alt={`img-${index}`}
                  width="150"
                  height="50"
                  loading="lazy"
                />
              </div>
            ))}
          </div>
        );
      }
    }

效果展示

从上面效果可以很明显的看到页面刷新的时候图片是没有加载的,是在滑动过程中动态的加载的。

img标签设置loading属性方式优点:

  1. 简单快速:无需编写 JavaScript,适合快速实现,完全由浏览器处理。
  2. 性能优化:浏览器会自动处理图片的懒加载逻辑,减少不必要的网络请求。
  3. 兼容性好:大多数现代浏览器都已支持该属性,开发者可以轻松利用这一特性优化网页性能。

img标签设置loading属性方式缺点:

  1. 有限的控制能力:开发者无法自定义触发懒加载的阈值,或方式(例如图片距离视口多远时开始加载,和加载方式)。
  2. 依赖兼容问题:一些旧版浏览器不支持。

通常通过为<img>标签设置 loading="lazy" 属性,可以轻松实现原生的图片懒加载功能。然而,这种方式虽然简单高效,但在需要更精细控制加载行为时显得有些局限。此时,借助 IntersectionObserver API 实现自定义懒加载方案,则能提供更大的灵活性和更强的性能优化能力。

使用 IntersectionObserver(交叉观察器)

IntersectionObserver接口,可以检测DOM节点是否出现在可视窗口,当DOM节点出现在可视窗口中才加载图片。在IntersectionObserver实例对象的回调函数里实现当观测到图片进入视口时,我们就将占位图换成真实的图片地址,并且停止观察这个元素。

javascript 复制代码
<style>
    .img-box .lazy-img {
        width: 100%;
        height: 600px; /*如果已知图片高度可以设置*/
    }
</style>

<div class="img-box">
	<img class="lazy-img" data-src="./img/1.jpg" alt="懒加载1"  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" />
    <img class="lazy-img" data-src="./img/2.jpg" alt="懒加载2"  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="/>
    <img class="lazy-img" data-src="./img/3.jpg" alt="懒加载3"  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="/>
    <img class="lazy-img" data-src="./img/4.jpg" alt="懒加载4"  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="/>
    <img class="lazy-img" data-src="./img/5.jpg" alt="懒加载5" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" />
</div>

<script>
    function Observer() {
        let images = document.querySelectorAll(".lazy-img");
        let observer = new IntersectionObserver(entries => {
            entries.forEach(item => {
               if (entry.isIntersecting) {
                const img = entry.target;
                const realSrc = img.dataset.src;
                // 加载真实图片
                img.src = realSrc;
                // 停止观察该图片
                this.observer.unobserve(img);
              }
            });
        }, {
    		rootMargin: "50px", // 提前加载的缓冲区域
    		threshold: 0.01 // 触发加载的可见比例
  		});
        images.forEach(img => observer.observe(img));
    }
    Observer()
</script>

上面通过创建了一个观察器,当图片与视口发生交集时,会触发图片的真实加载。此方法适用于需要复杂逻辑判断的页面。还可以扩展自己自定义加载方式。

举个例子,我们可以在图片加载时添加一个动画class

javascript 复制代码
<style>
  /* 添加动画关键帧 */
  @keyframes slideIn {
    from {
      opacity: 0;
      transform: translateX(200px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }
  /* 应用动画的类名 */
  .lazy-img {
    opacity: 0;
    /* 初始透明 */
    transition: opacity 0.3s ease-out;
    /* 备用淡入效果 */
  }
  .lazy-img.loaded {
    animation: slideIn 0.6s ease-out forwards;
  }
  /* 图片容器样式 */
  .img-box {
    padding: 20px;
  }
</style>
<script>
    function Observer() {
        let images = document.querySelectorAll(".lazy-img");
        let observer = new IntersectionObserver(entries => {
            entries.forEach(item => {
               if (entry.isIntersecting) {
          		...
                // 加载真实图片
                img.src = realSrc;
				// 图片加载完成后添加动画类
  				img.onload = () => {
      				img.classList.add('loaded');
  				};
           		...
              }
            });
        }
        images.forEach(img => observer.observe(img));
    }
    Observer()
</script>

下面我们就展示一下懒加载的动画效果

从上面可以很明显的看出当滑动到响应位置才加载对应的图片,并且可以添加相对应的动画。

使用 IntersectionObserver优点:

  1. 高度可控:可以通过 rootMargin 和 threshold 自定义触发懒加载的条件。
  2. 灵活性强:可以扩展功能,例如为图片添加占位符、动画效果等。

使用 IntersectionObserver缺点:

  1. 复杂性较高:需要编写额外的 JavaScript 代码。
  2. 性能开销:相比原生 loading="lazy",手动实现懒加载可能会增加一定的性能开销,尤其是在图片数量较多时。
  3. 旧版兼容:旧版浏览器去要通过 polyfill 支持

上面两种方法都存在一定的浏览器或者版本兼容问题。当两种都不适应的话我们就不得不适用 Scroll 事件实现方式 ,Scroll 事件监听实现懒加载方式就相对上面两种复杂一点,但是它不依赖新特性,兼容性好。

Scroll 事件监听实现懒加载的原理

Scroll 事件监听是通过 图片顶部到文档顶部的距离 > 浏览器可视窗口高度 + 滚动条滚过的高度 ,此时的图片就是不可见 的,如果 图片顶部到文档顶部的距离 < 浏览器可视窗口高度 + 滚动条滚过的高度 那么该图片就应该出现在可见区域内了。然后就是通过Scroll计算检测图片是否到可视区域,后面的流程就和IntersectionObserver 逻辑差不多。

javascript 复制代码
// 处理滚动事件
handleScroll = () => {
  console.log('懒加载检测中...');
  // requestAnimationFrame 是浏览器提供的一个 API,能够更好地利用浏览器的渲染机制,避免不必要的计算
  requestAnimationFrame(() => {
    const windowHeight = window.innerHeight; // 浏览器可视窗口的高度
    const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; // 滚动距离
    const imgs = document.querySelectorAll('.lazy-img');
    imgs.forEach((img) => {
      if (!img.dataset.loaded && // 图片尚未加载
        windowHeight + scrollHeight > img.offsetTop  // 图片顶部进入视口
      ) {
        img.src = img.dataset.src; // 加载真实图片
        img.dataset.loaded = true; // 标记为已加载
      }
    });
  });
};

上面就是一个简单的 Scroll 事件 实现的懒加载。但是在真正的项目中他并不能直接使用。我们简单的来回滑动一下,滚动事件被触发了好几十次,大量的滚动事件对浏览器性能来说是一个很大的负担。

这里我们可以添加节流函数稀释除法 Scroll 事件频率,以达到平衡性能和用户体验的最佳效果。

javascript 复制代码
// 处理滚动事件
handleScroll = this.throttle(() => {
  console.log('懒加载检测中...');
  requestAnimationFrame(() => {
	// 原有逻辑...
  });
}, 100);

使用 Scroll 事件 实现的懒加载 优点:

  1. 适应能力强:对于包含复杂布局的网页来说,基于 scroll 事件监听的懒加载能够很好地适应不同的布局需求。它不需要对整个页面结构做出严格假设,因此更加灵活多变。
  2. 灵活性强:可以扩展功能,例如为图片添加占位符、动画效果等。
  3. 兼容性好:几乎所有现代浏览器都支持 scroll 事件,这意味着你可以在几乎任何环境中部署基于此方法的懒加载方案,而不用担心兼容性问题。

使用 Scroll 事件 实现的懒加载缺点:

  1. 复杂性较高:需要计算和优化。
  2. 频繁触发 scroll 事件:频繁触发 scroll 事件增加一定的性能开销。
对比总结
特性 loading="lazy" Intersection Observer Scroll 监听
实现难度 最简单(一行代码) 中等(需编写观察器逻辑) 复杂(需计算和优化)
性能 最优(浏览器原生优化) 高(无频繁事件) 低(需节流和计算)
可控性 高(可配置触发条件) 最高(完全自定义逻辑)
兼容性 中(现代浏览器支持) 中(需 Polyfill 支持旧版) 高(全浏览器支持)
适用场景 简单需求、快速实现 现代项目、需精细控制 兼容旧浏览器的复杂场景

选择建议:

  • 如果你只需要简单的懒加载功能,并且目标浏览器支持 loading="lazy",那么这是最简便的选择。
  • 对于需要更多控制和灵活性的应用,尤其是涉及大量动态内容的场景,推荐使用 IntersectionObserver。
  • 在某些特定情况下,比如兼容旧系统且不介意一定的性能损失时,scroll 事件监听也是一种可行的解决方案。不过,考虑到性能和维护成本,尽量避免在大规模应用中使用这种方法。

总结

预加载指的是提前加载页面中所需的图片,确保用户访问时能够快速查看内容,减少等待时间。这种方法适用于关键图片资源,确保重要图片在用户滚动到之前就已经加载完毕。一般应用场景有:

  • 关键重要图片的快速展示: 对于那些需要立即展现的重要图片,例如电商平台上的产品主图或是新闻门户首页的头条图片,推荐使用预加载技术。这确保了这些核心视觉元素能够在页面加载初期就迅速呈现出来,从而提升用户的首次访问体验和整体满意度。
  • 幻灯片与轮播图的无缝切换: 在设计包含幻灯片或轮播图组件的网页时,通过预加载即将展示的图片资源,可以让用户在进行内容切换时享受流畅无阻的视觉体验。这种方式保证了下一张图片已预先准备好,实现了即时显示,避免任何可能的延迟。
  • 适用于小型网站及单页面应用: 针对页面数量不多且图片资源有限的小型网站或单页面应用程序,考虑直接采用全面预加载策略。这种方法简化了图片资源的管理流程,使得所有图片均能在初次加载时完成准备,为用户提供一种无缝、无需等待的浏览体验。这样做不仅提升了用户体验,也简化了开发者的资源管理负担。

懒加载则是一种按需加载的技术,即仅当下图进入用户的视口(可见区域)或即将进入视口时才进行加载(甚至不加载)。这样可以减少初始页面加载时间,提高页面性能,特别适合于长页面或图片量较大的网站。一般应用场景有:

  • 图文丰富的页面: 对于新闻资讯、博客等含有大量图片和文字的页面,采用懒加载技术,仅在初始时加载首屏图片,其余图片待滚动至视口时再加载,以避免拖慢页面加载速度。
  • 长列表或图库页面: 在电商详情页、图片社交类页面中,通常包含大量的图片列表。为了避免影响加载性能,使用懒加载技术只加载当前视口内的图片,提高页面响应速度。
  • 无限滚动页面: 针对使用无限滚动加载数据的页面,无法一次性加载所有图片,必须应用懒加载策略来确保只有当图片进入或即将进入视口时才进行加载,从而优化性能和用户体验。

预加载和懒加载是优化网页图片显示的两种策略,它们适用于不同的场景并通常涉及不同的技术。在某些情况下,这两种方法还可以结合使用以达到最佳效果。

相关推荐
魔云连洲4 小时前
深入解析:Object.prototype.toString.call() 的工作原理与实战应用
前端·javascript·原型模式
JinSo4 小时前
alien-signals 系列 —— 认识下一代响应式框架
前端·javascript·github
开心不就得了4 小时前
Glup 和 Vite
前端·javascript
szial4 小时前
React 快速入门:菜谱应用实战教程
前端·react.js·前端框架
西洼工作室4 小时前
Vue CLI为何不显示webpack配置
前端·vue.js·webpack
黄智勇5 小时前
xlsx-handlebars 一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使
前端
brzhang6 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
brzhang6 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
井柏然7 小时前
前端工程化—实战npm包深入理解 external 及实例唯一性
前端·javascript·前端工程化
IT_陈寒8 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端