Skip to content

组件懒加载

组件懒加载 是什么?

  • 组件懒加载,又称延迟加载或按需加载,是一种优化前端性能的技术手段,主要应用于 Web 开发中。
  • 在 Vue.js 等现代前端框架中,组件懒加载指的是根据用户交互或者视口可见性动态地、按需地加载和渲染组件的策略。

组件懒加载 解决了什么问题

  1. 性能优化:避免一次性加载页面中的所有组件及其相关资源(如图片、字体、脚本等),从而降低首屏加载时间和总体页面体积
  2. 资源利用效率提升:仅在网络请求和渲染那些对当前用户体验有直接影响的组件,使得网络和 CPU 资源得到更高效的分配与利用
  3. 改善用户体验:通过缩短用户等待时间,特别是对于包含大量内容和复杂组件的单页应用,可实现更快的内容展现,减少用户感知的页面空白或加载等待时间

组件懒加载 实现思路

  1. 事件监听或 API 调用

    • 使用 IntersectionObserver API:监视元素是否进入视口。当组件即将进入视口时,通过回调函数获取通知,从而触发组件的加载操作
    • 滚动事件监听:不支持 IntersectionObserver 的老旧浏览器,可以监听滚动事件并结合计算当前滚动位置与目标组件的位置关系来判断是否应该加载组件
  2. 定义加载状态和切换逻辑

    • 在组件内部,通常会维护一个加载状态标志(如 isLoaded 或 isLoading),初始化为未加载状态。当满足加载条件时,将该状态切换为加载中,同时开始异步加载组件所需资源
  3. 过渡动画支持:

    • 提升用户体验,可以在组件加载前后添加过渡效果。Vue 的 transition 或 transition-group 组件包裹待加载内容,

实现示例:

vue
<template>
  <transition-group
    :tag="tagName"
    name="lazy-component"
    style="position: relative"
    @before-enter="(el) => $emit('before-enter', el)"
    @before-leave="(el) => $emit('before-leave', el)"
    @after-enter="(el) => $emit('after-enter', el)"
    @after-leave="(el) => $emit('after-leave', el)"
  >
    <div v-if="isInit" key="component">
      <slot :loading="loading"></slot>
    </div>

    <div v-else-if="$slots.skeleton" key="skeleton">
      <slot name="skeleton"></slot>
    </div>

    <div v-else key="loading"></div>
  </transition-group>
</template>

<script>
export default {
  name: "VueLazyComponent",

  props: {
    timeout: {
      type: Number,
    },
    tagName: {
      type: String,
      default: "div",
    },
    viewport: {
      type: typeof window !== "undefined" ? window.HTMLElement : Object,
      default: () => null,
    },
    threshold: {
      type: String,
      default: "0px",
    },
    direction: {
      type: String,
      default: "vertical",
    },
    maxWaitingTime: {
      type: Number,
      default: 50,
    },
  },

  data() {
    return {
      isInit: false,
      timer: null,
      io: null,
      loading: false,
    };
  },

  created() {
    // 如果指定timeout则无论可见与否都是在timeout之后初始化
    if (this.timeout) {
      this.timer = setTimeout(() => {
        this.init();
      }, this.timeout);
    }
  },

  mounted() {
    if (!this.timeout) {
      // 根据滚动方向来构造视口外边距,用于提前加载
      let rootMargin;
      switch (this.direction) {
        case "vertical":
          rootMargin = `${this.threshold} 0px`;
          break;
        case "horizontal":
          rootMargin = `0px ${this.threshold}`;
          break;
      }

      try {
        // 观察视口与组件容器的交叉情况
        this.io = new window.IntersectionObserver(this.intersectionHandler, {
          rootMargin,
          root: this.viewport,
          threshold: [0, Number.MIN_VALUE, 0.01],
        });
        this.io.observe(this.$el);
      } catch (e) {
        this.init();
      }
    }
  },

  beforeDestroy() {
    // 在组件销毁前取消观察
    if (this.io) {
      this.io.unobserve(this.$el);
    }
  },

  methods: {
    // 交叉情况变化处理函数
    intersectionHandler(entries) {
      if (
        // 正在交叉
        entries[0].isIntersecting ||
        // 交叉率大于0
        entries[0].intersectionRatio
      ) {
        this.init();
        this.io.unobserve(this.$el);
      }
    },

    // 处理组件和骨架组件的切换
    init() {
      // 此时说明骨架组件即将被切换
      this.$emit("beforeInit");
      this.$emit("before-init");

      // 此时可以准备加载懒加载组件的资源
      this.loading = true;

      // 由于函数会在主线程中执行,加载懒加载组件非常耗时,容易卡顿
      // 所以在requestAnimationFrame回调中延后执行
      this.requestAnimationFrame(() => {
        this.isInit = true;
        this.$emit("init");
      });
    },

    requestAnimationFrame(callback) {
      // 防止等待太久没有执行回调
      // 设置最大等待时间
      setTimeout(() => {
        if (this.isInit) return;
        callback();
      }, this.maxWaitingTime);

      // 兼容不支持requestAnimationFrame 的浏览器
      return (
        window.requestAnimationFrame ||
        ((callback) => setTimeout(callback, 1000 / 60))
      )(callback);
    },
  },
};
</script>

参考文献

Released under the MIT License.