
Next.js 应用变慢的情况比你想象的更常见。过长的加载时间会让用户感到沮丧,降低参与度。但大多数性能问题都可以归结为几个常见原因 ------ 从繁重的数据获取、路由延迟到过大的包体积、缓存错误和未优化的图像。
在本文中,我将指出 Next.js 应用中 8 个常见的性能问题,并分享清晰、实用的解决办法,帮助你打造更快、更流畅的用户体验,让用户切实感受到差异。
我假设你已经基本掌握 React 组件、useState 和 useEffect 等钩子,以及 Next.js 路由和数据获取的基础知识。你还应该熟悉使用浏览器开发者工具和在命令行中运行构建命令。如果这些听起来不熟悉,你可能需要先复习一下,不过我会尽量把解释写得简单明了。
快速说明一下 ------ 我会在示例中使用 getServerSideProps,因为许多现有项目仍然依赖 Pages Router,而且性能问题并非 Next.js 特定版本所独有。即使语法略有变化,这些优化原则同样适用于使用较新的 App Router 的情况。我们的目标是专注于最重要的解决办法,无论你的项目配置如何。
1. 感知性能不足
我们来谈谈感知性能 ------ 即应用给用户的感觉有多快,而不仅仅是它实际有多快。Jakob Nielsen 在他 1993 年的《可用性工程》一书中,为用户耐心设定了一些经典基准:
-
0.1 秒 ------ 系统感觉 "即时响应" 的极限。在此范围内,用户不会察觉到延迟。
-
1.0 秒 ------ 保持用户思维流畅的上限,尽管他们会注意到延迟。
-
10 秒 ------ 用户完全失去注意力之前的最长时间。
如果你的 Next.js 应用显示内容的时间超过 1 秒,对用户来说它就正式 "变慢" 了,即使数据实际上正在后台加载。而等待 10 秒?那几乎是数字时代的永恒,足以让用户彻底失去兴趣。
但这里有个关键点:你的应用真的慢吗?还是只是 "感觉" 慢?
Reddit 上有个讨论:"为什么大多数 Next.js 应用都这么慢?"
用户说:"每次我访问一个网站,点击链接要等 2 秒,我就会想'这肯定是个 Next 应用',查看代码后发现确实如此。我猜是服务器组件在等待数据获取,但初始加载真的很慢(之后就好了)。我见过一些优化得很好的 Next 应用,初始加载很快,但 90% 我遇到的都有加载慢的问题。是新手开发者的问题还是框架本身的问题?"
这就是感知性能不足的体现。有时候,无论数据获取需要多长时间,你处理等待的方式都会产生巨大差异。
解决办法:使用加载状态和 React Suspense
关键在于立即向用户展示一些内容 ------ 即使不是最终内容。设计良好的加载状态可以让 2 秒的等待感觉比盯着空白屏幕 1 秒更快。
React Suspense 让在 Next.js 中实现这一点变得容易。你可以这样包裹组件,在内容加载时显示占位符:
jsx
import { Suspense } from 'react';
export default function Dashboard () {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
当用户重新加载页面时,他们会立即看到占位符,让他们知道系统正在处理:
( Dashboard 重新加载演示:Next.js Suspense + 骨架屏加载演示)
(展示内容:John Doe 的仪表盘,包含用户信息、总用户数、收入、最近活动等,加载时显示骨架屏)
2. Next.js 混合渲染拖慢速度
说完了感知性能,我们来看看一些实际的性能问题。如你所知,Next.js 不仅是关于服务器端渲染 ------ 也不纯粹是单页应用。它是一个混合框架,兼顾两者的优点。但有时,也会带来两者的缺点。
这种混合模式很强大。它让你的应用可以为 SEO 和快速初始加载提供完全渲染的页面,然后切换到 SPA 行为以实现流畅的客户端导航。但这也意味着你要同时处理两种不同的性能特征 ------ 这正是问题可能出现的地方。
理解两种模式
首次加载时,你的 Next.js 应用表现得像传统的服务器渲染网站。它在服务器上获取数据,渲染完整的 HTML,然后发送到浏览器。这对 SEO 和快速显示有意义的内容非常有利:
jsx
// 首次访问:服务器承担所有繁重工作
export async function getServerSideProps() {
// 这在服务器上运行,会阻塞响应
const userData = await fetchUser();
const dashboardData = await fetchDashboard(userData.id);
const notifications = await fetchNotifications(userData.id);
return {
props: {
userData,
dashboardData,
notifications
}
};
}
但一旦初始页面加载完成,React 接管后,Next.js 就会切换到 SPA 模式。点击操作会触发客户端导航,无需整页重新加载 ------ 就像 React Router 一样:
jsx
// 后续导航:纯 SPA 行为
function Dashboard({ userData }) {
const router = useRouter();
const goToProfile = () => {
// 不访问服务器------纯客户端导航
router.push('/profile');
};
return (
<div>
<h1>Welcome, {userData.name}</h1>
<button onClick={goToProfile}>View Profile</button>
</div>
);
}
性能问题出现的地方
棘手之处在于,你实际上在运行两个应用:
-
处理初始请求的服务器渲染应用
-
处理导航和交互的客户端 SPA
每个都有自己的性能特征,如果没有适当优化,每个都可能拖慢整个应用:
-
在服务器上,缓慢的数据库查询或阻塞操作会延迟 HTML 响应
-
在客户端,过大的 JavaScript 包或低效的 API 调用会阻碍流畅的导航
更糟的是,这些问题会相互叠加。假设用户访问你的主页(服务器渲染),然后点击进入仪表盘(客户端)。这个仪表盘需要一个 2MB 的 JavaScript 包 ------ 而且还没有缓存。那么用户就必须等待包加载和客户端数据获取:
jsx
// 性能陷阱:繁重的客户端页面
import HeavyChart from './HeavyChart'; // 500KB
import ComplexTable from './ComplexTable'; // 300KB
import RichEditor from './RichEditor'; // 400KB
export default function Dashboard () {
const [data, setData] = useState(null);
useEffect(() => {
// 导航后在客户端获取数据
fetchDashboardData().then(setData);
}, []);
// 用户需要等待包加载 + 数据加载
if (!data) return <div>Loading...</div>;
return (
<div>
<HeavyChart data={data.charts} />
<ComplexTable data={data.tables} />
<RichEditor content={data.content} />
</div>
);
}
解决办法:同时优化两种模式
当服务器和客户端都经过优化时,你就能获得两者的最佳效果 ------ 快速的初始加载、良好的 SEO,以及流畅的、类应用的导航。但如果忽视任何一方,整个体验都会变得迟缓。
现在我们已经了解了混合渲染如何引入隐藏的性能成本,接下来让我们聚焦服务器端一个最大的罪魁祸首 ------ 缓慢的、顺序的数据获取。
3. 数据获取过慢且按顺序执行
你点击应用中的某个东西,然后...... 什么都没有。没有反馈,没有内容。十有八九,问题在于同步数据获取。这是指应用 "礼貌地" 一次加载一条数据。以下是典型 Next.js 配置中的情况:
jsx
// 慢方法------每个请求都要等前一个完成
export async function getServerSideProps({ req }) {
// 步骤 1:获取用户(300ms)
const user = await fetchUser(req.session.userId);
// 步骤 2:等用户数据,再获取个人资料(400ms)
const profile = await fetchUserProfile(user.id);
// 步骤 3:等个人资料,再获取仪表盘数据(600ms)
const dashboardData = await fetchDashboardData(user.id, profile.preferences);
// 步骤 4:等仪表盘数据,再获取通知(200ms)
const notifications = await fetchNotifications(user.id);
// 总时间:300 + 400 + 600 + 200 = 1500ms(1.5 秒!)
return {
props: {
user,
profile,
dashboardData,
notifications
}
};
}
这种方法把本可以 4800ms 的页面加载变成了 1.5 秒的加载。每个 await 都在说 ------"暂停所有操作 ------ 在这个完成之前我们不继续。" 但这些请求真的需要一个接一个地进行吗?
解决办法:使用 Promise.all ()
如果你的请求彼此不依赖,就没有理由不能同时运行。这就是 Promise.all () 的用武之地:
jsx
export async function getServerSideProps({ req }) {
// 步骤 1:先获取用户(仍然是其他请求所需要的)
const user = await fetchUser(req.session.userId);
// 步骤 2:并行获取其他所有数据
const [profile, dashboardData, notifications] = await Promise.all([
fetchUserProfile(user.id), // 400ms
fetchDashboardData(user.id), // 600ms
fetchNotifications(user.id) // 200ms
]);
// 总时间:300ms(用户) + 600ms(最长的并行请求) = 900ms
// 我们节省了 600ms!
return {
props: {
user,
profile,
dashboardData,
notifications
}
};
}
这个简单的改变就减少了 600ms------ 而且这只是一次页面加载。
额外解决办法:全局数据的并行获取
你可以更进一步。如果某些数据不依赖于用户,比如系统范围的设置或服务器状态,你可以在获取用户数据的同时获取这些数据:
jsx
export async function getServerSideProps({ req }) {
// 并行获取与用户无关的数据和用户数据
const [user, globalSettings, systemStatus] = await Promise.all([
fetchUser(req.session.userId),
fetchGlobalSettings(), // 不需要用户数据
fetchSystemStatus() // 不需要用户数据
]);
// 现在并行获取依赖于用户的数据
const [profile, dashboardData, notifications] = await Promise.all([
fetchUserProfile(user.id),
fetchDashboardData(user.id),
fetchNotifications(user.id)
]);
return {
props: {
user,
profile,
dashboardData,
notifications,
globalSettings,
systemStatus
}
};
}
通过更智能的并行化,你可以减少加载时间,提高响应速度 ------ 不需要复杂的工具或库。只需要更好地使用 JavaScript。
即使数据获取更快,你的应用可能仍然感觉不流畅。为什么?因为你的路由行为可能在进行不必要的完整服务器往返。让我们接下来看看这个问题。
4. 路由触发不必要的服务器往返
使用 Next.js App Router 时,每次导航都可能触发服务器端渲染 ------ 即使是本可以(也应该)在客户端处理的路由。这不仅低效,而且肯定会让快速的应用感觉变慢。
假设你有一个用户在浏览产品目录。在传统的 SPA 中,在产品页面之间点击应该是即时的。JavaScript 处理状态更新,URL 变化而无需重新加载页面。
但如果 Next.js App Router 配置不当,每次点击都可能往返服务器。情况如下:
jsx
// app/products/[id]/page.js
// 每次访问产品页面时,这都会在服务器上运行
export default async function ProductPage({ params }) {
// 每次导航都向服务器发起网络请求
const product = await fetchProduct(params.id);
const reviews = await fetchReviews(params.id);
const recommendations = await fetchRecommendations(params.id);
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
<RecommendationGrid recommendations={recommendations} />
</div>
);
}
在这种设置下,每次点击都要经过同样缓慢的流程:
-
点击产品链接
-
浏览器向服务器发出请求
-
服务器获取产品数据(数据库调用)
-
服务器渲染 HTML
-
服务器将 HTML 发送到浏览器
-
浏览器显示页面
这有六个步骤,每个都可能有延迟。乘以十次产品浏览,就有十次服务器请求 ------ 而不是十次即时的页面转换。
解决办法:转向客户端路由
为避免这些往返,尽可能将路由转移到客户端。方法如下:
jsx
// app/products/[id]/page.js
'use client'; // 这使其成为客户端渲染
import { useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
export default function ProductPage () {
const params = useParams();
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState(null);
useEffect(() => {
// 在客户端获取数据------没有服务器往返
Promise.all([
fetch(`/api/products/${params.id}`).then(res => res.json()),
fetch(`/api/reviews/${params.id}`).then(res => res.json())
]).then(([productData, reviewsData]) => {
setProduct(productData);
setReviews(reviewsData);
});
}, [params.id]);
if (!product) return <ProductSkeleton />;
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
</div>
);
}
这样,产品页面之间的导航会立即发生 ------ 数据在后台加载,无需在服务器上重新渲染整个页面。
何时使用服务器端 vs 客户端渲染
快速指南:
使用服务器端渲染(SSR)当:
-
SEO 至关重要(产品页面、博客文章)
-
你要显示用户特定的敏感数据
-
初始页面加载速度比导航速度更重要
-
内容不经常变化
使用客户端渲染(CSR)当:
- 用户频繁在相似页面之间导航
- 你可以有效地缓存数据
- SEO 不是优先事项(用户仪表盘、管理面板)
- 你想要即时的、类应用的导航
额外解决办法:混合渲染
有时,你需要 SSR 用于初始页面加载,但希望后续交互受益于 CSR。Next.js App Router 允许你结合两者:
jsx
// app/products/[id]/page.js
// 为 SEO 进行服务器渲染初始页面
export default async function ProductPage({ params }) {
const initialProduct = await fetchProduct(params.id);
return (
<div>
<ProductClient initialData={initialProduct} productId={params.id} />
</div>
);
}
// components/ProductClient.js
'use client';
export default function ProductClient({ initialData, productId }) {
const [product, setProduct] = useState(initialData);
// 后续导航在客户端进行
const router = useRouter();
const navigateToProduct = async (newId) => {
// 立即更新 URL(感觉是即时的)
router.push(`/products/${newId}`);
// 在后台获取新数据
const newProduct = await fetch(`/api/products/${newId}`).then(res => res.json());
setProduct(newProduct);
};
return (
<div>
<ProductDetails product={product} />
<RelatedProducts onProductClick={navigateToProduct} />
</div>
);
}
但即使有智能路由和优化的数据获取,如果应用拖着过大的 JavaScript 包,仍然会感觉迟缓。让我们谈谈为什么会出现这个问题 ------ 以及如何解决。
5. JavaScript 包体积过大
我曾在一个黑客马拉松期间加入一个 Next.js 项目,当时主 JavaScript 包的大小是 2.3 MB。之前的开发者为了使用几个函数就导入了整个库。没有代码分割。没有动态导入。只是把一个巨大的负载丢给每个用户。
JavaScript 包体积直接影响你的交互时间(TTI)------ 衡量页面何时完全可用的指标。包越大,用户盯着加载 spinner 的时间就越长。
以下是经常导致包膨胀的原因:
jsx
// 第一个包膨胀器:导入整个库
import _ from 'lodash'; // 导入整个 70KB 的库
import * as dateFns from 'date-fns'; // 另一个大型导入
// 第二个包膨胀器:在各处导入重型组件
import { DataVisualization } from './DataVisualization'; // 500KB 组件
import { VideoPlayer } from './VideoPlayer'; // 300KB 组件
import { RichTextEditor } from './RichTextEditor'; // 400KB 组件
export default function HomePage () {
return (
<div>
<h1>Welcome</h1>
{/* 这些组件可能在初始加载时甚至不可见 */}
<DataVisualization />
<VideoPlayer />
<RichTextEditor />
</div>
);
}
这种方法会向每个用户加载所有内容 ------ 即使他们从未与这些组件交互。幸运的是,有更好的方法。
解决办法:代码分割和动态导入
Next.js 默认支持智能代码分割。但要充分利用它,你需要使用动态导入来仅在需要时加载代码。
基于路由的代码分割
默认情况下,Next.js 按路由分割代码。但你可以使用 next/dynamic 进一步优化:
jsx
// pages/dashboard.js - 仅当用户访问 /dashboard 时加载
import dynamic from 'next/dynamic';
// 仅在需要时加载重型组件
const AnalyticsChart = dynamic(() => import('../components/AnalyticsChart'), {
loading: () => <ChartSkeleton />,
ssr: false // 对仅客户端组件跳过服务器端渲染
});
const DataExporter = dynamic(() => import('../components/DataExporter'), {
loading: () => <p>Loading exporter...</p>
});
export default function Dashboard() {
const [showAnalytics, setShowAnalytics] = useState(false);
const [showExporter, setShowExporter] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowAnalytics(true)}>
View Analytics
</button>
{showAnalytics && <AnalyticsChart />}
<button onClick={() => setShowExporter(true)}>
Export Data
</button>
{showExporter && <DataExporter />}
</div>
);
}
通过这种模式,用户只在请求时才下载图表或导出逻辑,而不是之前。
基于组件的代码分割
如果你有在路由之间共享但仅在特定情况下需要的组件,也可以延迟加载它们:
jsx
// components/ConditionalFeatures.js
import dynamic from 'next/dynamic';
// 仅当用户有高级订阅时加载
const PremiumChart = dynamic(() => import('./PremiumChart'), {
loading: () => <div>Loading premium features...</div>
});
// 仅当用户点击"高级设置"时加载
const AdvancedSettings = dynamic(() => import('./AdvancedSettings'));
export function ConditionalFeatures({ user, showAdvanced }) {
return (
<div>
{user.isPremium && <PremiumChart />}
{showAdvanced && <AdvancedSettings />}
</div>
);
}
这确保你的用户不会为他们甚至无法访问的功能付出性能代价。
额外解决办法:使用 @next/bundle-analyzer 分析包
要查看是什么占用了你的包体积,使用官方的包分析器:
jsx
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
});
module.exports = withBundleAnalyzer({
// 你的 Next.js 配置
});
运行 ANALYZE=true npm run build 查看 JavaScript 的可视化地图 ------ 每个过大的库、每个庞大的组件。这就像性能问题的 X 光片。
通过动态导入、条件加载和包分析,你可以毫不费力地将初始包缩小 50-70%。
6. React hydration(水合)也可能是问题所在
即使你小心处理 JavaScript 包,React 应用中还有一个性能杀手 ------ 水合。服务器向浏览器发送 HTML 后,React 需要 "水合" 它,即附加事件监听器,并使虚拟 DOM 与服务器渲染的标记协调。这个过程可能会阻塞交互性,影响性能。
问题是这样的:
jsx
// 传统 Next.js 页面,存在水合瓶颈
export default function ProductPage({ products }) {
return (
<div>
<Header /> {/* 必须先水合,用户才能交互 */}
<ProductGrid products={products} /> {/* 大型组件树 */}
<FilterSidebar /> {/* 复杂的交互组件 */}
<Footer /> {/* 不需要 JS 的静态内容 */}
{/* 所有内容同时水合,阻塞交互性 */}
</div>
);
}
在水合期间,浏览器的主线程会被阻塞,而 React 处理整个组件树。对于复杂页面,这在低端设备上可能需要数百毫秒甚至几秒,造成用户能看到 UI 但无法交互的令人沮丧的延迟。
解决办法:使用 React 服务器组件和部分水合
Next.js App Router 带来了 React 服务器组件,从根本上改变了这种动态,让你可以选择应用的哪些部分需要客户端 JavaScript:
jsx
// app/products/page.js - 服务器组件(不向客户端发送 JS)
import { ProductGrid } from './components/ProductGrid';
import { ClientSideFilter } from './components/ClientSideFilter';
// 这个组件在服务器上运行,只发送 HTML
export default async function ProductPage () {
// 数据获取在服务器上进行
const products = await fetchProducts();
return (
<div>
<h1>Products</h1>
{/* 静态部分仅作为 HTML 存在 */}
<ProductGrid products={products} />
{/* 仅交互部分需要水合 */}
<ClientSideFilter products={products} />
</div>
);
}
// components/ClientSideFilter.js
'use client'; // 标记为需要水合
export function ClientSideFilter({ products }) {
const [filters, setFilters] = useState({});
// 交互组件逻辑...
}
这种方法带来几个主要性能优势:
-
默认零 JavaScript------ 服务器组件只向浏览器发送 HTML,除非用 'use client' 明确标记
-
选择性水合 ------ 只有交互组件消耗客户端 JavaScript
-
流式渲染 ------ 页面的各个部分可以独立加载并变得可交互
-
减小包体积 ------ 服务器组件的代码永远不会发送到客户端
实施智能水合技术是个好开始,但如果你的应用不断重新获取相同的数据,就像失忆了一样,用户仍然会感到延迟。让我们谈谈缓存。
7. 没有在请求间有效缓存数据
缓存就像给你的应用一个好记性。它防止应用每次都要重新获取信息。但我见过很多 Next.js 应用把每个请求都当作第一次处理 ------ 特别是对于权限、用户数据或博客文章等内容。
糟糕的缓存不仅会减慢应用速度 ------ 还会浪费服务器资源。最常见的缓存错误往往很基础:
重复获取相同数据:
jsx
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// 每次组件挂载时运行------没有缓存!
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
每次页面加载都拉取新数据:
jsx
export async function getServerSideProps({ params }) {
// 每次请求都访问数据库
const posts = await db.posts.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
});
return { props: { posts } };
}
这就是我所说的 "系统失忆症"------ 应用在用户刷新或点击离开时就忘记了所有学到的东西。
解决办法:尽可能使用 SSG 和 SWR
有效的缓存在不同层面发挥作用:API 路由、页面渲染,甚至数据库查询。让我们看看如何让它为你工作:
使用 ISR 进行服务器端缓存
如果你的数据不是每秒都变化,就不要每秒都重新获取。使用增量静态再生(ISR)来提供预构建的页面,并偶尔刷新它们:
jsx
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return {
props: { post },
revalidate: 3600, // 最多每小时再生一次
};
}
export async function getStaticPaths() {
// 为热门帖子生成路径
const popularPosts = await fetchPopularPosts();
return {
paths: popularPosts.map((post) => ({
params: { slug: post.slug }
})),
fallback: 'blocking' // 按需生成其他页面
};
}
这能保持内容新鲜和快速,同时最小化服务器负载。
额外解决办法:在 API 上应用智能缓存控制头
对于消耗大的 API 操作,使用 unstable_cache 缓存服务器端逻辑:
jsx
// pages/api/posts.js
import { unstable_cache } from 'next/cache';
const getCachedPosts = unstable_cache(
async () => {
// 消耗大的数据库查询
return await db.posts.findMany({
include: {
author: true,
comments: { take: 5 },
tags: true
},
orderBy: { createdAt: 'desc' }
});
},
['posts-list'],
{
revalidate: 300, // 缓存 5 分钟
tags: ['posts']
}
);
export default async function handler(req, res) {
const posts = await getCachedPosts();
res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
res.json(posts);
}
现在你的服务器不必为相同的查询过度工作,用户也能获得更快的体验。
如果缓存得当,你的应用会感觉像提前知道了用户的下一步行动。但即使有完美的缓存,还有一个陷阱会拖慢一切 ------ 未优化的图像。
8. 媒体资源拖慢应用
我曾审计过一个 Next.js 应用,其中单个英雄图像有 4.2MB------ 而且在每个页面上都加载。为了让你有概念,这比大多数完整应用的整个 JavaScript 包还要大。
问题不仅仅是文件大小。处理不当的图像会导致布局偏移、延迟页面渲染、在解码时阻塞主线程,并使最大内容绘制(LCP)远远超出可接受范围。这就像看电影时视频不断缓冲 ------ 技术上能看,但体验很糟糕。
我经常看到的错误是这样的:
使用原始 标签:
jsx
export default function ProductCard({ product }) {
return (
<div className="product-card">
{/* 没有优化,没有懒加载,导致布局偏移 */}
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
急切加载所有内容:
jsx
export default function Gallery({ images }) {
return (
<div className="gallery">
{images.map((image, index) => (
// 所有 50 张图像同时加载,即使用户只看到 6 张
<img key={index} src={image.url} alt={image.caption} />
))}
</div>
);
}
这种策略向用户交付了远超他们实际需要的内容,破坏了性能和用户体验。
解决办法:使用 next/image 和响应式尺寸
Next.js 提供了一个图像组件,处理响应式尺寸、懒加载和格式转换(如 WebP/AVIF)。它更快、更易访问,并节省大量带宽。以下是有效使用它的方法:
基本优化
jsx
// components/ProductCard.js
import Image from 'next/image';
export default function ProductCard({ product }) {
return (
<div className="product-card">
<Image
src={product.imageUrl}
alt={product.name}
width={300}
height={200}
priority={product.featured} // 立即加载精选产品
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
className="rounded-lg object-cover"
/>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
这本身就能改善 LCP、防止布局偏移,并帮助用户更快开始交互。
响应式英雄图像
对于在不同屏幕上显示不同尺寸的图像:
jsx
// components/HeroSection.js
import Image from 'next/image';
export default function HeroSection () {
return (
<div className="hero relative h-screen">
<Image
src="/hero-image.jpg"
alt="Hero image"
fill
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center">
<h1 className="text-white text-6xl font-bold">Welcome</h1>
</div>
</div>
);
}
sizes 属性确保浏览器为每个屏幕尺寸选择最佳版本,在小设备上节省带宽。
带懒加载的智能画廊
对于图像画廊,实现渐进式加载:
jsx
// components/ImageGallery.js
import Image from 'next/image';
import { useState } from 'react';
export default function ImageGallery({ images }) {
const [visibleCount, setVisibleCount] = useState(6);
const loadMore = () => {
setVisibleCount(prev => Math.min(prev + 6, images.length));
};
return (
<div className="gallery">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{images.slice(0, visibleCount).map((image, index) => (
<div key={image.id} className="aspect-square relative">
<Image
src={image.url}
alt={image.caption}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover rounded-lg"
priority={index < 6} // 优先加载前 6 张图像
/>
</div>
))}
</div>
{visibleCount < images.length && (
<button
onClick={loadMore}
className="mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Load More ({images.length - visibleCount} remaining)
</button>
)}
</div>
);
}
这样,用户只下载他们看到的内容 ------ 提高性能并减少移动设备上的内存使用。
总而言之 ------ 如果你在 Next.js 项目中不使用 组件,你就错过了巨大的性能提升。优化你的图像,用户会立即感受到差异。
为什么在移动设备上优化更为重要
性能问题影响所有人 ------ 但移动用户受到的影响最严重。这就是为什么你的 Next.js 应用需要特别友好的移动体验:
-
网络速度慢 ------ 许多移动用户仍然使用 3G 或不稳定的 4G 网络。在宽带下 200ms 加载完的 500KB JavaScript 包,在移动网络上可能需要超过 2 秒。
-
CPU 较弱 ------ 移动设备的处理能力远低于桌面。在你的 MacBook 上 300ms 运行完的 JavaScript,在廉价安卓手机上可能需要 1.5 秒 ------ 只是为了水合一个页面。
-
内存限制严格 ------ 移动浏览器更容易崩溃和频繁垃圾回收,特别是当你的应用依赖大的包或重型图像时。
这对你的 Next.js 性能策略意味着:
-
最小化包体积 ------ 这不是奢侈品,而是必需品
-
优化图像 ------ 臃肿的英雄图像不仅减慢页面速度,还可能消耗用户的实际流量
-
使用智能加载状态 ------ 在较慢的网络上,感知性能更重要
专业提示 ------ 如果你的应用在廉价手机上通过 3G 网络运行良好,那么在其他地方都会流畅运行。
衡量关键指标:如何优化性能
Next.js 中的性能优化不是选择一个解决方案 ------ 而是识别问题所在,并有条不紊地解决。事实是,性能工作不是一个勾选框,而是开发速度和用户体验之间持续的平衡。
每个新功能、每个额外的依赖项、每个在截止日期压力下走的捷径,都可能慢慢侵蚀你已经取得的进展。最好的方法是不要把性能当作事后才考虑的事情。从一开始就考虑它。
不要盲目优化。使用以下工具:
-
Next.js 内置分析,用于核心网络指标
-
Lighthouse CI,用于 CI/CD 管道中的自动化性能测试
-
真实用户监控(RUM),了解实际用户体验
-
包分析器,及早发现依赖膨胀
我们在本指南中涵盖的大部分内容 ------ 缓存、图像、代码分割和 SSR 策略 ------ 可以解决大约 80% 的 Next.js 性能问题。剩下的 20% 通常涉及更复杂的优化,如边缘渲染、CDN 策略、查询优化,有时甚至是全面的架构调整。
但不要从边缘情况开始。先关注大的改进点。
结论
性能的棘手之处在于:应用实际有多快和用户感觉它有多快之间存在差距。你的应用可能在技术上 2 秒内加载完成 ------ 但如果用户在 1.8 秒内都盯着空白屏幕,那感觉会非常慢。感知和指标同样重要。
记住这一点。如果感觉快,那就是快 ------ 至少对用户来说是这样。
所以要牢记这一点进行开发。添加加载状态,显示占位符,给用户视觉反馈。这样,当有人访问你的应用时,他们不仅会看到速度 ------ 还会感受到速度。
LogRocket:全面监控生产环境中的 Next.js 应用
调试 Next 应用可能很困难,特别是当用户遇到难以重现的问题时。如果你有兴趣监控和跟踪状态、自动显示 JavaScript 错误、跟踪缓慢的网络请求和组件加载时间,可以试试 LogRocket。