Intersection Observer 的实战方案

Intersection Observer 的实战方案

  • 优化目标:长列表首屏渲染性能优化

1- 用户场景

markdown 复制代码
用户行为路径:
1. 打开 AI 工具箱页面
2. 浏览热门工具(首屏)
3. 滚动或点击分类导航查看特定分类工具
4. 点击工具卡片查看详情

关键问题:用户首次进入页面时,大部分工具并不在视口内,但初版实现会一次性渲染所有工具。


初版实现与性能瓶颈

2.1 初版代码结构

tsx 复制代码
// AIToolboxContent.tsx - 初版实现
export default function AIToolboxContent() {
    const [allToolsCategories, setAllToolsCategories] = useState<CategoryWithTools[]>([]);
    
    // 获取所有工具数据
    useEffect(() => {
        const fetchAllTools = async () => {
            const response = await toolApi.getAllTools();
            if (response.code === 200 && response.data) {
                setAllToolsCategories(response.data);
            }
        };
        fetchAllTools();
    }, []);

    return (
        <div className={styles.content}>
            {/* 侧边栏 */}
            <ToggleButton {...props} />
            
            {/* 主内容区 */}
            <div ref={rightContainerRef} className={styles.right}>
                {/* 热门工具 */}
                <ToolList
                    title="热门工具"
                    icon={getCategoryIcon('hot')}
                    tools={featuredTools}
                />

                {/* ! 一次性渲染所有分类 */}
                {allToolsCategories.map((category) => {
                    const categoryTools = category.tools.map((tool) => ({
                        id: tool.id,
                        title: tool.toolName,
                        description: tool.toolDesc,
                        iconUrl: tool.iconUrl,
                    }));

                    return (
                        <ToolList
                            key={category.categoryId}
                            title={category.categoryName}
                            icon={getCategoryIcon(category.categorySlug)}
                            tools={categoryTools}
                        />
                    );
                })}
            </div>
        </div>
    );
}

2.2 组件层级结构

scss 复制代码
AIToolboxContent (父组件)
├── ToggleButton (侧边栏)
└── RightContainer (主内容区)
    ├── ToolList (热门工具)
    │   └── ToolCard × 20
    ├── ToolList (AI 写作)
    │   └── ToolCard × 35
    ├── ToolList (图像处理)
    │   └── ToolCard × 42
    ├── ToolList (视频生成)
    │   └── ToolCard × 28
    └── ... (共 15+ 个 ToolList)
        └── ToolCard × N 

2.3 性能瓶颈分析

问题 1:首屏渲染时间长
diff 复制代码
初版性能指标(1000+ 工具):
- 首次渲染时间:2.5s - 3.5s
- DOM 节点数:3000+
- 内存占用:~80MB
- FCP (First Contentful Paint):1.8s
- TTI (Time to Interactive):3.2s
问题 2:无效渲染

问题描述

  • 用户首屏只能看到 "热门工具" 和前 1-2 个分类(约 50 个工具)
  • 但浏览器需要渲染全部 1000+ 个工具卡片
  • 剩余 950+ 个不在视口内的组件完全是无效渲染
问题 3:内存浪费
javascript 复制代码
// 每个 ToolCard 组件的内存占用估算
单个 ToolCard 组件:
- React Fiber 节点:~1KB
- DOM 节点:~2KB
- 图片资源:~50KB (懒加载前)
- 事件监听:~0.5KB

1000 个 ToolCard 总计:
- React 内存:~1MB
- DOM 内存:~2MB
- 图片内存:~50MB (未优化)
- 总计:~53MB (仅工具卡片部分)
问题 4:滚动性能差
  • 初次渲染后,滚动时会出现轻微卡顿
  • 长列表导致浏览器重排(reflow)计算复杂

3.1 方案设计

核心思路
markdown 复制代码
1. 初始状态:只渲染首屏内容(热门工具 + 占位符)
2. 监听滚动:使用 Intersection Observer 监听每个分类容器
3. 触发加载:当分类容器即将进入视口时(提前 200px)
4. 渲染内容:替换占位符为实际的 ToolList 组件
5. 保持状态:已加载的分类保持渲染状态
数据流设计
typescript 复制代码
// 状态设计
interface State {
  // 所有工具数据(一次性获取)
  allToolsCategories: CategoryWithTools[];
  
