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

路由懒加载

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

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

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

这里以react为例子,主要是使用了react中的lazy和Suspense。

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>
  );
}

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

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

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

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

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

offsetTop:当前元素顶部距离最近父元素顶部的距离,和有没有滚动条没有关系。 clientHeight:当前元素的高度,包括padding但不包括border、水平滚动条、margin的元素的高度。 scrollTop:代表在有滚动条时,滚动条向下滚动的距离也就是元素顶部被遮住部分的高度。在没有滚动条时scrollTop===0。 首先先将loading的图片路径赋给每个img标签中的src,将真实的图片路径赋值到src上。

然后就是监听滚动事件,当前元素距离顶部的高度-clientHeight<=0的时候,即说明已经进入可视区域了,这时候咱们就会将img标签中的src路径修改为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的时候,将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)}
          src={item}
          src={loadingPath}
        />
      ))}
    </div>
  );
}

基于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>
  );
}

模块懒加载

小伙伴在工作中有没有遇到那种一个页面中有很多个单独的模块,然后每个模块都会有自己相关的一些渲染或者请求的?如果咱们在一开始就将这些模块渲染出来,首先会消耗大量的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请求,这样子可有有效的减少咱们服务端的并发压力,以及首屏的渲染压力,即可以减少首页白屏的时间。

贡献者: mankueng