从理论到代码实操,图片预加载和懒加载

1,677 阅读3分钟

前言

在开发的过程中,为了页面价加载速度快、用户体验好我们常常会对图片进行预加载和懒加载。对于这两种什么时候用,具体要看情况而定。

预加载

什么是预加载

提前加载图片,加载完毕后会缓存到本地,当用户需要查看时可直接从本地缓存中渲染,拥有良好的用户体验。

为什么要使用预加载?

用户更好的体验,减少等待的时间。这种做法实际上牺牲了服务器的性能换取了更好的用户体验

使用举例

像是专门的图片网站,为了第一次打开的用户体验(不长时间的白屏,没有耐心的用户会关闭网页),就会使用预加载,但也不是全部加载就预加载一部分,当你在网站中浏览图片时,这时候又悄悄的加载一部分图片,这样就可以达到图片无缝连接的效果,你丝毫不会感觉慢。

还有像是浏览一组图片时,只给你显示第一张图片,然后可以通过一些形式(点击按钮)来切换图片,在你浏览第一张图片时,其实一整组图片都预加载完了,就等你切换了。

代码实现预加载

使用预加载前

可以明显看出在点击按钮时有一段的延时,然后再切换图片,这就是在请求图片(用户体验不行),再看图确实是从服务器请求来的,没有使用缓存。

动画.gif

Snipaste_2022-04-07_14-52-57.jpg

使用预加载

可以看到点击切换按钮后,页面迅速做出反应,无缝连接(用户体验极好),再看图片确实是提请请求,使用本地缓存。等到需要的时候,命中缓存,然后直接从缓存中渲染。

动画.gif

Snipaste_2022-04-07_14-54-53.jpg

使用JS来实现

<template>
  <div>
    <img :src="imgData[imgInd]" alt="" />
  </div>
  <button @click="onLeft">Left</button>
  <button @click="onRight">Right</button>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  name: "App",
  setup() {
    let imgData = ref<string[]>([
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/3.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/4.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/5.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/7.jpg",
    ]);
    // 预加载图片
    function preloadImg(srcArr: string[]): void {
      if (srcArr instanceof Array) {
        for (var i = 0; i < srcArr.length; i++) {
          var oImg = new Image();
          oImg.src = srcArr[i];
        }
      }
    }
    preloadImg(imgData.value);

    let imgInd = ref<number>(0);
    function onLeft() {
      imgInd.value - 1 < 0 ? 0 : --imgInd.value;
    }
    function onRight() {
      imgInd.value + 1 == imgData.value.length
        ? imgData.value.length - 1
        : ++imgInd.value;
    }
    return {
      onLeft,
      onRight,
      imgInd,
      imgData,
    };
  },
};
</script>

CSS实现

  <div>
    <img :src="imgData[imgInd]" alt="" />
  </div>
  <button @click="onLeft">Left</button>
  <button @click="onRight">Right</button>
  <div class="img1"></div>
  <div class="img2"></div>
  <div class="img3"></div>
  <div class="img4"></div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  name: "App",
  setup() {
    let imgData = ref<string[]>([
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/3.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/4.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/5.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/7.jpg",
    ]);
    
    let imgInd = ref<number>(0);
    function onLeft() {
      imgInd.value - 1 < 0 ? 0 : --imgInd.value;
    }
    function onRight() {
      imgInd.value + 1 == imgData.value.length
        ? imgData.value.length - 1
        : ++imgInd.value;
    }
    return {
      onLeft,
      onRight,
      imgInd,
      imgData,
    };
  },
};
</script>