  // 记录哪些分类已经可见
  visibleCategories: Set<string>; // ['hot', 'writing', 'image']
}

// 渲染逻辑
function render() {
  allToolsCategories.map(category => {
    if (visibleCategories.has(category.slug)) {
      // 已可见 => 渲染完整 ToolList
      return <ToolList {...category} />;
    } else {
      // 未可见 => 渲染占位符
      return <Placeholder />;
    }
  });
}
架构设计
sql 复制代码
┌─────────────────────────────────────────┐
│        AIToolboxContent (Container)      │
│  ┌─────────────────────────────────┐   │
│  │  Intersection Observer Setup     │   │
│  │  - rootMargin: 200px            │   │
│  │  - threshold: 0.01               │   │
│  └─────────────────────────────────┘   │
│                                          │
│  ┌─────────────────────────────────┐   │
│  │  State Management               │   │
│  │  - visibleCategories: Set       │   │
│  │  - toolListRefs: Map            │   │
│  └─────────────────────────────────┘   │
│                                          │
│  ┌─────────────────────────────────┐   │
│  │  Render Logic                   │   │
│  │  - Conditional Rendering         │   │
│  │  - Placeholder / ToolList        │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

        ↓ Scroll Event ↓

┌─────────────────────────────────────────┐
│      IntersectionObserver Callback       │
│  - Detect Intersection                   │
│  - Update visibleCategories             │
│  - Trigger Re-render                    │
└─────────────────────────────────────────┘

核心实现

4.1 状态管理

typescript 复制代码
export default function AIToolboxContent() {
    // 所有工具数据(按分类组织)
    const [allToolsCategories, setAllToolsCategories] = useState<CategoryWithTools[]>([]);
    
    // 关键状态:记录哪些分类已经可见(进入过视口)
    // 初始值包含 'hot',确保热门工具立即渲染
    const [visibleCategories, setVisibleCategories] = useState<Set<string>>(
        new Set(['hot'])
    );

    // 右侧滚动容器的引用(作为 Intersection Observer 的 root)
    const rightContainerRef = useRef<HTMLDivElement>(null);
    
    // 存储每个 ToolList 的 DOM 引用(用于滚动和观察)
    const toolListRefs = useRef<Map<string, HTMLDivElement>>(new Map());
    
    // Intersection Observer 实例
    const observerRef = useRef<IntersectionObserver | null>(null);
}

5.2 Ref 管理系统

typescript 复制代码
// 设置 ToolList ref 的辅助函数
// 使用 useCallback 避免每次渲染都创建新函数
const setToolListRef = useCallback((slug: string) => (element: HTMLDivElement | null) => {
    if (element) {
        // 元素挂载:保存引用并开始观察
        toolListRefs.current.set(slug, element);
        
        //  关键:立即将元素添加到 Observer 监听列表
        if (observerRef.current) {
            observerRef.current.observe(element);
        }
    } else {
        // 元素卸载:清理引用和监听
        const oldElement = toolListRefs.current.get(slug);
        if (oldElement && observerRef.current) {
            observerRef.current.unobserve(oldElement);
        }
        toolListRefs.current.delete(slug);
    }
}, []);

设计要点

  1. 闭包捕获 :使用柯里化(Currying)传递 slug 参数
  2. 自动观察:元素挂载时自动添加到 Observer
  3. 清理机制:元素卸载时自动移
相关推荐
青莲8432 小时前
Kotlin Flow 深度探索与实践指南——中部:实战与应用篇
android·前端
cindershade2 小时前
事件委托(Event Delegation)的原理
前端
开发者小天2 小时前
React中useMemo的使用
前端·javascript·react.js
1024肥宅2 小时前
JS复杂去重一定要先排序吗?深度解析与性能对比
前端·javascript·面试
im_AMBER3 小时前
weather-app开发手记 04 AntDesign组件库使用解析 | 项目设计困惑
开发语言·前端·javascript·笔记·学习·react.js
用泥种荷花3 小时前
VueCropper加载OBS图片跨域问题
前端
董世昌413 小时前
什么是事件冒泡?如何阻止事件冒泡和浏览器默认事件?
java·前端
Bigger3 小时前
在 React 里优雅地 “隐藏 iframe 滚动条”
前端·css·react.js
小沐°3 小时前
vue3-ElementPlus出现Uncaught (in promise) cancel 报错
前端·javascript·vue.js