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

Next.js SSR场景下Tabs组件SEO友好懒加载实践方案

Next.js SSR 下的内容延迟渲染方案(兼顾SEO)

一、Tabs 标签页场景实现

需求分析

SSR 阶段输出完整的所有标签内容(满足SEO抓取),客户端初始仅渲染激活标签的内容,点击非激活标签时再加载对应内容。

实现思路

服务器端正常渲染所有标签内容,客户端在 hydration 完成后(通过useEffect触发),将非激活标签的内容替换为占位;点击标签时恢复原始内容或重新渲染组件,避免 hydration 不匹配问题。

代码示例

import { useState, useEffect, useRef } from 'react';

export default function SSRCompatibleTabs() {
  const [activeTab, setActiveTab] = useState(0);
  // 保存非激活标签的原始DOM内容
  const contentRefs = useRef([null, null]);

  // Hydration完成后处理非激活标签
  useEffect(() => {
    const inactiveIndex = activeTab === 0 ? 1 : 0;
    const targetEl = contentRefs.current[inactiveIndex];
    if (targetEl) {
      // 缓存原始HTML内容
      targetEl.dataset.originalContent = targetEl.innerHTML;
      // 替换为占位内容
      targetEl.innerHTML = '<div class="tab-placeholder">点击标签查看内容</div>';
    }
  }, []);

  const handleTabSwitch = (index) => {
    if (index === activeTab) return;
    
    // 恢复之前激活标签的占位状态
    const prevInactiveIndex = activeTab;
    const prevEl = contentRefs.current[prevInactiveIndex];
    if (prevEl?.dataset.originalContent) {
      prevEl.innerHTML = '<div class="tab-placeholder">点击标签查看内容</div>';
    }

    // 切换当前激活标签并恢复内容
    setActiveTab(index);
    const currEl = contentRefs.current[index];
    if (currEl?.dataset.originalContent) {
      currEl.innerHTML = currEl.dataset.originalContent;
    }
  };

  return (
    <div className="tabs-container">
      <ul className="tab-heading">
        <li 
          onClick={() => handleTabSwitch(0)} 
          className={activeTab === 0 ? 'active' : ''}
        >
          tab1
        </li>
        <li 
          onClick={() => handleTabSwitch(1)} 
          className={activeTab === 1 ? 'active' : ''}
        >
          tab2
        </li>
      </ul>
      <ul className="tab-content">
        <li 
          ref={el => contentRefs.current[0] = el}
          className={activeTab === 0 ? 'active' : ''}
        >
          <Content1 />
        </li>
        <li 
          ref={el => contentRefs.current[1] = el}
          className={activeTab === 1 ? 'active' : ''}
        >
          <Content2 />
        </li>
      </ul>
    </div>
  );
}

// 示例内容组件
function Content1() {
  return <div className="tab-content-item">Tab1 的详细内容,SSR会完整渲染这部分</div>;
}

function Content2() {
  return <div className="tab-content-item">Tab2 的详细内容,SSR会完整渲染,客户端初始为占位</div>;
}

方案优势

  1. 服务器端输出全量HTML,完全满足SEO需求;
  2. 客户端初始仅保留激活标签的有效内容,减少DOM节点数量和渲染开销;
  3. 直接复用SSR生成的DOM内容,无需重新请求数据或渲染组件。

二、大表格虚拟化场景实现

需求分析

SSR 阶段输出全量表格行(满足SEO),客户端仅渲染可视区域的行,滚动时动态加载对应行,避免大量DOM节点导致的性能问题。

实现思路

服务器端渲染完整的表格行,客户端 hydration 完成后,将所有行隐藏并缓存DOM;通过监听滚动事件计算可视区域范围,仅显示对应范围内的行,滚动时动态切换显示状态。

代码示例

import { useState, useEffect, useRef } from 'react';

export default function SSRVirtualTable({ tableData }) {
  const tableContainerRef = useRef(null);
  const rowRefs = useRef([]);
  const [visibleRowRange, setVisibleRowRange] = useState({ start: 0, end: 0 });
  const rowHeight = 42; // 固定每行高度,用于计算可视范围

  useEffect(() => {
    const container = tableContainerRef.current;
    if (!container) return;

    // 缓存所有行的DOM并隐藏
    const allRows = Array.from(container.querySelectorAll('tbody tr'));
    rowRefs.current = allRows;
    allRows.forEach(row => row.style.display = 'none');

    // 计算初始可视行范围
    const clientHeight = container.clientHeight;
    const initialStart = 0;
    const initialEnd = Math.min(Math.floor(clientHeight / rowHeight) + 1, allRows.length - 1);
    updateVisibleRows(initialStart, initialEnd);

    // 监听滚动事件更新可视行
    const handleScroll = () => {
      const scrollTop = container.scrollTop;
      const start = Math.floor(scrollTop / rowHeight);
      const end = Math.min(start + Math.floor(clientHeight / rowHeight) + 1, allRows.length - 1);
      updateVisibleRows(start, end);
    };

    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, []);

  // 更新可视行的显示状态
  const updateVisibleRows = (start, end) => {
    // 隐藏之前的可视行
    for (let i = visibleRowRange.start; i <= visibleRowRange.end; i++) {
      if (rowRefs.current[i]) rowRefs.current[i].style.display = 'none';
    }
    // 显示新的可视行
    for (let i = start; i <= end; i++) {
      if (rowRefs.current[i]) rowRefs.current[i].style.display = '';
    }
    setVisibleRowRange({ start, end });
  };

  // SSR阶段渲染全量行
  return (
    <div 
      ref={tableContainerRef}
      style={{ height: '500px', overflowY: 'auto', border: '1px solid #eee' }}
    >
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <thead>
          <tr style={{ backgroundColor: '#f5f5f5' }}>
            <th style={{ padding: '10px', border: '1px solid #eee' }}>序号</th>
            <th style={{ padding: '10px', border: '1px solid #eee' }}>内容</th>
            <th style={{ padding: '10px', border: '1px solid #eee' }}>详情</th>
          </tr>
        </thead>
        <tbody>
          {tableData.map((item, index) => (
            <tr key={index} ref={el => rowRefs.current[index] = el}>
              <td style={{ padding: '10px', border: '1px solid #eee' }}>{index + 1}</td>
              <td style={{ padding: '10px', border: '1px solid #eee' }}>{item.content}</td>
              <td style={{ padding: '10px', border: '1px solid #eee' }}>{item.detail}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// 页面中使用,通过getServerSideProps获取全量数据
export async function getServerSideProps() {
  // 模拟从数据库获取1000行数据
  const tableData = Array.from({ length: 1000 }).map((_, i) => ({
    content: `第${i+1}行内容`,
    detail: `第${i+1}行详情描述`
  }));
  return { props: { tableData } };
}

方案优势

  1. SSR输出全量表格内容,搜索引擎可抓取完整数据;
  2. 客户端仅维护少量可视DOM节点,大幅提升滚动性能;
  3. 复用SSR生成的DOM,无需重新请求数据或渲染组件,性能开销极低。

内容的提问来源于stack exchange,提问作者shahrooz bazrafshan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 02:02:02