React-lazyload
手写 react-lazyload,实现懒加载,图片懒加载。
使用场景
长列表渲染优化
- 社交媒体的信息流
- 商品列表
- 评论列表
图片资源懒加载
- 图片画廊
- 商品详情页
- Banner 轮播图
组件按需加载
- 复杂页面的次要内容
- 模态框/弹窗组件
- 折叠面板内容
实现原理
监听方案
- Intersection Observer API (推荐)
- scroll 事件 + getBoundingClientRect()
使用
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>
);
};
性能优化建议
- 设置适当的
rootMargin
,提前加载 - 合理设置占位高度,避免布局抖动
- 配合
React.memo
减少不必要的重渲染 - 大列表考虑配合虚拟滚动使用
React.lazy vs 自定义 LazyLoad 组件
功能定位
- React.lazy: 用于代码分割,动态导入组件
- LazyLoad: 用于视口内按需渲染组件/内容
实现原理
- React.lazy: 基于动态 import(),拆分打包文件
- LazyLoad: 基于 IntersectionObserver,监听元素可见性
使用场景
- React.lazy:
tsxconst OtherComponent = React.lazy(() => import("./OtherComponent")); function App() { return ( <Suspense fallback={<Loading />}> <OtherComponent /> </Suspense> ); }
- LazyLoad:
tsxfunction App() { return ( <LazyLoad placeholder={<Loading />}> <HeavyComponent /> </LazyLoad> ); }
性能优化点
- React.lazy: 减小初始包体积,按需加载代码
- LazyLoad: 减少初始渲染内容,优化内存占用
加载时机
- React.lazy: 路由切换/条件渲染时
- LazyLoad: 滚动或出现到可视区域时