我有一个使用 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
是指数据重新验证的时间,比如这里是一小时,表示最少需要一小时数据才会更新,但并不是每一个小时更新一次,假设你在一小时后第一次访问这个接口返回的依然是缓存的数据、同时更新数据,第二次访问的时候就是新数据了
总结
类似个人博客等对内容新鲜度没有太高要求的页面可以使用 服务端渲染 + 数据缓存的方式,加快页面的访问速度