原来懒加载有这些玩法,你确定不看看?

lxf2023-02-16 15:48:50

本文为稀土AdminJS技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本文相关demo

本文相关仓库

前言

hello,小伙伴们好呀,我是小羽同学~

性能优化系列文章又更新啦,这次主要是和小伙伴们来介绍一下懒加载以及相关的玩法。

懒加载是前端性能优化中很重要的一环,可以有效地降低首页的白屏时间,并且可以广泛地应用到小伙伴们的项目中。

提到懒加载,前端的小伙伴们通常的第一想法就是图片的懒加载啦。

哈哈哈,其实懒加载并不止局限于图片懒加载,它也是有很多的应用场景

别着急,咱们一个一个的来介绍。

原来懒加载有这些玩法,你确定不看看?

路由懒加载

使用过react/vue框架的小伙伴们,或多或少都有使用过这个东西吧?

特别是使用了vite的小伙伴们,如果不做路由的懒加载,按vite的逻辑,会将每一个tsxless文件都加载出来,从而导致第一次加载的时间变得超级久的。

路由懒加载的作用就是将咱们的路由模块,剥离出来成为一个个单独的jscss文件,当你需要使用到他的时候,再将相关的文件加载并渲染出来。

这里以react为例子,主要是使用了react中的lazySuspense

lazy和Suspense配合使用,可以显著减少主包的体积,加快加载速度,从而提升用户体验。当路由切换的时候才会加载lazy中的代码。而代码的加载是一个异步的过程,所以当代码没有完成加载的时候则会显示fallback中的内容,一般是一个loading组件,用于告诉用户正在加载中。当加载完成了,就会显示lazy的内容。

import React, { lazy, Suspense } from 'react';
import { useRoutes, Navigate } from 'react-router-dom';
​
const LazyBrowser = lazy(() => import('@/pages/LazyBrowser'));
const LazyIntersectionObserver = lazy(
  () => import('@/pages/LazyIntersectionObserver'),
);
const LazyScroll = lazy(() => import('@/pages/LazyScroll'));
​
export default function Router() {
  let element = useRoutes([
    {
      path: '/lazy-browser',
      element: <LazyBrowser />,
      children: [],
    },
    {
      path: '/lazy-intersection-observer',
      element: <LazyIntersectionObserver />,
      children: [],
    },
    {
      path: '/lazy-scroll',
      element: <LazyScroll />,
      children: [],
    }
  ]);
​
  return <Suspense fallback={<div>loading...</div>}>{element}</Suspense>;
}

如下图所示,咱们的网站不会一次性加载所有的文件,当点击了其他的路由才会陆续加载相关的内容

原来懒加载有这些玩法,你确定不看看?

图片懒加载

嘻嘻,图片懒加载,前端同学或多或少都会听过的东西啦。

这里小羽将会介绍三种图片懒加载的方式

原来懒加载有这些玩法,你确定不看看?

基于浏览器特性的图片懒加载

如果你不想引入任何的库,又不想写太多的代码的话,这可能是最适合你的方案了。

只需要在你的image标签中添加一个loading=“lazy”即可

import React from 'react';
import { imageList } from '@/utils/imageList';
import './index.less';
​
export default function LazyBrowser() {
  return (
    <div className='lazy-browser'>
      {imageList.map((item) => (
        <div className='image' key={item}>
          <img loading='lazy' src={item} />
        </div>
      ))}
    </div>
  );
}

原来懒加载有这些玩法,你确定不看看?

好了,最简单的实现方案已经搞定了。但是这个方案还是会还有一些缺点,比如无法设置默认的加载图片加载失败的图片。

基于滚动事件的图片懒加载

有理想的小伙们,对此又采用其他方案,从而实现了这些功能。

第一种是通过监听滚动事件,判断高度与图片的位置,从而实现图片懒加载