<style scoped>
.img1 {
  height: 0px;
  width: 0px;
  background: url(https://cdn.jsdelivr.net/gh/lztnb/img@master/3.jpg);
}
.img2 {
  height: 0px;
  width: 0px;
  background: url(https://cdn.jsdelivr.net/gh/lztnb/img@master/4.jpg);
}
.img3 {
  height: 0px;
  width: 0px;
  background: url(https://cdn.jsdelivr.net/gh/lztnb/img@master/5.jpg);
}
.img4 {
  height: 0px;
  width: 0px;
  background: url(https://cdn.jsdelivr.net/gh/lztnb/img@master/5.jpg);
}
</style>

懒加载

什么是懒加载

懒加载可以说是延迟加载甚至是不加载。当访问一个页面的时候,用统一图片路径来代替(这样就请求一次,减轻服务器压力,俗称占位图),只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,请求服务器。

为什么要使用懒加载

懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求。页面加载速度快、可以减轻服务器的压力、节约了流量,用户体验好

使用举例

像是商场的页面,图片数量比较多,要是一次性请求加载,没个十几秒出不来。所以先出现用户界面需要的图片,将来可能需要出现在页面上的,等将来到了再加载(预加载是不等将来,直接请求)。

代码实现图片懒加载

没有使用懒加载 动画.gif

使用懒加载,第一次进页面就快了不少 动画.gif

懒加载代码

原理:图片到最外层offsetTop距离-(图片最近滚动父元素)到最外层offsetTop距离-(图片最近滚动父元素)的scrollTop距离 <= (图片最近滚动父元素)clientHeight距离[+X] (当然可以为了更好的用户体验可以提前加载,再加上一个距离X)。(随便你怎么嵌套滚动,都是可以的,可以在这个基础上再判断父元素是否出现在body的可视范围内。)

看不懂描述没关系,看图

image.png

<template>
  <div>图片懒加载</div>
  <div class="imgFrame">
    <img
      v-for="(data, ind) in imgData"
      :key="ind"
      :src="imgSrcArray[ind]"
      :ref="imgDom"
    />
  </div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  name: "App",
  setup() {
    let imgSrcArray = ref<string[]>([]);
    // 要懒加载的数据
    let imgData = ref<string[]>([
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/3.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/4.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/5.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/6.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/7.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/14.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/15.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/16.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/17.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/18.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/19.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/20.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/22.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/23.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/24.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/25.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/26.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/27.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/28.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/29.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/30.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/31.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/32.jpg",
      "https://cdn.jsdelivr.net/gh/lztnb/img@master/33.jpg",
    ]);
    // 使用默认图片
    imgData.value.forEach((val, ind: number) => {
      imgSrcArray.value[ind] =
        "https://acmphoto.oss-cn-beijing.aliyuncs.com/%E5%8A%A0%E8%BD%BD%E4%B8%AD4_3.png";
    });
    // imgDOM的元素
    let imgDomArray = ref<HTMLElement[]>([]);
    const imgDom = (el: HTMLElement) => {
      imgDomArray.value.push(el);
    };
    // 获取最近可以滚动的父元素
    function getParent<T extends HTMLElement>(e: T): T {
      let parentDom = e.parentNode;
      // eslint-disable-next-line no-constant-condition
      while (true) {
        if (parentDom == document.body) {
          break;
        }
        if ((parentDom as T).clientHeight == (parentDom as T).scrollHeight) {
          parentDom = getParent(parentDom as T);
        } else {
          break;
        }
      }
      return parentDom as T;
    }
    // 得到距离页面最上方的距离
    function getoffsetTop<T extends HTMLElement>(e: T): number {
      let offset = e.offsetTop;
      // 一直递归到最外层
      if (e.offsetParent != null) {
        offset += getoffsetTop(e.offsetParent as T);
      }
      return offset;
    }
    // 节流
    let time: number | null;
    onScroll();
    function onScroll() {
      if (time == null) {
        time = setTimeout(() => {
          onShow();
          time = null;
        }, 500);
      }
    }
    // 判断是否加载
    function onShow(): void {
      for (let i = 0; i < imgSrcArray.value.length; i++) {
        let cHeight = 0;
        let sTop = 0;
        let parentDom = getParent(imgDomArray.value[i]);
        parentDom.addEventListener("scroll", onScroll);
        if (parentDom == document.body) {
          cHeight = document.documentElement.clientHeight as number;
          sTop = document.documentElement.scrollTop as number;
          window.addEventListener("scroll", onScroll);
        } else {
          cHeight = parentDom.clientHeight;
          sTop = parentDom.scrollTop;
          parentDom.addEventListener("scroll", onScroll);
        }
        //判断是否加载的关键
        if (
          getoffsetTop(imgDomArray.value[i]) - getoffsetTop(parentDom) - sTop <=
          cHeight
        ) {
          imgSrcArray.value[i] = imgData.value[i];
        }
      }
    }
    return {
      imgData,
      imgSrcArray,
      imgDom,
    };
  },
};
</script>

<style scoped>
.imgFrame {
  margin-top: 20px;
  width: 800px;
  height: 600px;
  overflow: auto;
}
img {
  height: 100%;
  width: 100%;
  object-fit: contain;
}
</style>

结语

通过这样的从理论到实操,应该对图片的预加载和懒加载都有所了解。不过在开发中还是有插件就用插件,这样使开发更快更简单,不用自己写很多代码,说不定还有bug。但是合格的打工人还是要知道原理和会手撸代码,插件可以用,当原理也要会。