vue图片懒加载——首屏性能优化

一、懒加载

1、什么是懒加载?

懒加载也叫做"延迟加载"或者"按需加载",是一种常用的网页性能优化方式之一。在含有很多图片的页面中,如果所有图片都加载出来了,而用户却只能看到可是窗口的那一部分图片数据,就会影响性能,造成不必要的请求。

2、方案

解决上述问题,项目中一般会使用图片的懒加载。在滚动屏幕之前,只加载可视区域的图片,可视区域之外的图片不会进行加载。这样就可以加快页面的加载速度,减少服务器的负载。懒加载一般常用在图片较多的长列表场景中。

3、懒加载的优点:

markdown 复制代码
1. 提升页面渲染效率,避免大量图片加载造成页面卡顿;
2. 按需加载,减少无效的图片加载,节约网络资源。

4、懒加载的核心原理

markdown 复制代码
1. 将图片的src属性置空或者将图片地址存放在自定义属性[data-xxx]中,阻止图片加载;
2. 判断这些图片是否在可视区域内。
    offsetTop
    scrollTop
    clientHeight
    intersectionObserver
3. 触发方式
    假设我们判断图片在可视区域内并进行图片懒加载操作函数是 lazyLoad 。一般都是页面滚动时触发,监听 window 对象的 scroll 事件。如果不考虑浏览器的兼容性 ,IntersctionObserver这个 API 提供的功能则是对目标元素进行监听,是一个比较新的api。
4. 防抖/节流
    一次懒加载就可以加载整个可视页面的图片,而我们滚动一个页面的距离会触发很多次滚动事件,所以需要进行节流处理(当我们采用window.onscroll 方式监听并触发懒加载),需要将懒加载函数套一层节流函数,来限制其触发次数。如果采用的是 IntersectionOberserver,他会逐个检测目标元素是否进入可视窗口内,只有在可视窗口中才会触发对应的加载。
5. 快速滚动到页面底部
    如果不使用 IntersectionObserver 判断图片是否在可视区域内,仅考虑从上到下滚动式,图片超过可视区域底部就触发懒加载的话,有一种场景: 用户进入页面后快速滚动到页面底部,这样就会导致所有图片都在可视区域出现过,所以所有图片都被会加载,导致用户想看的页面底部的图片很久都加载不出来。
    所以如果使用传统的计算方法判断图片是否可见,既要计算是否超过可视区域底部,也要计算是否滑过可视区域上面去了,再配合节流限制懒加载函数的触发频率,从而达到性能的优化。

二、懒加载实现方式

原生页面, 先把自定义属性 data-src 内容写成正常的 src 地址,

js 复制代码
   <div class="container" ref="container">
      <ul>
        <li>
          <!-- 如果有占位图,可以将图片的src都设为占位图的路径 -->
          <img data-src="./img/1.jpg" alt="懒加载1" />
          <img data-src="./img/2.jpg" alt="懒加载2" />
          <img data-src="./img/3.jpg" alt="懒加载3" />
          <img data-src="./img/4.jpg" alt="懒加载4" />
          <img data-src="./img/5.jpg" alt="懒加载5" />
          ...
        </li>
      </ul>
    </div>
    <script src="./lazyLoad.js"></script>

计算逻辑

图片顶部到文档顶部的距离 > 浏览器可视窗口高度 + 滚动条滚过的高度 图片的高度 + 图片顶部到文档顶部的距离 < 滚动条滚过的高度

待加载图片的高度:

图片不在可视窗口内的条件: 图片距离容器的距离 > 容器的clientHeight 图片距离容器的距离 < -图片的高度

参数整理 待加载图片的高度:img.clientHeight 图片距离容器的距离: 图片的top - 容器的top 容器可视窗口的高度:container clientHeight

方案一: offsetTop,clientHeight,

未加防抖/节流的代码如下

js 复制代码
// 获取到所有的 img 标签对应的元素,并存到 imgs 数组中
const imgs = document.getElementsByTagName('img'),
  container = document.getElementById('container');

