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>; }
方案优势
- 服务器端输出全量HTML,完全满足SEO需求;
- 客户端初始仅保留激活标签的有效内容,减少DOM节点数量和渲染开销;
- 直接复用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 } }; }
方案优势
- SSR输出全量表格内容,搜索引擎可抓取完整数据;
- 客户端仅维护少量可视DOM节点,大幅提升滚动性能;
- 复用SSR生成的DOM,无需重新请求数据或渲染组件,性能开销极低。
内容的提问来源于stack exchange,提问作者shahrooz bazrafshan
相关产品推荐
相关产品推荐

