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

如何在Next.js页面渲染必要数据后再加载图片?

解决方案:页面渲染后延迟加载图片优化Next.js首页性能

问题背景

当前使用Next.js的Home组件通过getServerSideProps获取API数据渲染页面,但首页初始数据体积超出128kB阈值,导致页面性能下降。需要实现页面渲染完成后再加载图片,避免一次性加载所有图片资源,减少初始数据体积。

核心解决思路

  1. 剥离初始数据中的图片字段:SSR阶段只返回必要的文本数据(标题、ID、分类、时间等),不包含图片URL,直接降低初始数据大小。
  2. 客户端动态加载图片:页面渲染完成后,通过二次请求获取图片资源;或结合视口检测,仅当卡片进入用户视野时才加载对应图片。
  3. 利用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 11:32:07