function lazyLoad(imgs) {
  // 容器可视窗口的高度
  let containerH = container.clientHeight,
    containerT = container.offsetTop;
  for (let i = 0; i < imgs.length; i++) {
    // 根据之前分析的理论进行在窗口内的判断逻辑
    // 图片距离容器的距离
    let disTop = imgs[i].offsetTop - containerT;
    if (disTop < containerH && disTop > -imgs[i].offsetHeight && !imgs[i].src) {
      // 使用data-xxx 的自定义属性可以通过 dom 元素的 dataset.xxx 取值
      imgs[i].src = imgs[i].dataset.src;
    }
  }
}
// 进入页面时执行一次加载
lazyLoad(imgs);
// 监听滚动事件,进行图片懒加载
window.onscroll = () => lazyLoad(imgs)

节流函数处理

js 复制代码
/**
 * @description: 节流函数
 * @param { Function } fn 回调函数
 * @param { Number } delay 间隔时间
 * @param { ...any } args 回调函数 fn 需要用到的参数
 * @return {*}
 */
// 节流处理
function throttle(fn,delay,...args) {
  let timer = null;
  return () => {
    const context = this; // 保存当前上下文
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(context, args); // 执行函数,并传入参数
        timer = null; 清空定时器
      },delay)
    }
  }
}
// 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)
window.onscroll = throttle(lazyLoad,500,imgs)

整合代码

js 复制代码
// 获取到所有的 img 标签对应的元素,并存到 imgs 数组中
const imgs = document.getElementsByTagName('img'),
  container = document.getElementById('container');

function lazyLoad(imgs) {
  // 容器可视窗口的高度
  let containerH = container.clientHeight,
    containerT = container.offsetTop;
  for (let i = 0; i < imgs.length; i++) {
    // 根据之前分析的理论进行在窗口内的判断逻辑
    // 图片距离容器的距离
    let disTop = imgs[i].offsetTop - containerT;
    if (disTop < containerH && disTop > -imgs[i].offsetHeight && !imgs[i].src) {
      // 使用data-xxx 的自定义属性可以通过 dom 元素的 dataset.xxx 取值
      imgs[i].src = imgs[i].dataset.src;
    }
  }
}
// 进入页面时执行一次加载
lazyLoad(imgs);
// // 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)

/**
 * @description: 节流函数
 * @param { Function } fn 回调函数
 * @param { Number } delay 间隔时间
 * @param { ...any } args 回调函数 fn 需要用到的参数
 * @return {*}
 */
// 节流处理
function throttle(fn,delay,...args) {
  let timer = null;
  return () => {
    const context = this; // 保存当前上下文
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(context, args); // 执行函数,并传入参数
        timer = null; 清空定时器
      },delay)
    }
  }
}
// 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)
window.onscroll = throttle(lazyLoad,500,imgs)

方案二:getBoundingClientRect()

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

所以我们只要判断 元素相对于可视窗口顶部的距离 < 可视窗口高度 来确保滚动条到了图片的位置,同事 元素相对于可视窗口的顶部距离 大于 负的元素本身的高度 来保证图片没有滚动到可视窗口上方去。

js 复制代码
// 获取到所有的 img 标签对应的元素,存到 imgs 数组中
let imgs = document.getElementsByTagName('img'),
  container = document.getElementById('container');
function lazyLoad(imgs) {
  // 容器可视窗口高度
  const containerH = container.offsetHeight,
    containerT = container.offsetTop;
  for (let i = 0; i < imgs.length; i++) {
    let imgT = imgs[i].getBoundingClientRect().top,
      imgH = imgs[i].getBoundingClientRect().bottom - imgs[i].getBoundingClientRect().top
    divTop = imgT - containerT;
    // !imgs[i].src 当该图片已经加载好之后,无需重新加载
    if (divTop < containerH && divTop > -imgH && !imgs[i].src) {
      // 使用data-xxx 自定义属性 可以通过dom 元素的 dataset.xxx 取得。
      imgs[i].src = imgs[i].dataset.src;
    }
  }
}
// 进入页面,先执行一次
lazyLoad(imgs);

