Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【性能优化】手把手实现图片懒加载+Vue封装 #46

Open
amandakelake opened this issue Apr 21, 2018 · 4 comments
Open

【性能优化】手把手实现图片懒加载+Vue封装 #46

amandakelake opened this issue Apr 21, 2018 · 4 comments
Labels
Web 网络、性能、优化类

Comments

@amandakelake
Copy link
Owner

amandakelake commented Apr 21, 2018

【性能优化】手把手实现图片懒加载+Vue封装

本文以懒加载为主,先看看懒加载和预加载两者的概念对比

一、总览

293bf8f6-acff-447b-b8d3-4a66bc11ef69

1、为什么要懒加载或者预加载

对页面加载速度影响最大的就是图片
当页面图片比较多,加载速度慢,非常影响用户体验

思考一下,页面有可能有几百张图片,但是首屏上需要展示的可能就一张而已,其他的那些图片能不能晚一点再加载,比如用户往下滚动的时候……

这是为什么要用懒加载的原因

那预加载呢?
这个非常语义化,预备,提前……
就是让用户感觉到你加载图片非常快,甚至用户没有感受到你在加载图片

2、懒加载原理

图片先用占位符表示,不要将图片地址放到src属性中,而是放到其它属性(data-original)中
页面加载完成后,监听窗口滚动,当图片出现在视窗中时再给它赋予真实的图片地址,也就是将data-original中的属性拿出来放到src属性中
在滚动页面的过程中,通过给scroll事件绑定lazyload函数,不断的加载出需要的图片

注意:请对lazyload函数使用防抖与节流,不懂这两的可以自己去查

3、懒加载方式

1)、纯粹的延迟加载,使用setTimeOut或setInterval

这种方式,本质上不算懒加载
加载完首屏内容后,隔一段时间,去加载全部内容
但这个时间差已经完成了用户对首屏加载速度的期待

2)、条件加载

用户点击或者执行其他操作再加载
其实也包括的滚动可视区域,但大部分情况下,大家说的懒加载都是只可视区域的图片懒加载,所以就拿出来说了

3)、可视区加载

这里也分为两种情况
1、页面滚动的时候计算图片的位置与滚动的位置
2、通过新的API: IntersectionObserver API(可以自动"观察"元素是否可见)Intersection Observer API - Web API 接口 | MDN

4、预加载

提前加载图片,当用户需要查看时可直接从本地缓存中渲染

加载方式目前主要有两种

1)、CSS预加载

先通过CSS将图片加载到不可见区域

#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; }

待到满足触发条件后,再通过JS渲染

2)、JS预加载

通过new Image()把图片下载到本地,让浏览器缓存起来,设置其src来实现加载,再使用onload方法回调加载完成事件

5、两者对比

其实两者的概念是相反的
一个是延迟加载,一个是提前加载
一个是减低服务器压力,一个是增加服务器压力(换取用户体验)

二、懒加载具体实现代码分析

1、核心原理

将非首屏的图片的src属性设置一个默认值,监听事件scroll resize``orientationchange,判断元素进入视口viewport时则把真实地址赋予到src上

2、img标签自定义属性相关

<img class="lazy" src="[占位图]" data-src="[真实url地址]" data-srcset="[不同屏幕密度下,不同的url地址]" alt="I'm an image!">

如上,data-*属于自定义属性, ele.dataset.* 可以读取自定义属性集合
img.srcset 属性用于设置不同屏幕密度下,image自动加载不同的图片,比如<img src="image-128.png" srcset="image-256.png 2x" />

3、判断元素进入视口viewport

1)、图片距离顶部距离 < 视窗高度 + 页面滚动高度(太LOW了~)

imgEle.offsetTop < window.innerHeight + document.body.scrollTop

2)、getBoundingClientRect (很舒服的一个API)

Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置,具体参考文档Element.getBoundingClientRect() - Web API 接口 | MDN

  function isInViewport(ele) {
    // 元素顶部 距离 视口左上角 的距离top <= 窗口高度 (反例:元素在屏幕下方的情况)
    // 元素底部 距离 视口左上角 的距离bottom > 0 (反例:元素在屏幕上方的情况)
    // 元素display样式不为none
    const notBelow = ele.getBoundingClientRect().top <= window.innerHeight ? true : false;
    const notAbove = ele.getBoundingClientRect().bottom >= 0 ? true : false;
    const visable = getComputedStyle(ele).display !== "none" ? true : false;
    return notBelow && notAbove && visable ? true : false;
  }

3)、Intersection Observer(存在兼容性问题,但帅啊)

由于兼容性问题,暂时不写,具体可参考文档
Intersection Observer - Web API 接口 | MDN

【更新】可参考这篇文章Intersection Observer + Vue指令 优雅实现图片懒加载

3、具体实现

1)、适合简单的HTML文件或者服务端直出的首页

注意DOMContentLoaded,在DOM解析完之后立马执行,不适合前后端分离的单页应用,因为SPA应用一般来说图片数据是异步请求的,在DOMContentLoaded的时候,页面上未必完全解析完JS和CSS,这时候let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));拿到的不是真正首屏的所有图片标签