这里小伙伴们需要先熟悉三个滚动相关的参数:offsetTopclientHeightscrollTop

  • offsetTop: 当前元素顶部距离最近父元素顶部的距离,和有没有滚动条没有关系。
  • clientHeight:当前元素的高度,包括padding但不包括border、水平滚动条、margin的元素的高度。
  • scrollTop:代表在有滚动条时,滚动条向下滚动的距离也就是元素顶部被遮住部分的高度。在没有滚动条时scrollTop===0。

首先先将loading的图片路径赋给每个img标签中的src,将真实的图片路径赋值到data-src上。

然后就是监听滚动事件,当前元素距离顶部的高度-clientHeight<=0的时候,即说明已经进入可视区域了,这时候咱们就会将img标签中的src路径修改为data-src中的真实图片路径。

import React, { useEffect, useRef } from 'react';
import { imageList } from '@/utils/imageList';
import './index.less';
​
const loadingPath = location.href + '/images/loading.gif';
export default function LazyScroll() {
  const domRef = useRef([]);
  const lazyScrollRef = useRef<HTMLDivElement>(null);
​
  useEffect(() => {
    getTop();
    lazyScrollRef.current.addEventListener('scroll', getTop);
    return () => {
      if (lazyScrollRef.current) {
        lazyScrollRef.current.removeEventListener('scroll', getTop);
      }
    };
  }, []);
​
  const getTop = () => {
    // 当前视窗的可视区域
    let clientHeight = lazyScrollRef.current.clientHeight;
    let len = domRef.current.length;
    for (let i = 0; i < len; i++) {
      // 元素距离页面顶部的距离
      let { top } = domRef.current[i].getBoundingClientRect();
      // 当图片减去可视区域高度小于等于0的时候,将data-src的值赋值给src
      if (top - clientHeight <= 0) {
        if (domRef.current[i].src === loadingPath) {
          domRef.current[i].src = domRef.current[i].dataset.src;
        }
      }
    }
  };
  return (
    <div className='lazy-scroll' ref={lazyScrollRef}>
      {imageList.map((item, index) => (
        <img
          className='image'
          key={item}
          ref={(e) => (domRef.current[index] = e)}
          data-src={item}
          src={loadingPath}
        />
      ))}
    </div>
  );
}
​

如下图所示,当图片不在可视区域内的时候就会显示咱们的loading图片,当到达咱们的可视区域就会加载真正的图片并且显示出来,从而达到省流的效果。

原来懒加载有这些玩法,你确定不看看?

基于intersectionObserver的图片懒加载

咱们可以通过监听scroll事件,判断图片是否在可视区域的方式外,从而实现图片懒加载。

但是这样子做的话,其实咱们是饶了一大圈来实现图片懒加载,那有没有什么东西可以直接判断图片是否在可是区域内的呢?

答案就是intersectionObserver

但是这个api在旧的浏览器上有一定的兼容性问题,如can i use中所示,如果需要兼容ie浏览器的小伙伴可以移步了,如果只需要兼容新版本的浏览器的小伙伴可以放心食用。

原来懒加载有这些玩法,你确定不看看?

ok,那么咱们就基于intersectionObserver简单的封装一个自定义的hooks吧,这个hooks的作用主要是会监听咱们的dom节点,如果未在可视区域的时候会返回false,如果在可视区域则会返回true,并且当第一次出现在可视区域的时候会清除监听,然后在销毁的时候也是需要记得清除一下监听哦。

import { useState, useEffect, useRef, useMemo } from 'react';
​
const useIntersectionObserver = (domRef: any) => {
  const [visible, setVisible] = useState(false);
  const intersectionObserver = useMemo(
    () =>
      new IntersectionObserver(
        (
          entries: IntersectionObserverEntry[],
          observer: IntersectionObserver,
        ) => {
          entries.map((item) => {
            if (item.isIntersecting) {
              setVisible(true);
              observer.disconnect();
            }
          });
        },
      ),
    [],
  );
​
  useEffect(() => {
    if (domRef.current) {
      intersectionObserver.observe(domRef.current);
    }
  }, [domRef.current]);
​
  useEffect(() => {
    return () => {
      // 清除监听
      intersectionObserver.disconnect();
    };
  }, []);
​
  return visible;
};
​
export default useIntersectionObserver;
​