function throttle(fn, delay, ...args) {
  let timer = null;
  return () => {
    const context = this;
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(context, args) // 执行函数,并传入参数
        timer = null; // 清空定时器
      },delay)
    }
  }
}
// 监听滚动事件,加载后面的图片
window.onscroll = throttle(lazyLoad,500,imgs)

方案三: IntersectionObserver

IntersectionObserver 是专门检测某个元素是否出现在可视窗口中

  1. 用法
js 复制代码
const observer = new IntersectionObserver(callback[, options]);

以 IntersectionObserver 构造函数新建一个对象,接收两个参数 callback 和 options . 2. 实例方法 observe() 将一个元素加入监听目标集合 unobserve() 将一个元素一处监听目标集合 3. callback 当监听目标发生滚动变化时触发的回调函数。该回调函数接收一个参数 entries , 他其实就是被监听的元素与根元素容器的交叉状态。而每一个entry 有一个 target 属性,指向这个被监听的元素,有一个isIntersecting 属性,判断元素是否出现在视口内。同时也进行了节流处理,无需我们自己在进行手动节流处理

js 复制代码
document.addEventListener('DOMContentLoaded', () => {
  if ('IntersectionObserver' in window) {
    const imgs = document.getElementsByTagName('img');
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        // 通过该属性判断元素是否出现在视口内
        // isIntersecting 布尔值,目标元素与交集观察者的根节点是否相交
        if (entry.isIntersecting) {
          // entry.target 能够获取到那个 dom 元素
          // 被观察的目标元素,是一个DOM节点对象
          let img = entry.target;
          img.src = img.dataset.src;
          // 图片加载完成后解除监听
          observer.unobserve(img)
        }
      })
    });
    [...imgs].forEach(img => {
      // 将所有的图片假如监听
      observer.observe(img);
    })
  } else {
    alert('您的浏览器尚不支持IntersectionObserver')
  }
})

方案四: vue中自定义懒加载

v-lazy + InterSectionObserver

自定义指令 ,可以通过 app.directive 来全局注册。我们可以在 src 目录下的 directives 文件夹中新增一个 lazyLoad.js 文件 存放我们自定义懒加载指令

js 复制代码
const lazyLoad = {
  // mounted 在绑定元素的父组件及它自己的所有子节点都挂载完成后调用
  mounted(el,binding) {
    // 如果有需要可以先设置src为 loading
    // el.setAttribute('src','loading 图的路径')
    const options = {
      threshold: [0.1], // 他是数组,决定何时触发回调函数,0.1 即交叉比例为10%时触发
      rootMargin: '0px', // 用来扩大或者缩小视图的大小,使用 css 的定义方法,10px 10px 30px 20px 表示 top right bottom 和 left的值
      // root: null, // 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素
    };
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // binding 是一个对象, binding.value 是传递给指令的值
          el.setAttribute('src', binding.value);
          // 移除监听
          observer.unobserve(el)
        }
      })
    }, options);
    // 监听
    observer.observe(el);
  }
}

export default lazyLoad;

在 main.js 文件中注册我们的 v-lazy 指令:

js 复制代码
// ...
import lazyLoad from './directives/lazyload';
const app = createApp(App); // App 组件
app.directive('lazy', lazyLoad);
// ...

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

  1. 使用绝对路径
  2. 使用 require 引入路径(Webpack) 或者 getImageUrl 引入路径(vite)
js 复制代码
<script setup>
// getImageUrl
const getImageUrl = (name) => {
  return new URL(`./img/${name}`, import.meta.url).href
 }
</script>

