Skip to content

React-lazyload

手写 react-lazyload,实现懒加载,图片懒加载。

使用场景

  1. 长列表渲染优化

    • 社交媒体的信息流
    • 商品列表
    • 评论列表
  2. 图片资源懒加载

    • 图片画廊
    • 商品详情页
    • Banner 轮播图
  3. 组件按需加载

    • 复杂页面的次要内容
    • 模态框/弹窗组件
    • 折叠面板内容

实现原理

  1. 监听方案

    • Intersection Observer API (推荐)
    • scroll 事件 + getBoundingClientRect()
  2. 使用 Intersection Observer API 实现

tsx
import { CSSProperties, ReactNode, useEffect, useRef, useState } from "react";

// 定义组件的props接口
interface MyLazyLoadProps {
  className?: string; // 自定义类名
  style?: CSSProperties; // 自定义样式
  placeholder?: ReactNode; // 加载占位内容
  offset?: string | number; // 视口偏移量,用于提前加载
  width?: number | string; // 容器宽度
  height?: string | number; // 容器高度
  onContentVisible?: () => void; // 内容可见时的回调
  children: ReactNode; // 需要懒加载的内容
}

const MyLazyLoad = (props: MyLazyLoadProps) => {
  // 解构props,设置默认值
  const {
    className = "",
    style,
    offset = 0,
    width,
    onContentVisible,
    placeholder,
    height,
    children,
  } = props;

  // 容器ref,用于观察元素
  const containerRef = useRef<HTMLDivElement>(null);
  // 控制内容是否显示
  const [visible, setVisible] = useState(false);
  // 存储 IntersectionObserver 实例
  const elementObserver = useRef<IntersectionObserver>();

  // 处理元素进入/离开视口的回调
  function lazyloadHandler(entries: IntersectionObserverEntry[]) {
    const [entry] = entries;
    const { isIntersecting } = entry;

    if (isIntersecting) {
      setVisible(true);
      onContentVisible?.();

      // 元素可见后取消观察
      const node = containerRef.current;
      if (node && node instanceof HTMLElement) {
        elementObserver.current?.unobserve(node);
      }
    }
  }

  useEffect(() => {
    // 配置观察器选项
    const options = {
      // 将数字类型的offset转换为像素值
      rootMargin: typeof offset == "number" ? `${offset}px` : offset || "0px",
      threshold: 0, // 元素出现在视口就触发
    };

    // 创建观察器实例
    elementObserver.current = new IntersectionObserver(
      lazyloadHandler,
      options
    );

    const node = containerRef.current;

    // 开始观察目标元素
    if (node instanceof HTMLElement) {
      elementObserver.current.observe(node);
    }

    // 清理函数:停止观察
    return () => {
      if (node && node instanceof HTMLElement) {
        elementObserver.current?.unobserve(node);
      }
    };
  }, []);

  // 合并样式
  const styles = { height, width, ...style };

  return (
    <div ref={containerRef} className={className} style={styles}>
      {visible ? children : placeholder} {/* 根据visible状态切换显示内容 */}
    </div>
  );
};

export default MyLazyLoad;

使用示例

1.基础图片懒加载

tsx
<LazyLoad placeholder={<div>loading...</div>} height={200}>
  <img src="/large-image.jpg" alt="lazy loaded" />
</LazyLoad>

2.列表懒加载

tsx
const ListExample = () => {
  const items = Array.from({ length: 100 }, (_, i) => ({
    id: i,
    title: `Item ${i}`,
  }));

  return (
    <div>
      {items.map((item) => (
        <LazyLoad
          key={item.id}
          height={100}
          offset={100} // 提前100px加载
          placeholder={<div>Loading...</div>}
        >
          <ListItem data={item} />
        </LazyLoad>
      ))}
    </div>
  );
};

3.组件懒加载

tsx
<LazyLoad
  height={300}
  onContentVisible={() => console.log("Component is visible")}
  placeholder={<div>Loading Component...</div>}
>
  <ComplexComponent />
</LazyLoad>

4.带样式的懒加载

tsx
<LazyLoad
  className="custom-lazy-container"
  style={{
    background: "#f5f5f5",
    borderRadius: "8px",
  }}
  height={200}
  width="100%"
  placeholder={
    <div className="loading-skeleton">
      <Spinner />
    </div>
  }
>
  <ContentCard />
</LazyLoad>

5.图片画廊示例

tsx
const Gallery = () => {
  const images = [
    "/image1.jpg",
    "/image2.jpg",
    "/image3.jpg",
    // ...更多图片
  ];

  return (
    <div className="gallery-grid">
      {images.map((src, index) => (
        <LazyLoad
          key={index}
          height={200}
          offset={200}
          placeholder={
            <div className="image-placeholder">
              <LoadingSpinner />
            </div>
          }
        >
          <img
            src={src}
            alt={`Gallery item ${index}`}
            className="gallery-image"
          />
        </LazyLoad>
      ))}
    </div>
  );
};

6.折叠面板示例

tsx
// 折叠面板中的懒加载
const CollapsiblePanel = () => {
  // 点击按钮之后,进行加载;点击按钮之前,只要元素不可见,就不算出现在视口中,也不会触发加载。
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle Panel</button>

      <div style={{ display: isOpen ? "block" : "none" }}>
        <LazyLoad placeholder={<div>Loading...</div>}>
          <HeavyComponent />
        </LazyLoad>
      </div>
    </div>
  );
};

性能优化建议

  1. 设置适当的 rootMargin,提前加载
  2. 合理设置占位高度,避免布局抖动
  3. 配合 React.memo 减少不必要的重渲染
  4. 大列表考虑配合虚拟滚动使用

React.lazy vs 自定义 LazyLoad 组件

  1. 功能定位

    • React.lazy: 用于代码分割,动态导入组件
    • LazyLoad: 用于视口内按需渲染组件/内容
  2. 实现原理

    • React.lazy: 基于动态 import(),拆分打包文件
    • LazyLoad: 基于 IntersectionObserver,监听元素可见性
  3. 使用场景

    • React.lazy:
    tsx
    const OtherComponent = React.lazy(() => import("./OtherComponent"));
    
    function App() {
      return (
        <Suspense fallback={<Loading />}>
          <OtherComponent />
        </Suspense>
      );
    }
    • LazyLoad:
    tsx
    function App() {
      return (
        <LazyLoad placeholder={<Loading />}>
          <HeavyComponent />
        </LazyLoad>
      );
    }
  4. 性能优化点

    • React.lazy: 减小初始包体积,按需加载代码
    • LazyLoad: 减少初始渲染内容,优化内存占用
  5. 加载时机

    • React.lazy: 路由切换/条件渲染时
    • LazyLoad: 滚动或出现到可视区域时

Crafted with care.