You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确优化React中含大量内容的可折叠卡片渲染耗时?

问题

假设当前DOM中挂载了若干卡片,所有卡片初始均处于collapsed(折叠)状态。点击某张卡片的click to expand按钮时,才会渲染该卡片的内容。当前存在的问题是:如果卡片包含大量内容,加载过程会耗时数秒。

在实际应用中,用户点击卡片后,该卡片的状态会被设置为expanded(展开),所有与该卡片关联的实体内容都会在卡片区域内渲染;但如果关联实体数量较多,加载过程就会产生明显等待。

目前考虑过的修复思路:

  • 从Redux状态中移除全量实体数据,初始仅渲染每个可用card_id对应的卡片基础框架,最多只渲染卡片标题
  • 当用户点击某张卡片时,再向后端发起请求拉取所有关联实体数据
  • 进一步将请求分块:不通过单次请求拉取全量内容/实体,而是拆分为2-4次请求,加载过程中展示spinner(加载动画)或loading提示文本,直到所有实体数据成功获取完成

但即使是仅渲染虚拟测试内容的示例,渲染过程也需要数秒时间,示例代码如下:

import * as React from "react";
import styles from "./styles";

export default function App() {
  const [isExpanded, setIsExpanded] = React.useState(false);

  const handleExpand = React.useCallback(
    (isExpanded) => () => {
      setIsExpanded(!isExpanded);
    },
    []
  );

  return (
    <div style={styles.container}>
      <div style={styles.listContainer}>
        <div
          style={styles.button}
          onClick={handleExpand(isExpanded)}
        >
          click to {isExpanded ? "collapse" : "expand"}
        </div>
        <div style={styles.list}>
          {isExpanded &&
            Array.from({ length: 5000 }, (_, index) => (
              <div style={styles.content}>
                <div>some header {index + 1}</div>
                <input key={index} value={`some value ${index + 1}`} />
                <div>some content heading {index + 1}</div>
                <input value={`some other field ${index + 1}`} />
                <input
                  style={{ marginBottom: "1rem" }}
                  value={`and another field ${index + 1}`}
                />
              </div>
            ))}
        </div>
      </div>
    </div>
  );
}

注:已调研过虚拟滚动方案,认为其并不适用于当前场景:业务场景并非标准列表,只是若干卡片,当卡片内容数量较多时会出现离屏渲染的情况。


回答

卡顿的根本原因不是接口请求慢,而是一次性生成、挂载过量DOM节点阻塞了浏览器主线程,上述纯本地渲染Demo已经可以验证这点:哪怕所有数据都存在内存中,一次性渲染5000条包含多节点的内容块,浏览器需要完成节点创建、样式计算、布局、绘制全流程,必然会出现数秒的无响应。
之前考虑的分块拉取接口方案只能解决数据传输的等待问题,解决不了前端一次性渲染大量节点的卡顿,哪怕分4次请求,等所有请求返回后一次性渲染全量内容,还是会卡。

可落地优化方案(按改造成本从低到高排序)

  • 时间分片增量渲染
    不要在点击展开后一次性把全量内容推入React渲染队列,而是把内容拆成多个小批次,利用浏览器空闲时间逐批渲染:
    1. 点击展开后立刻渲染首屏可容纳的内容量(通常20-50条),让用户第一时间看到内容反馈
    2. 用requestAnimationFrame或requestIdleCallback逐帧追加渲染下一批内容,每批控制在20-50条,直到所有内容渲染完成
      核心实现参考:
    // 单卡片组件内逻辑
    const BATCH_SIZE = 30;
    const [isExpanded, setIsExpanded] = useState(false);
    const [renderedCount, setRenderedCount] = useState(0);
    const totalCount = cardData.entities.length; // 卡片关联内容总条数
    
    const handleToggleExpand = () => {
      const nextExpanded = !isExpanded;
      setIsExpanded(nextExpanded);
      if (!nextExpanded) {
        setRenderedCount(0);
        return;
      }
      // 展开时启动增量渲染
      const renderNextBatch = () => {
        setRenderedCount(prev => {
          const nextCount = Math.min(prev + BATCH_SIZE, totalCount);
          if (nextCount < totalCount) {
            requestAnimationFrame(renderNextBatch);
          }
          return nextCount;
        });
      };
      // 首屏立刻渲染第一批
      setRenderedCount(BATCH_SIZE);
      requestAnimationFrame(renderNextBatch);
    };
    
    // 渲染逻辑:只渲染已完成调度的内容块
    return (
      <div className="card">
        <div className="card-header" onClick={handleToggleExpand}>
          click to {isExpanded ? "collapse" : "expand"}
        </div>
        <div className="card-content">
          {cardData.entities.slice(0, renderedCount).map((entity, idx) => (
            <div className="content-item" key={entity.id}>
              {/* 单条内容结构 */}
            </div>
          ))}
        </div>
      </div>
    );
    
    这个方案几乎不需要改动现有业务结构,不需要引入额外依赖,用户点击后不会出现数秒无响应的情况,内容会平滑逐步加载,感知体验远好于全局loading等待。
  • 离屏内容懒挂载
    针对内容量特别大的卡片,不需要等所有内容都渲染到DOM中,用浏览器原生IntersectionObserver监听内容块位置,只有当内容块滚动到距离视口一定阈值(比如200px)时才挂载真实DOM,离屏位置只渲染等高占位容器即可。
    这个方案不需要把内容改成标准列表结构,完全适配独立卡片的场景,比虚拟滚动的适配成本低80%以上,能把任意时刻页面上的DOM节点总量控制在几百个以内,从根源上解决渲染卡顿。
  • 降低单内容块渲染开销
    • 给独立内容项套React.memo做浅比较缓存,避免卡片状态变化时所有已渲染内容项重复重渲染
    • 非编辑状态下的表单输入控件直接用普通文本节点展示,用户触发编辑操作时再替换为真实input/select控件,能减少30%-50%的DOM节点数量
    • 避免在内容项渲染函数中创建新的对象/函数作为props,减少不必要的重渲染触发

补充说明

此前调研的虚拟滚动方案并非完全不适用,只是不需要给整页卡片列表套虚拟滚动,仅针对展开后内容量超过千条的单个卡片,在卡片内容区域内部应用虚拟滚动即可,只是该方案改造成本高于上述两个方案,内容量在万条以内时优先用前两个方案即可。


内容的提问来源于stack exchange,提问作者Mike K

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 06:42:37