如何使用Next.js 14 App Router生成并提供大型动态站点地图?
Next.js 14 App Router 大型媒体站点地图优化方案
核心优化思路:分块分页 + 流式生成 + 批量处理
1. 实现分页式站点地图索引(Sitemap Index)
将超大媒体条目拆分为多个子站点地图(建议每1万条一个文件),通过主索引文件统一指向这些子文件,避免一次性加载全量数据。
主索引文件 app/sitemap.js
import { getImageCount, getVideoCount } from "@/services/mediaService"; const ITEMS_PER_SITEMAP = 10000; // 每个子站点地图条目上限 export default async function sitemap() { const imageCount = await getImageCount(); const videoCount = await getVideoCount(); // 生成图片类子站点地图列表 const imageSitemaps = Array.from({ length: Math.ceil(imageCount / ITEMS_PER_SITEMAP) }, (_, i) => ({ url: `${process.env.NEXT_PUBLIC_DEV_URL}/media/sitemap-images-${i + 1}.xml`, lastModified: new Date(), })); // 生成视频类子站点地图列表 const videoSitemaps = Array.from({ length: Math.ceil(videoCount / ITEMS_PER_SITEMAP) }, (_, i) => ({ url: `${process.env.NEXT_PUBLIC_DEV_URL}/media/sitemap-videos-${i + 1}.xml`, lastModified: new Date(), })); return [ { url: `${process.env.NEXT_PUBLIC_DEV_URL}`, lastModified: new Date(), changeFrequency: "monthly", priority: 1, }, // 其他静态路由... ...imageSitemaps, ...videoSitemaps, ]; }
动态子站点地图路由 app/media/sitemap-images-[page].js
import { fetchImageIdsByPage } from "@/services/imageService"; const ITEMS_PER_SITEMAP = 10000; export default async function sitemap({ params }) { const page = parseInt(params.page); const imageIds = await fetchImageIdsByPage(page, ITEMS_PER_SITEMAP); return imageIds.map(id => ({ url: `${process.env.NEXT_PUBLIC_DEV_URL}/media/image/${id}`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8, })); }
数据库分页查询实现(services/imageService.js)
export async function fetchImageIdsByPage(page, limit) { const offset = (page - 1) * limit; // 仅查询ID字段,避免加载冗余数据 const images = await db.image.findMany({ select: { id: true }, skip: offset, take: limit, }); return images.map(img => img.id); } export async function getImageCount() { return await db.image.count(); }
2. 流式响应减少内存占用
直接生成XML字符串并流式输出,避免将所有URL对象存储在内存中,彻底解决OOM问题。
修改子站点路由为流式返回:
import { fetchImageIdsByPage, getImageCount } from "@/services/imageService"; import { Readable } from 'stream'; const ITEMS_PER_SITEMAP = 10000; // 预先生成所有分页参数 export async function generateStaticParams() { const count = await getImageCount(); const pages = Math.ceil(count / ITEMS_PER_SITEMAP); return Array.from({ length: pages }, (_, i) => ({ page: (i + 1).toString() })); } export async function GET({ params }) { const page = parseInt(params.page); const imageIds = await fetchImageIdsByPage(page, ITEMS_PER_SITEMAP); // 分段生成XML内容 const xmlStart = `<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`; const xmlEntries = imageIds.map(id => ` <url> <loc>${process.env.NEXT_PUBLIC_DEV_URL}/media/image/${id}</loc> <lastmod>${new Date().toISOString()}</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url>`).join(''); const xmlEnd = ` </urlset>`; // 流式输出 const stream = Readable.from([xmlStart, xmlEntries, xmlEnd]); return new Response(stream, { headers: { 'Content-Type': 'application/xml', }, }); } // 启用ISR,24小时自动更新 export const revalidate = 86400;
3. 数据库层面深度优化
- 仅查询必要字段:始终只获取
id和lastModified(如果需要),避免加载整个媒体文档 - 索引优化:确保分页查询用到的字段(如
id)有数据库索引 - 游标分页替代OFFSET:千万级以上数据集用游标分页(基于上一条的
id),避免OFFSET导致的性能衰减
4. 服务器资源兜底优化
- 调整Node.js内存限制:启动命令添加
--max-old-space-size=4096(分配4GB内存) - 启用边缘运行时:在子站点路由顶部添加
export const runtime = 'edge';,利用边缘节点资源,降低冷启动耗时
内容的提问来源于stack exchange,提问作者Junbutaw Tawbutaw
相关产品推荐
相关产品推荐

