如何在Next.js页面渲染必要数据后再加载图片?
解决方案:页面渲染后延迟加载图片优化Next.js首页性能
问题背景
当前使用Next.js的Home组件通过getServerSideProps获取API数据渲染页面,但首页初始数据体积超出128kB阈值,导致页面性能下降。需要实现页面渲染完成后再加载图片,避免一次性加载所有图片资源,减少初始数据体积。
核心解决思路
- 剥离初始数据中的图片字段:SSR阶段只返回必要的文本数据(标题、ID、分类、时间等),不包含图片URL,直接降低初始数据大小。
- 客户端动态加载图片:页面渲染完成后,通过二次请求获取图片资源;或结合视口检测,仅当卡片进入用户视野时才加载对应图片。
- 利用Next.js图片优化能力:结合
next/image组件的懒加载、占位图等特性,进一步优化图片加载体验。
具体实现步骤
1. 修改API,剥离初始数据中的图片字段
保持现有API返回文本数据(不包含图片URL),新增单独获取图片的接口,可选择以下两种方案:
方案A:批量获取图片URL的接口
创建pages/api/news/images.ts,接收文章ID数组,返回对应图片URL的映射:
import dbConnect from "../../../utils/dbConnect"; import { NextApiRequest, NextApiResponse } from "next"; import NewsPosts from "../../../models/NewsPosts"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { await dbConnect(); if (req.method === "POST") { try { const { postIds } = req.body; const posts = await NewsPosts.find( { _id: { $in: postIds } }, { _id: 1, image: 1 } ); const imageMap = posts.reduce((acc, post) => { acc[post._id.toString()] = post.image; return acc; }, {} as Record<string, string>); res.status(200).json(imageMap); } catch (e: any) { res.status(500).json(e); } } else { res.status(405).json({ message: "不允许的请求方法" }); } }
方案B:单篇文章获取图片的接口
创建pages/api/news/[id]/image.ts,根据文章ID返回对应图片URL:
import dbConnect from "../../../../utils/dbConnect"; import { NextApiRequest, NextApiResponse } from "next"; import NewsPosts from "../../../../models/NewsPosts"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { await dbConnect(); const { id } = req.query; if (req.method === "GET") { try { const post = await NewsPosts.findById(id, { image: 1 }); if (!post) { return res.status(404).json({ message: "文章不存在" }); } res.status(200).json({ image: post.image }); } catch (e: any) { res.status(500).json(e); } } else { res.status(405).json({ message: "不允许的请求方法" }); } }
2. 修改Home组件,实现客户端图片加载
方案A:页面渲染完成后批量加载所有图片
import styles from "../styles/Home.module.scss"; import MainCard from "../components/MainCard"; import NormalCard from "../components/NormalCard"; import { News } from "../types/types"; import { GetServerSideProps } from "next"; import axios from "axios"; import React, { useEffect, useState } from "react"; import { listEng } from "./category/[category]"; import dynamic from "next/dynamic"; const LastNews = dynamic(() => import("../components/LastNews"), { ssr: false }); const NewsBlock = dynamic(() => import("../components/NewsBlock"), { ssr: false }); type Props = { lastFivePosts: News[]; topThreeRatedPosts: News[]; postsByCategory: News[]; }; export default function Home({ lastFivePosts, topThreeRatedPosts, postsByCategory }: Props) { const [postImages, setPostImages] = useState<Record<string, string>>({}); const [isLoadingImages, setIsLoadingImages] = useState(true); if (!lastFivePosts && !topThreeRatedPosts) { return <div>正在进行技术维护</div>; } // 收集所有需要图片的文章ID并去重 useEffect(() => { const getAllPostIds = () => { const ids: string[] = []; if (lastFivePosts) ids.push(...lastFivePosts.map(post => post._id)); if (topThreeRatedPosts) ids.push(...topThreeRatedPosts.map(post => post._id)); if (postsByCategory) ids.push(...postsByCategory.map(post => post._id)); return [...new Set(ids)]; }; const loadImages = async () => { const postIds = getAllPostIds(); if (postIds.length === 0) { setIsLoadingImages(false); return; } try { const res = await axios.post( `${process.env.NEXT_PUBLIC_API_CONNECT_URL}/api/news/images`, { postIds } ); setPostImages(res.data); } catch (err) { console.error("图片加载失败:", err); } finally { setIsLoadingImages(false); } }; // 页面渲染完成后触发图片加载 loadImages(); }, [lastFivePosts, topThreeRatedPosts, postsByCategory]); return ( <div className={styles.wrap}> <div className={styles.upper_block}> <div className={styles.leftBlock}> <NormalCard news={lastFivePosts[1]} imageUrl={postImages[lastFivePosts[1]._id]} /> <NormalCard news={lastFivePosts[2]} imageUrl={postImages[lastFivePosts[2]._id]} /> </div> <div className={styles.middleBlock}> <MainCard news={lastFivePosts[0]} imageUrl={postImages[lastFivePosts[0]._id]} /> </div> <div className={styles.rightBlock}> <NormalCard news={lastFivePosts[3]} imageUrl={postImages[lastFivePosts[3]._id]} /> <NormalCard news={lastFivePosts[4]} imageUrl={postImages[lastFivePosts[4]._id]} /> </div> </div> <hr className={styles.hrHome} /> <div className={styles.titlePopular}> <h1>编辑精选</h1> <span> 这里我们收集了近期编辑部评选的三篇最佳新闻。我们的记者努力工作,为您提供来自全球的最新、最有趣的信息。 </span> </div> <div className={styles.lastNewsBlock}> <div className={styles.lastNews}> <LastNews items={topThreeRatedPosts} postImages={postImages} /> </div> </div> <div className={styles.newsBlock}> {listEng.map((category, index) => ( <NewsBlock news={postsByCategory} key={index} category={category} postImages={postImages} /> ))} </div> </div> ); } export const getServerSideProps: GetServerSideProps = async () => { let res = null; try { res = await axios.get( `${process.env.NEXT_PUBLIC_API_CONNECT_URL}/api/news` ); } catch (err) { console.log(err); } return { props: { lastFivePosts: res?.data.lastFivePosts, topThreeRatedPosts: res?.data.topThreeRatedPosts, postsByCategory: res?.data.postsByCategory, }, }; };
方案B:视口触发的懒加载(更优性能)
修改卡片组件(以NormalCard为例),使用Intersection Observer仅当卡片进入视口时加载图片:
import React, { useEffect, useState, useRef } from "react"; import { News } from "../types/types"; import axios from "axios"; import Image from "next/image"; type Props = { news: News; }; export default function NormalCard({ news }: Props) { const [imageUrl, setImageUrl] = useState<string | null>(null); const cardRef = useRef<HTMLDivElement>(null); useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach(async (entry) => { if (entry.isIntersecting && !imageUrl) { try { const res = await axios.get( `${process.env.NEXT_PUBLIC_API_CONNECT_URL}/api/news/${news._id}/image` ); setImageUrl(res.data.image); } catch (err) { console.error("图片加载失败:", err); } observer.unobserve(entry.target); } }); }, { threshold: 0.1 } // 卡片进入视口10%时触发 ); if (cardRef.current) observer.observe(cardRef.current); return () => { if (cardRef.current) observer.unobserve(cardRef.current); }; }, [news._id, imageUrl]); return ( <div ref={cardRef} className="card"> <h3>{news.title}</h3> {/* 使用Next.js Image组件优化加载 */} <Image src={imageUrl || "/placeholder-blur.jpg"} alt={news.title} width={300} height={200} loading="lazy" placeholder="blur" blurDataURL="/placeholder-blur.jpg" onError={() => setImageUrl("/placeholder-blur.jpg")} /> </div> ); }
3. 卡片组件适配
无论使用哪种方案,都需要修改卡片组件(MainCard、NormalCard、LastNews等),让它们接收图片URL作为props,或自行处理图片加载逻辑,并添加占位图避免布局偏移。
额外优化建议
- 使用
next/image组件替代原生img,它会自动处理图片压缩、格式转换、懒加载和占位图,进一步提升性能。 - 为占位图生成模糊缩略图,提升用户体验。
- 对图片URL使用CDN加速,减少加载时间。
内容的提问来源于stack exchange,提问作者markiv
相关产品推荐
相关产品推荐