<template>
    <div class="container" ref="container">
      <ul>
       <li>
          <!-- 如果有占位图,可以将图片的src都设为占位图的路径 -->
          <img v-lazy="getImageUrl('1.jpg')" alt="懒加载1" />
          <img v-lazy="getImageUrl('2.jpg')" alt="懒加载2" />
          <img v-lazy="getImageUrl('3.jpg')" alt="懒加载3" />
          <img v-lazy="getImageUrl('4.jpg')" alt="懒加载4" />
          <img v-lazy="getImageUrl('5.jpg')" alt="懒加载5" />
        </li>
      </ul>
    </div> 
</template>

v-lazy + getBoundingClientRect()

上面我们通过 v-lazy:[container] = 'image.cover' 来接管每个图片的原始地址,其中 container 为父容器的类名,在指令里判断图片是否在可视区会用到父容器,并且可以在 data 中动态指定。

另外,需要提前准备一张占位图,每个图片默认开始时使用占位图,地址('src/directives/lazy/img/cover-def.gif')

封装一个事件总线

js 复制代码
import Vue from 'vue'
const bus = new Vue();
Vue.prototype.$bus = bus
export default bus

页面模版

js 复制代码
<script>
import { getImages } from '@/api/images'
export default {
data () {
  return {
    imgs: []
  }
  },
async created () {
  const { data } = await getImages();
  this.imgs = Object.freeze(data.list);
  loadImages(this.imgs)
},
mounted(){
  this.$ref.container.addEventListener('scroll', () => {
    this.$bus.$emit('isScroll',this.$ref.container)
  })
}
}
</script>
 
<template>
  <div class='container' ref="container">
    <ul>
      <li v-for="(image,idx) in images" :key="image.id">
        <img v-lazy:[container]="image.cover"  alt="" :data-id="idx">
      </li>
    </ul>
  </div>
</template>
<style scoped lang="less">
</style>

自定义指令

js 复制代码
import Vue from 'vue'
import { debounce } from '@/utils'
import defImage from './img/cover-def.gif'
import eventBus from '@/eventBus'


// 用来收集需要懒加载的图片信息
let imgs = [];
const lazyLoad = {
  mounted(el, binding) {
    const { value, arg } = binding;
    let container = el.parentNode
    // 设置默认图片
    el.src = defImage
    // 找到容器
    while (container && container.className.indexOf(arg) == -1) {
      container = container.parentNode
    }
    // options
    const img = {
      el,
      src: value,
      container: {
        top: container && container.getBoundingClientRect().top || 0,
        clientHeight: container && container.clientHeight || 0
      },
      height: el.height || 0
    }
    // 添加到当前需要被懒加载的图片集合内,当㚵时,通过加载真实图片
    imgs.push(img)
    // 进入页面后立即执行一次
    loadImg(img);
  },
  unmounted(el,binding) {
    // 当前图片的指令接触绑定时,需要从imgs中删除(因为此时当前图片已经销毁)
    deleteImg(el);
  },
}

// 查看所有待加载的图片
function loadImages() {
  for (const img of imgs) {
    loadImg(img)
  }
}

// 加载真实图片
function loadImg(img) {
  const { el, src, height, container } = img;
  img.top = el.getBoundingClientRect().top || 0;
  const disTop = img.top - container.top;
  // 不在可视区 或者 el.src!=defImage 已经加载过了
  if (disTop > container.clientHeight && disTop < -height || el.src != defImage) return
  // 在可视区
  const image = new Image();
  image.onload = () => {
    el.src = src
  }
  image.src = src
  // 加载过的图片,需要将其从imgs中清除
  // deleteImg(el)
}

function deleteImg(el) {
  // 删除imgs 中已经加载的真实图片
  imgs = imgs.filter((img)=> img.el != el)
}

// 监听滚动
eventBus.$on('isScroll', debounce(loadImages, 500))

Vue.directive('lazy', lazyLoad)
export default lazyLoad;
相关推荐
开心工作室_kaic10 分钟前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿29 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具1 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161772 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript