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. 清理机制:元素卸载时自动移
相关推荐
小二·37 分钟前
Python Web 开发进阶实战:无障碍深度集成 —— 构建真正包容的 Flask + Vue 应用
前端·python·flask
niucloud-admin8 小时前
web 端前端
前端
胖者是谁12 小时前
EasyPlayerPro的使用方法
前端·javascript·css
EndingCoder12 小时前
索引类型和 keyof 操作符
linux·运维·前端·javascript·ubuntu·typescript
liux352812 小时前
Web集群管理实战指南:从架构到运维
运维·前端·架构
沛沛老爹12 小时前
Web转AI架构篇 Agent Skills vs MCP:工具箱与标准接口的本质区别
java·开发语言·前端·人工智能·架构·企业开发
小光学长13 小时前
基于Web的长江游轮公共服务系统j225o57w(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库
Joe55614 小时前
vue2 + antDesign 下拉框限制只能选择2个
服务器·前端·javascript
ChangYan.14 小时前
monorepo 多包管理识别不到新增模块,解决办法
前端·chrome
Jinuss14 小时前
React元素创建介绍
前端·react.js