优化 Nextjs 开发的个人博客首页,秒开!

我有一个使用 Nextjs 开发的个人博客,功能大概有首页、文章列表、留言板、创建编辑文章

但是我对 Nextjs 是干中学的状态,属于是一边学一边写

使用 vercel 部署后总觉得这个博客首页打开有点慢 🧐 猛击查看我的个人博客

首页会获取 GitHub、掘金的个人信息进行展示,掘金还好,GitHub 获取用户信息的接口是有点慢的

所以我做了以下处理:1. 骨架屏 2. 接口缓存

但还是慢因为需要先加载骨架屏然后在渲染出实际内容,虽然能很快加载出页面,但是看到页面实际内容会慢

当时觉得可以优化,但是不知道怎么去优化,写完就没再动了

最近看了点 Nextjs 的文档和文章,有了点思路然后就开始审查首页的代码

在查看代码的时候发现使用了 'use client' 声明,也就是渲染 掘金、GitHub 信息的时候使用了客户端渲染

以掘金文章模块为例

ts 复制代码
'use client'

import { Icon } from '@iconify/react'
import { ContentCard } from './ContentCard'
import { useEffect, useState } from 'react'
import { Skeleton } from '@/components/ui/skeleton'
import Link from 'next/link'
import { Article } from '@prisma/client'
import { TimeInSeconds } from '@/lib/enums'

function NoFound() {
  return <p className="text-center text-gray-500 dark:text-gray-400 py-8">No articles found.</p>
}

function LoadingComponent() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {[...Array(6)].map((_, index) => (
        <div key={index}>
          <Skeleton className="h-6 w-3/4 mb-2" />
          <Skeleton className="h-4 w-full mb-2" />
          <Skeleton className="h-4 w-1/4" />
          {index < 2 && <div className="my-6 border-b border-gray-200 dark:border-gray-700"></div>}
        </div>
      ))}
    </div>
  )
}

function ArticleInfo({ articles }: { articles: Article[] }) {
  return (
    <div key="content" className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {articles?.map((article) => (
        <div
          key={article.id}
          className="block transition duration-300 ease-in-out hover:bg-black/10 dark:hover:bg-white/10 rounded p-2"
        >
          <Link
            href={`https://juejin.cn/post/${article.id}`}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h3 className="text-lg font-semibold mb-2 transition duration-300">{article.title}</h3>
            <p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
              {article.summary || 'No description available'}
            </p>
            <div className="flex items-center text-sm text-gray-500 dark:text-gray-400 space-x-4">
              <span className="flex items-center">
                <Icon icon="mdi:star" className="w-4 h-4 mr-1 text-yellow-500" />
                {article.favorites}
              </span>
              <span className="flex items-center">
                <Icon icon="mdi:thumb-up" className="w-4 h-4 mr-1 text-green-500" />
                {article.likes}
              </span>
              <span className="flex items-center">
                <Icon icon="mdi:eye" className="w-4 h-4 mr-1 text-blue-500" />
                {article.views}
              </span>
            </div>
          </Link>
        </div>
      ))}
    </div>
  )
}

export function JueJinArticles() {
  const [articles, setArticles] = useState<Article[]>([])
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    const loadData = async () => {
      try {
        const res = await fetch('/api/articles/all', {
          next: { revalidate: TimeInSeconds.oneHour }
        }).then((res) => res.json())

        if (res.code === 0) {
          const list: Article[] = res?.data || []
          const sortedData = list.sort((a, b) => b.likes - a.likes).slice(0, 6)
          setArticles(sortedData)
        }
      } catch (error) {
        console.error('Failed to fetch articles:', error)
      } finally {
        setIsLoading(false)
      }
    }
    loadData()
  }, [])

  return (
    <ContentCard title="掘金文章">
      {isLoading ? <LoadingComponent /> : <ArticleInfo articles={articles} />}

      {articles.length === 0 && !isLoading && <NoFound />}
    </ContentCard>
  )
}

看代码其实也能发现是因为使用了 useEffect、useState 所以只能使用客户端渲染

首页中需要获取数据的组件基本都是这个逻辑

在看文档时候发现可以这样写,使用服务端渲染