document.addEventListener("DOMContentLoaded", () => {
  // 获取所有class为lazy的img标签
  let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
  // 这个active是节流throttle所用的标志位,这里用到了闭包知识
  let active = false;

  const lazyLoad = () => {
    // throttle相关:200ms内只会执行一次lazyLoad方法
    if (active) return;
    active = true;

    setTimeout(() => {
      lazyImages.forEach(lazyImage => {
        // 判断元素是否进入viewport
        if (isInViewport(lazyImage)) {
          // <img class="lazy" src="[占位图]" data-src="[真实url地址]" data-srcset="[不同屏幕密度下,不同的url地址]" alt="I'm an image!">
          // ele.dataset.* 可以读取自定义属性集合,比如data-*
          // img.srcset 属性用于设置不同屏幕密度下,image自动加载不同的图片  比如<img src="image-128.png" srcset="image-256.png 2x" />
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          // 删除class  防止下次重复查找到改img标签
          lazyImage.classList.remove("lazy");
        }
        // 更新lazyImages数组,把还没处理过的元素拿出来
        lazyImages = lazyImages.filter(image => {
          return image !== lazyImage;
        });
        // 当全部处理完了,移除监听
        if (lazyImages.length === 0) {
          document.removeEventListener("scroll", lazyLoad);
          window.removeEventListener("resize", lazyLoad);
          window.removeEventListener("orientationchange", lazyLoad);
        }
      })

      active = false;
    }, 200);
  }

  document.addEventListener("scroll", lazyLoad);
  document.addEventListener("resize", lazyLoad);
  document.addEventListener("orientationchange", lazyLoad);
})

2)、适合单页应用的写法(模拟封装vue的懒加载)

① 核心实现

  • 因为是demo,所以执行时机放到vue的全局mounted钩子里面(这样的首屏体验其实是不好的),不过足够理解就好了
  • 跟上面不同的地方:let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));的获取时机放在了定时器里面,不是一开始就拿到全局的lazyImages,而是每次刷新时才拿到还没处理过的
function LazyLoad() {
  // 这个active是节流throttle所用的标志位,这里用到了闭包知识
  let active = false;

  const lazyLoad = () => {
    // throttle相关:200ms内只会执行一次lazyLoad方法
    if (active) return;
    active = true;

    setTimeout(() => {
      // 获取所有class为lazy的img标签,这里由于之前已经把处理过的img标签的class删掉了  所以不会重复查找
      let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

      lazyImages.forEach(lazyImage => {
        // 判断元素是否进入viewport
        if (isInViewport(lazyImage)) {
          // <img class="lazy" src="[占位图]" data-src="[真实url地址]" data-srcset="[不同屏幕密度下,不同的url地址]" alt="I'm an image!">
          // ele.dataset.* 可以读取自定义属性集合,比如data-*
          // img.srcset 属性用于设置不同屏幕密度下,image自动加载不同的图片  比如<img src="image-128.png" srcset="image-256.png 2x" />
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          // 删除class  防止下次重复查找到改img标签
          lazyImage.classList.remove("lazy");
        }

        // 当全部处理完了,移除监听
        if (lazyImages.length === 0) {
          document.removeEventListener("scroll", lazyLoad);
          window.removeEventListener("resize", lazyLoad);
          window.removeEventListener("orientationchange", lazyLoad);
        }
      })

      active = false;
    }, 200);
  }

  document.addEventListener("scroll", lazyLoad);
  document.addEventListener("resize", lazyLoad);
  document.addEventListener("orientationchange", lazyLoad);
}

② 在全局中的mounted钩子中执行

const vm = new Vue({
  el: '.wrap',
  store,
  mounted: function () {
    LazyLoad();
  }
});

③ 封装 img-lazy组件

<template>
  <img :class="['lazy', className]" :src="defaultImg" :data-src="url" :data-srcset="`${url} 1x`" alt="fordeal">
</template>

<script>
  export default {
    props: {
      url: {
        type: String
      },
      defaultImg: {
        type: String,
        default: [默认图片]
      className: {
        type: String,
        default: ''
      }
    }
  }
</script>

④ 使用

<img-lazy className="image" :url="item.display_image" />
@amandakelake amandakelake added the Web 网络、性能、优化类 label Apr 21, 2018
@amandakelake amandakelake changed the title 【性能优化】图片加载:懒加载与预加载 【性能优化】手把手实现图片懒加载+Vue封装 Oct 14, 2018
@ghost
Copy link

ghost commented May 21, 2019

const notAbove = ele.getBoundingClientRect().bottom >= 0 ? true : false;
直接写出 const notAbove = ele.getBoundingClientRect().bottom >= 0;
就好了

@JustDoIt521
Copy link

老哥稳!!!

@flyher
Copy link

flyher commented Oct 31, 2023

5年过去了,主流浏览器已经能很好的支持 Intersection Observer 了。

另外懒加载的同时可以捕获一下img的异常,提升网络不佳下的友好度。🤣

Intersection Observer - Web API 接口 | MDN

Javascript catch image load error

@ciloi
Copy link

ciloi commented Oct 31, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Web 网络、性能、优化类
Projects
None yet
Development

No branches or pull requests

4 participants