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

Next 13 无限加载实现方案咨询:首屏SEO友好的滚动加载

Next.js 13 无限加载最优实现方案(兼顾SEO)

核心思路

完全契合你的需求:用Server Component完成首屏20条数据的服务端渲染(保证SEO),用Client Component处理滚动触底、加载更多的交互逻辑,两者通过数据传递实现无缝衔接。


实现步骤与代码示例

1. Server Component:获取并传递初始数据

默认情况下,App Router中的组件都是Server Component,无需额外标记。它负责在服务端拉取首屏数据,直接传递给Client Component。

// app/posts/page.js
import InfinitePostsList from './InfinitePostsList';

// 服务端获取首屏20条数据
async function fetchInitialPosts() {
  // 可替换为你的数据库查询或内部API调用
  const res = await fetch('https://your-api-domain/posts?page=1&limit=20', {
    cache: 'force-cache' // 静态场景用缓存,动态数据用no-store
  });
  return res.json();
}

export default async function PostsPage() {
  const initialPosts = await fetchInitialPosts();

  return (
    <div className="container">
      <h1>文章列表</h1>
      {/* 将初始数据传给Client Component */}
      <InfinitePostsList initialPosts={initialPosts} />
    </div>
  );
}

2. Client Component:处理无限加载逻辑

必须添加'use client'指令,负责渲染列表、监听滚动、加载后续数据。初始数据直接作为React状态的初始值,避免重复渲染。

// app/posts/InfinitePostsList.js
'use client';

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

export default function InfinitePostsList({ initialPosts }) {
  const [posts, setPosts] = useState(initialPosts);
  const [currentPage, setCurrentPage] = useState(1);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const listContainerRef = useRef(null);

  // 用Intersection Observer实现高效触底检测(替代scroll事件)
  useEffect(() => {
    if (!hasMore || isLoading) return;

    const observer = new IntersectionObserver(
      (entries) => {
        const target = entries[0];
        if (target.isIntersecting) fetchNextPage();
      },
      { root: listContainerRef.current, rootMargin: '0 0 100px 0' }
    );

    const lastItem = document.querySelector('.post-list li:last-child');
    if (lastItem) observer.observe(lastItem);

    return () => {
      if (lastItem) observer.unobserve(lastItem);
    };
  }, [isLoading, hasMore, posts.length]);

  // 加载下一页数据
  const fetchNextPage = async () => {
    setIsLoading(true);
    try {
      const nextPage = currentPage + 1;
      const res = await fetch(`https://your-api-domain/posts?page=${nextPage}&limit=20`);
      const newPosts = await res.json();

      if (newPosts.length === 0) {
        setHasMore(false);
      } else {
        setPosts(prev => [...prev, ...newPosts]);
        setCurrentPage(nextPage);
      }
    } catch (err) {
      console.error('加载失败:', err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div ref={listContainerRef} style={{ maxHeight: '85vh', overflowY: 'auto' }}>
      <ul className="post-list">
        {posts.map(post => (
          <li key={post.id} style={{ padding: '1rem', borderBottom: '1px solid #eee' }}>
            <h3>{post.title}</h3>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
      
      {isLoading && <p style={{ textAlign: 'center', padding: '1rem' }}>加载中...</p>}
      {!hasMore && !isLoading && <p style={{ textAlign: 'center', padding: '1rem', color: '#666' }}>已加载全部内容</p>}
    </div>
  );
}

关键细节说明

  • SEO保障:Server Component在服务端将初始20条数据渲染到HTML中,搜索引擎爬虫能直接抓取到内容,完全满足SEO需求。
  • 组件分工:Server Component只做数据获取和传递,不涉及任何交互;Client Component专注处理浏览器环境下的交互逻辑,职责清晰。
  • 性能优化:用Intersection Observer替代scroll事件,减少不必要的函数触发;加载过程中禁用重复请求,避免并发问题。
  • 状态管理:初始数据直接作为useState的初始值,无需从props手动同步到状态,Next.js会自动将Client Component的初始状态渲染到静态HTML中。

内容的提问来源于stack exchange,提问作者Matthew Barnden

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 13:43:04