当咱们把这个hooks封装好了,你会发现原来图片懒加载如此简单。只需要往useIntersectionObserver中传入你的dom节点,根据返回值是false 或者 true,分别显示loading和真实的图片即可。

实现的效果和上个方案一致,就不单独截图啦。使用方式如下:

import React, { useRef } from 'react';
import { imageList } from '@/utils/imageList';
import useIntersectionObserver from '@/hooks/useIntersectionObserver';
import './index.less';
​
const loadingPath = location.origin + '/images/loading.gif';
​
const Item = ({ url }) => {
  const itemRef = useRef<HTMLDivElement>();
  const visible = useIntersectionObserver(itemRef);
  return (
    <div className='image' ref={itemRef}>
      {visible ? <img src={url} /> : <img src={loadingPath} />}
    </div>
  );
};
​
export default function LazyIntersecctionObserver() {
  return (
    <div className='lazy-intersection-observer'>
      {imageList.map((item) => (
        <Item url={item} key={item} />
      ))}
    </div>
  );
}

模块懒加载

emmm,这个目前的话应该是暂时没有这样的一个定义的,是小羽基于应用场景这样子称呼。

小伙伴在工作中有没有遇到那种一个页面中有很多个单独的模块,然后每个模块都会有自己相关的一些渲染或者请求的?如果咱们在一开始就将这些模块渲染出来,首先会消耗大量的cpu性能导致页面初始化的时候会存在卡顿的问题,其次如果这些单独的模块涉及到了相关的请求,那么它又会消耗用户的流量。总的来说,就是对用户的体验可能不会很好。

因此,针对这种场景,咱们使用模块的懒加载

其实这里使用的模块懒加载,其实就是咱们基于intersectionObserver图片懒加载的延伸使用方案。

小伙伴们仔细想一下,咱们的useIntersectionObserver这个自定义的hooks里做了些什么?

这个hooks会监听咱们传入的dom,然后当这个dom元素在可视区域的时候会返回true。如果咱们在true的时候将图片替换成咱们相关模块,这不就是模块化的懒加载了吗?

使用的方式一致

import React, { useEffect, useState, useRef } from 'react';
import useIntersectionObserver from '@/hooks/useIntersectionObserver';
import axios from 'axios';
import { Spin } from 'antd';
​
export default function LazyModuleItem() {
  const itemRef = useRef(null);
  const visible = useIntersectionObserver(itemRef);
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState('');
  const init = () => {
    setLoading(true);
    axios
      .get('https://api.uomg.com/api/comments.163?format=text')
      .then((res: any) => {
        setLoading(false);
        console.log(res);
        setData(res.data);
      });
  };
  useEffect(() => {
    if (visible) {
      init();
    }
  }, [visible]);
  return (
    <div ref={itemRef}>
      {!visible || loading ? <Spin /> : <div>{data}</div>}
    </div>
  );
}
​

实现效果如下图,咱们可以发现,在咱们不断滚动,使得模块进入咱们的视野,此时才会发起新的api请求,这样子可有有效的减少咱们服务端的并发压力,以及首屏的渲染压力,即可以减少首页白屏的时间。

原来懒加载有这些玩法,你确定不看看?

小结

本文小羽和小伙伴们介绍性能优化中的懒加载,包括了三种懒加载的方式:路由懒加载图片懒加载模块懒加载。并且都分别实现了相关的demo。希望可以在小伙伴们日常的开发中,可以提供到一些帮助。

如果看这篇文章后,感觉有收获的小伙伴们可以点赞+收藏哦~

原来懒加载有这些玩法,你确定不看看?

如果想和小羽交流技术可以加下wx,也欢迎小伙伴们来和小羽唠唠嗑,嘿嘿~