代码改造后如下:

ts 复制代码
import { Icon } from '@iconify/react'
import { ContentCard } from './ContentCard'
import Link from 'next/link'
import { Article } from '@prisma/client'
import { TimeInSeconds } from '@/lib/enums'

function NoFound() {
  return <p className="text-center text-gray-500 dark:text-gray-400 py-8">No articles found.</p>
}

function ArticleList({ articles }: { articles: Article[] }) {
  return (
    <div key="content" className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {articles?.map((article) => (
        <div
          key={article.id}
          className="block transition duration-300 ease-in-out hover:bg-black/10 dark:hover:bg-white/10 rounded p-2"
        >
          <Link
            href={`https://juejin.cn/post/${article.id}`}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h3 className="text-lg font-semibold mb-2 transition duration-300">{article.title}</h3>
            <p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
              {article.summary || 'No description available'}
            </p>
            <div className="flex items-center text-sm text-gray-500 dark:text-gray-400 space-x-4">
              <span className="flex items-center">
                <Icon icon="mdi:star" className="w-4 h-4 mr-1 text-yellow-500" />
                {article.favorites}
              </span>
              <span className="flex items-center">
                <Icon icon="mdi:thumb-up" className="w-4 h-4 mr-1 text-green-500" />
                {article.likes}
              </span>
              <span className="flex items-center">
                <Icon icon="mdi:eye" className="w-4 h-4 mr-1 text-blue-500" />
                {article.views}
              </span>
            </div>
          </Link>
        </div>
      ))}
    </div>
  )
}

async function getJueJinArticles() {
  try {
    const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/articles/all`, {
      next: { revalidate: TimeInSeconds.oneHour }
    })
    const json = await res.json()
    return json.code === 0 ? json.data : []
  } catch {
    return []
  }
}

export async function JueJinArticles() {
  const list = (await getJueJinArticles()) as Article[]

  const articles = list.sort((a, b) => b.likes - a.likes).slice(0, 6)

  return (
    <ContentCard title="掘金文章">
      {articles.length === 0 ? <NoFound /> : <ArticleList articles={articles} />}
    </ContentCard>
  )
}

改为服务端渲染,调整后去除了骨架屏的逻辑、修改了数据获取的逻辑、减少了几十行代码

测试可以正常运行后就把其他的组件依葫芦画瓢做了修改,最终成果如下

优化前后对比

优化前-禁用缓存(这个因为无法在无痕打开所以就没开无痕)

优化后-无痕模式、禁用缓存

关于数据缓存

ts 复制代码
const res = await fetch('/api/articles/all', {
          next: { revalidate: TimeInSeconds.oneHour }
        }).then((res) => res.json())

这里 revalidate 是指数据重新验证的时间,比如这里是一小时,表示最少需要一小时数据才会更新,但并不是每一个小时更新一次,假设你在一小时后第一次访问这个接口返回的依然是缓存的数据、同时更新数据,第二次访问的时候就是新数据了

总结

类似个人博客等对内容新鲜度没有太高要求的页面可以使用 服务端渲染 + 数据缓存的方式,加快页面的访问速度

相关推荐
掘金安东尼21 分钟前
⏰前端周刊第424期(2025年7月21日–7月27日)
前端·javascript·面试
江城开朗的豌豆33 分钟前
Vue和React的数据流之争:双向绑定 vs 单向数据流,谁更适合你?
前端·javascript·vue.js
OpenTiny社区36 分钟前
前端可智能识别的搜索组件 SearchBox 使用详解!
前端·vue.js·ui·开源·opentiny
世伟爱吗喽37 分钟前
最新面试题总结
前端·javascript·vue.js
江城开朗的豌豆44 分钟前
前端权限控制实战:手把手教你玩转角色权限分配
前端·javascript·vue.js
超浪的晨1 小时前
JavaWeb 入门:HTML 基础与实战详解(Java 开发者视角)
java·开发语言·前端·后端·html·个人开发
CCF_NOI.3 小时前
谷歌浏览器深入用法全解析:解锁高效网络之旅
大数据·运维·服务器·前端·计算机·谷歌
paopaokaka_luck6 小时前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失9497 小时前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json