当前位置:首页 > 技术分析 > 正文内容

前端性能优化之图片懒加载「三种原生实现+vue指令」

ruisui884周前 (03-31)技术分析17

什么是懒加载?

懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。

如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在图片进入可视区域后才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。

懒加载带来的好处

  • 减轻服务器的压力
    对于一个展示大量图片的网页,使用懒加载可以显著地减少浏览器向服务器发送的请求数量,降低了服务器的压力,同时也减小了浏览器的负担。
  • 提升用户体验
    如果同时加载所有图片,会导致首屏的加载速度较慢,因为浏览器会把不在可视区域内的图片也一并加载。使用懒加载可以保证浏览器首先加载用户可视区域内的资源,提高用户体验。

懒加载的实现原理/如何实现懒加载

懒加载的核心原理主要是两块:

  1. 将图片的 src 属性置空,阻止图片加载:
    本文采用 data-* 自定义数据属性代替 src 来存储图片资源路径。笔者之前学习的时候也接触过将 src 备份后给 src 赋值为空字符串的方式,原理是一致的。
  2. 判断图片是否在可视区域内
    我们可以借助一些 API 来实现这一功能: HTMLElement.offsetTop、Element.scrollTop、Element.clientHeight、Window.innerHeight、IntersectionObserver

懒加载的其他注意事项:

  • 触发方式
    假设我们判断图片是否在可视区域内并进行图片加载操作的函数是 lazyLoad,所谓触发方式,就是什么时候执行 lazyLoad 这个函数。传统的方式是监听 window 对象的 scroll 事件,即页面发生滚动时触发 lazyLoadIntersectionObserver 这个 API 提供的功能则是对目标元素进行监听,但这是一个比较新的 api,要注意你的项目是否有浏览器兼容需求。它的回调函数触发条件也是被监听元素发生了滚动变化,这就为我们引入了下面两个问题。
  • 防抖/节流
    一次懒加载就可以加载一整个可视页面的图片,而滚动一个页面的距离会触发非常多次滚动事件,造成性能的浪费。针对这种场景,如果采用的是 window.onscroll 方式监听并触发懒加载,需要将懒加载函数套上一层防抖或者节流函数以限制其触发次数;如果采用的是 IntersectionObserver,它会逐个检测这些目标元素是否进入视窗,从而触发对应的加载。这就意味着不会一下子触发所有图片的加载,快速滑过时有的对象的回调函数还没来得及触发就已经不在可视区域内了。IntersectionObserver 会自动帮你管理这些,确保在性能和用户体验之间取得平衡。
  • 快速滑动至页面底部
    大部分网上的代码在不使用 IntersectionObserver 时判断图片是否在可视区域内都仅仅考虑了从上向下滚动时,图片超过在页面底部,就触发懒加载了。那么假设这样一种场景:用户进入页面后快速滚动页面到底部,这样就会导致所有的图片都在可视区域出现过,进一步导致所有图片是否在可视区域内的判断条件都成立了,那么所有图片都会被加载,可能很久都不能加载出用户想看的底部图片。所以如果使用传统的计算方式判断图片是否可见时,既要计算是否超过可视区域底部,也要考虑是否已经滑过了到可视区域上边外部去了,同时配合节流限制懒加载函数的触发频率(这样快速滑动后懒加载被触发时,滑过但超出可视区域顶部的图片就不会被加载了)以达到性能优化。

懒加载的原生实现

首先我们编写一个 html 文件,后面用于展示懒加载的效果:

我们先把其中的 data-src 写成正常的 src,可以看到每次页面刷新时所有的图片都会被直接加载好。如果一个页面中有非常多的图片,这样同时加载所有图片对浏览器和服务器都会造成压力。

使用原生 JS 实现图片懒加载,有下面将呈现的三种方式。说是三种方式,其实就是借助了几种不同的 API,来实现判断当前图片是否在可视区域内。事实上这三种方式是随着原生 JS 推出了新的 API 而演变的,越新的 API 实现起来越便捷,而旧的 API 可以兼容老版本的浏览器。

方式一

第一种方式如下图,图片顶部到文档顶部的距离 > 浏览器可视窗口高度 + 滚动条滚过的高度,此时的图片就是不可见的,如果 图片顶部到文档顶部的距离 < 浏览器可视窗口高度 + 滚动条滚过的高度 那么该图片就应该出现在可视区域内了。
但你还记得我们前面提到的注意事项吗?如果用户直接滑到页面底部,那么这个判断条件对所有的图片都为真,还是会造成性能问题。所以我们要再加上一条判断条件 图片的高度 + 图片顶部到文档顶部的距离 > 滚动条滚过的高度,以确保图片确实在可视区域内,而不只是被滑过。

当理解了这一判断条件之后,就只需要通过对应的 API 获取到他们即可:

  • 待加载图片的高度:img.clientHeight
  • 图片顶部到文档顶部的距离:img.offsetTop
  • 浏览器窗口滚动过的距离:document.documentElement.scrollTop document.body.scrollTop
  • 浏览器可视窗口高度:document.documentElement.clientHeightwindow.innerHeight

下面编写 js 代码:

至此,我们就初步完成了一个图片懒加载功能的实现,让我们来看看效果:

还记得我们先前说过的通过监听滚动的方式实现懒加载需要进行防抖节流处理吗?我们可以看一下现在没有进行处理的代码,在 lazyLoad 函数中打一个 console,可以看到我们简单的五张图片来回滑动一下,滚动事件被触发了好几十次,大量的滚动事件对浏览器性能来说是一个很大的负担。

防抖和节流都可以规避频繁触发回调函数,懒加载应该和哪一种方式结合更好呢?笔者更加推荐采用节流的方式,因为防抖需要用户完全停止滚动一定时间才能进行加载,如果用户在查找指定图片而不松开控制滚动条的鼠标,很难保证完全不触发滚动事件,这样就会导致无法触发懒加载函数,图片展示不出来,带来的用户体验并不好。

添加节流函数对我们的懒加载功能进行优化:

踩坑警告:懒加载的场景不要使用时间戳实现的节流,这会导致当你快速滑到一个位置并立刻停止滑动时,无法进行图片的加载。

看一下添加节流函数后的效果:

同样的操作这次仅仅触发了几次懒加载函数,并且我们可以通过调整节流函数的参数,控制懒加载函数触发的频率,以达到平衡性能和用户体验的最佳效果。

方式二


Element.getBoundingClientRect()
方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。 如下图

所以我们只需要判断 元素相对于可视窗口顶部的距离 < 可视窗口高度 来确保滚动条到了图片的位置,同时 元素相对于可视窗口顶部的距离的绝对值 > 元素本身的高度 来保证图片没有滚到可视窗口上方去。将方法一中的判断条件稍作更改,即得到了方法二:

实现的效果、节流等部分与方法一中一致,这里就不再赘述了,主要区别只是使用了不同的 API。

方式三

IntersectionObserver 是专门为检测某个元素是否出现在可视窗口中而推出的 API,我们这里简单介绍一下我们要用到的东西,对这个 API 感兴趣可以点击跳转到 mdn。

  1. 用法:

const observer = new IntersectionObserver(callback[, options]);

IntersectionObserver 构造函数新建一个对象,接收两个参数callbackoptions

  1. 实例方法
  • observe() 将一个元素加入监听目标集合
  • unobserve() 将一个元素移出监听目标集合
  1. callback
    当监听目标发生滚动变化时触发的回调函数。该回调函数接受一个参数 entries, 它其实是 IntersectionObserverEntry 的实例。简单来讲这个 entries 就存储着我们用 observe() 添加给 observer 实例的那些需要被监听的元素与其根元素容器在某一特定过渡时刻的交叉状态(默认为顶级文档的视窗)。而每一个 entry 有一个 target 属性,指向这个被监听的元素。

话不多说,我们直接上代码:

看看效果:

可以看到这一方法省去了繁琐的可视区域判断表达式,完美的实现了懒加载功能。别说用户快速下滑到页面底部,就是直接从底部开始也不会触发上面图片的加载了,非常的 nice。我们这里并没有进行节流处理,来看一下频繁滚动的场景会发生什么:

console 的位置可以在上面代码的备注中看到,一开始的五次打印是将这五张图片加入监听,在我们向下滚动后图片都加载完成被移出了监听,来回滚动总共就触发了四次回调函数,而且整体的效果也比先前的计算判断条件丝滑很多。笔者也尝试了极速下滑至页面底部,中间的图片有时是不会进行加载的,可见该 API 确实对性能和用户体验进行了很好的平衡。
美中不足的是,mdn 中该 API 的许多页面都标注着实验性技术,如果你的项目有兼容性需求,先检查一下是否可用。

vue 实现懒加载指令 v-lazy

自定义指令 类似于我们常常在模板中使用的 v-ifv-show 等指令,可以通过 app.directive 来全局注册。 我们可以在 src 目录下新建一个 directives 文件夹,存放我们自定义的指令。

main.js 里添加注册我们的 v-lazy 指令:

最后在组件中使用该指令:

这种方式是为了方便快捷的使用懒加载,并且可以自己选择对哪些图片进行懒加载。至于具体的实现,原生中的每一种实现方式在这里都是可以用的,只不过多套了一层自定义指令的壳。

注意事项

自定义指令是在 main.js 中引入的,不会被打包工具编译,只有 src 目录下的文件才会被编译。故而在 'lazy.js' 中是不能够使用相对路径的。解决方案:

  • 使用绝对路径
  • 使用 requie 引入路径(Webpack)/ 使用 getImageUrl 引入路径(Vite)

笔者这里使用的是 Vite,我们稍稍修改用以展示的组件。其实对于本地图片场景,绝对路径也挺方便;如果图片都是在线链接那就最好,无需处理。笔者只想到一种情况需要通过打包工具的静态资源处理来加载图片,那就是图片是存在本地的,选择哪张图片又和数据有关。遇到这样的场景我们只需要把 getImageUrl 的参数修改成对应的变量即可。

但这样写其实不太方便,总要定义 getImageUrl 这个函数,要么写到工具方法中,要么每个需要引入静态资源的组件都要写一遍。所以可以考虑使用
vite-plugin-require-transform
这样的插件来解决此问题。

写在最后

写本文的初衷是因为面试中被问到图片懒加载,才发现自己对这块的理解十分混乱,故而梳理一番。如果感觉本文对你有帮助,可以给笔者点赞收藏关注,怎么方便怎么来,对笔者这个新手前端是莫大的鼓励。最后,感谢阅读,如有错漏,烦请指正。


原文链接:
https://juejin.cn/post/7270792006522257445

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3182.html

分享给朋友:

“前端性能优化之图片懒加载「三种原生实现+vue指令」” 的相关文章

管理费用预算管理办法

第一章 总则第一条 为实现公司各项经营指标,控制费用开支,规范管理费用预算,特制定本办法。第二条 本办法适用于公司内各部门的管理费用预算工作。第三条 职责划分(一)各职能部门,负责提出部门预算目标及确定依据,编制部门预算,并执行预算方案。(二)财务部门,负责汇总、审查、分析、平衡各部门预算,提出调整...

Git 分支管理策略与工作流程

(预警:因为详细,所以行文有些长,新手边看边操作效果出乎你的预料)团队开发中,遵循一个合理、清晰的Git使用流程,是非常重要的。否则,每个人都提交一堆杂乱无章的commit,项目很快就会变得难以协调和维护。看完这篇文章后,涉及GIT的工作中就会减少因为规范问题导致工作出错,当然如果你现在暂时还未有合...

2024年,不断突破的一年

迈凯伦F1车队不久前拿下了2024年度总冠军,距离上一次还是二十几年前。在此期间,另一领域内,一个充满革新活力的腕表品牌——RICHARD MILLE理查米尔,正不断发展,与F1运动、帆船、古董车展等领域,共享着对速度与极限的无尽向往。RICHARD MILLE的发展与F1车手们在赛道上的卓越表现交...

有效地简化导航-Part 1:信息架构

「四步走」——理想的导航系统要做一个可用的导航系统,网页设计师必须按顺序回答以下4个问题:1. 如何组织内容?2. 如何解释导航的选项?3. 哪种导航菜单最适合容纳这些选项?4. 如何设计导航菜单?前两个问题关注构建和便签内容,通常称为信息架构。信息架构师通常用网站地图(site map diagr...

佳能 EOS R8 深度评测

佳能 EOS R8 的定位是入门级全画幅无反光镜可换镜头相机。尽管在产品阵容中处于这一位置,R8 仍然是一个强大的相机,配备了先进的 R6 II 同款成像传感器、快速处理器和令人难以置信的自动对焦系统,体积小、重量轻、价格低。这款相机是发烧友、旅行者、家庭以及任何想要全画幅传感器相机的人的绝佳选择。...

数组、去重、排序、合并、过滤、删除

ES6数字去重 Array.from(new Set([1,2,3,3,4,4])) //[1,2,3,4] [...new Set([1,2,3,3,4,4])] //[1,2,3,4]2、ES6数字排序 [1,2,3,4].sort(); // [1, 2,3,4],默认是升序...