欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Next.js應(yīng)用變慢的8個(gè)原因及解決辦法

 更新時(shí)間:2025年07月17日 08:31:59   作者:周尛先森  
Next.js?應(yīng)用變慢的情況比你想象的更常見,過長的加載時(shí)間會讓用戶感到沮喪,降低參與度,在本文中,我將指出?Next.js?應(yīng)用中?8?個(gè)常見的性能問題,并分享清晰、實(shí)用的解決辦法,幫助你打造更快、更流暢的用戶體驗(yàn),讓用戶切實(shí)感受到差異,需要的朋友可以參考下

引言

Next.js 應(yīng)用變慢的情況比你想象的更常見。過長的加載時(shí)間會讓用戶感到沮喪,降低參與度。但大多數(shù)性能問題都可以歸結(jié)為幾個(gè)常見原因 —— 從繁重的數(shù)據(jù)獲取、路由延遲到過大的包體積、緩存錯(cuò)誤和未優(yōu)化的圖像。

在本文中,我將指出 Next.js 應(yīng)用中 8 個(gè)常見的性能問題,并分享清晰、實(shí)用的解決辦法,幫助你打造更快、更流暢的用戶體驗(yàn),讓用戶切實(shí)感受到差異。

我假設(shè)你已經(jīng)基本掌握 React 組件、useState 和 useEffect 等鉤子,以及 Next.js 路由和數(shù)據(jù)獲取的基礎(chǔ)知識。你還應(yīng)該熟悉使用瀏覽器開發(fā)者工具和在命令行中運(yùn)行構(gòu)建命令。如果這些聽起來不熟悉,你可能需要先復(fù)習(xí)一下,不過我會盡量把解釋寫得簡單明了。

快速說明一下 —— 我會在示例中使用 getServerSideProps,因?yàn)樵S多現(xiàn)有項(xiàng)目仍然依賴 Pages Router,而且性能問題并非 Next.js 特定版本所獨(dú)有。即使語法略有變化,這些優(yōu)化原則同樣適用于使用較新的 App Router 的情況。我們的目標(biāo)是專注于最重要的解決辦法,無論你的項(xiàng)目配置如何。

1. 感知性能不足

我們來談?wù)劯兄阅?—— 即應(yīng)用給用戶的感覺有多快,而不僅僅是它實(shí)際有多快。Jakob Nielsen 在他 1993 年的《可用性工程》一書中,為用戶耐心設(shè)定了一些經(jīng)典基準(zhǔn):

  • 0.1 秒 —— 系統(tǒng)感覺 “即時(shí)響應(yīng)” 的極限。在此范圍內(nèi),用戶不會察覺到延遲。
  • 1.0 秒 —— 保持用戶思維流暢的上限,盡管他們會注意到延遲。
  • 10 秒 —— 用戶完全失去注意力之前的最長時(shí)間。

如果你的 Next.js 應(yīng)用顯示內(nèi)容的時(shí)間超過 1 秒,對用戶來說它就正式 “變慢” 了,即使數(shù)據(jù)實(shí)際上正在后臺加載。而等待 10 秒?那幾乎是數(shù)字時(shí)代的永恒,足以讓用戶徹底失去興趣。

但這里有個(gè)關(guān)鍵點(diǎn):你的應(yīng)用真的慢嗎?還是只是 “感覺” 慢?

Reddit 上有個(gè)討論:“為什么大多數(shù) Next.js 應(yīng)用都這么慢?”
用戶說:“每次我訪問一個(gè)網(wǎng)站,點(diǎn)擊鏈接要等 2 秒,我就會想‘這肯定是個(gè) Next 應(yīng)用’,查看代碼后發(fā)現(xiàn)確實(shí)如此。我猜是服務(wù)器組件在等待數(shù)據(jù)獲取,但初始加載真的很慢(之后就好了)。我見過一些優(yōu)化得很好的 Next 應(yīng)用,初始加載很快,但 90% 我遇到的都有加載慢的問題。是新手開發(fā)者的問題還是框架本身的問題?”

這就是感知性能不足的體現(xiàn)。有時(shí)候,無論數(shù)據(jù)獲取需要多長時(shí)間,你處理等待的方式都會產(chǎn)生巨大差異。

解決辦法:使用加載狀態(tài)和 React Suspense

關(guān)鍵在于立即向用戶展示一些內(nèi)容 —— 即使不是最終內(nèi)容。設(shè)計(jì)良好的加載狀態(tài)可以讓 2 秒的等待感覺比盯著空白屏幕 1 秒更快。

React Suspense 讓在 Next.js 中實(shí)現(xiàn)這一點(diǎn)變得容易。你可以這樣包裹組件,在內(nèi)容加載時(shí)顯示占位符:

import { Suspense } from 'react';

export default function Dashboard () {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<DashboardSkeleton />}>
        <DashboardContent />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

當(dāng)用戶重新加載頁面時(shí),他們會立即看到占位符,讓他們知道系統(tǒng)正在處理:
( Dashboard 重新加載演示:Next.js Suspense + 骨架屏加載演示)
(展示內(nèi)容:John Doe 的儀表盤,包含用戶信息、總用戶數(shù)、收入、最近活動等,加載時(shí)顯示骨架屏)

2. Next.js 混合渲染拖慢速度

說完了感知性能,我們來看看一些實(shí)際的性能問題。如你所知,Next.js 不僅是關(guān)于服務(wù)器端渲染 —— 也不純粹是單頁應(yīng)用。它是一個(gè)混合框架,兼顧兩者的優(yōu)點(diǎn)。但有時(shí),也會帶來兩者的缺點(diǎn)。

這種混合模式很強(qiáng)大。它讓你的應(yīng)用可以為 SEO 和快速初始加載提供完全渲染的頁面,然后切換到 SPA 行為以實(shí)現(xiàn)流暢的客戶端導(dǎo)航。但這也意味著你要同時(shí)處理兩種不同的性能特征 —— 這正是問題可能出現(xiàn)的地方。

理解兩種模式

首次加載時(shí),你的 Next.js 應(yīng)用表現(xiàn)得像傳統(tǒng)的服務(wù)器渲染網(wǎng)站。它在服務(wù)器上獲取數(shù)據(jù),渲染完整的 HTML,然后發(fā)送到瀏覽器。這對 SEO 和快速顯示有意義的內(nèi)容非常有利:

// 首次訪問:服務(wù)器承擔(dān)所有繁重工作
export async function getServerSideProps() {
  // 這在服務(wù)器上運(yùn)行,會阻塞響應(yīng)
  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 模式。點(diǎn)擊操作會觸發(fā)客戶端導(dǎo)航,無需整頁重新加載 —— 就像 React Router 一樣:

// 后續(xù)導(dǎo)航:純 SPA 行為
function Dashboard({ userData }) {
  const router = useRouter();

  const goToProfile = () => {
    // 不訪問服務(wù)器——純客戶端導(dǎo)航
    router.push('/profile');
  };

  return (
    <div>
      <h1>Welcome, {userData.name}</h1>
      <button onClick={goToProfile}>View Profile</button>
    </div>
  );
}

性能問題出現(xiàn)的地方

棘手之處在于,你實(shí)際上在運(yùn)行兩個(gè)應(yīng)用:

  • 處理初始請求的服務(wù)器渲染應(yīng)用
  • 處理導(dǎo)航和交互的客戶端 SPA

每個(gè)都有自己的性能特征,如果沒有適當(dāng)優(yōu)化,每個(gè)都可能拖慢整個(gè)應(yīng)用:

  • 在服務(wù)器上,緩慢的數(shù)據(jù)庫查詢或阻塞操作會延遲 HTML 響應(yīng)
  • 在客戶端,過大的 JavaScript 包或低效的 API 調(diào)用會阻礙流暢的導(dǎo)航

更糟的是,這些問題會相互疊加。假設(shè)用戶訪問你的主頁(服務(wù)器渲染),然后點(diǎn)擊進(jìn)入儀表盤(客戶端)。這個(gè)儀表盤需要一個(gè) 2MB 的 JavaScript 包 —— 而且還沒有緩存。那么用戶就必須等待包加載和客戶端數(shù)據(jù)獲取:

// 性能陷阱:繁重的客戶端頁面
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(() => {
    // 導(dǎo)航后在客戶端獲取數(shù)據(jù)
    fetchDashboardData().then(setData);
  }, []);

  // 用戶需要等待包加載 + 數(shù)據(jù)加載
  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <HeavyChart data={data.charts} />
      <ComplexTable data={data.tables} />
      <RichEditor content={data.content} />
    </div>
  );
}

解決辦法:同時(shí)優(yōu)化兩種模式

當(dāng)服務(wù)器和客戶端都經(jīng)過優(yōu)化時(shí),你就能獲得兩者的最佳效果 —— 快速的初始加載、良好的 SEO,以及流暢的、類應(yīng)用的導(dǎo)航。但如果忽視任何一方,整個(gè)體驗(yàn)都會變得遲緩。

現(xiàn)在我們已經(jīng)了解了混合渲染如何引入隱藏的性能成本,接下來讓我們聚焦服務(wù)器端一個(gè)最大的罪魁禍?zhǔn)?—— 緩慢的、順序的數(shù)據(jù)獲取。

3. 數(shù)據(jù)獲取過慢且按順序執(zhí)行

你點(diǎn)擊應(yīng)用中的某個(gè)東西,然后…… 什么都沒有。沒有反饋,沒有內(nèi)容。十有八九,問題在于同步數(shù)據(jù)獲取。這是指應(yīng)用 “禮貌地” 一次加載一條數(shù)據(jù)。以下是典型 Next.js 配置中的情況:

// 慢方法——每個(gè)請求都要等前一個(gè)完成
export async function getServerSideProps({ req }) {
  // 步驟 1:獲取用戶(300ms)
  const user = await fetchUser(req.session.userId);

  // 步驟 2:等用戶數(shù)據(jù),再獲取個(gè)人資料(400ms)
  const profile = await fetchUserProfile(user.id);

  // 步驟 3:等個(gè)人資料,再獲取儀表盤數(shù)據(jù)(600ms)
  const dashboardData = await fetchDashboardData(user.id, profile.preferences);

  // 步驟 4:等儀表盤數(shù)據(jù),再獲取通知(200ms)
  const notifications = await fetchNotifications(user.id);

  // 總時(shí)間:300 + 400 + 600 + 200 = 1500ms(1.5 秒!)
  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications
    }
  };
}

這種方法把本可以 4800ms 的頁面加載變成了 1.5 秒的加載。每個(gè) await 都在說 ——“暫停所有操作 —— 在這個(gè)完成之前我們不繼續(xù)。” 但這些請求真的需要一個(gè)接一個(gè)地進(jìn)行嗎?

解決辦法:使用 Promise.all ()

如果你的請求彼此不依賴,就沒有理由不能同時(shí)運(yùn)行。這就是 Promise.all () 的用武之地:

export async function getServerSideProps({ req }) {
  // 步驟 1:先獲取用戶(仍然是其他請求所需要的)
  const user = await fetchUser(req.session.userId);

  // 步驟 2:并行獲取其他所有數(shù)據(jù)
  const [profile, dashboardData, notifications] = await Promise.all([
    fetchUserProfile(user.id),          // 400ms
    fetchDashboardData(user.id),        // 600ms
    fetchNotifications(user.id)         // 200ms
  ]);

  // 總時(shí)間:300ms(用戶) + 600ms(最長的并行請求) = 900ms
  // 我們節(jié)省了 600ms!

  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications
    }
  };
}

這個(gè)簡單的改變就減少了 600ms—— 而且這只是一次頁面加載。

額外解決辦法:全局?jǐn)?shù)據(jù)的并行獲取

你可以更進(jìn)一步。如果某些數(shù)據(jù)不依賴于用戶,比如系統(tǒng)范圍的設(shè)置或服務(wù)器狀態(tài),你可以在獲取用戶數(shù)據(jù)的同時(shí)獲取這些數(shù)據(jù):

export async function getServerSideProps({ req }) {
  // 并行獲取與用戶無關(guān)的數(shù)據(jù)和用戶數(shù)據(jù)
  const [user, globalSettings, systemStatus] = await Promise.all([
    fetchUser(req.session.userId),
    fetchGlobalSettings(),              // 不需要用戶數(shù)據(jù)
    fetchSystemStatus()                 // 不需要用戶數(shù)據(jù)
  ]);

  // 現(xiàn)在并行獲取依賴于用戶的數(shù)據(jù)
  const [profile, dashboardData, notifications] = await Promise.all([
    fetchUserProfile(user.id),
    fetchDashboardData(user.id),
    fetchNotifications(user.id)
  ]);

  return {
    props: {
      user,
      profile,
      dashboardData,
      notifications,
      globalSettings,
      systemStatus
    }
  };
}

通過更智能的并行化,你可以減少加載時(shí)間,提高響應(yīng)速度 —— 不需要復(fù)雜的工具或庫。只需要更好地使用 JavaScript。

即使數(shù)據(jù)獲取更快,你的應(yīng)用可能仍然感覺不流暢。為什么?因?yàn)槟愕穆酚尚袨榭赡茉谶M(jìn)行不必要的完整服務(wù)器往返。讓我們接下來看看這個(gè)問題。

4. 路由觸發(fā)不必要的服務(wù)器往返

使用 Next.js App Router 時(shí),每次導(dǎo)航都可能觸發(fā)服務(wù)器端渲染 —— 即使是本可以(也應(yīng)該)在客戶端處理的路由。這不僅低效,而且肯定會讓快速的應(yīng)用感覺變慢。

假設(shè)你有一個(gè)用戶在瀏覽產(chǎn)品目錄。在傳統(tǒng)的 SPA 中,在產(chǎn)品頁面之間點(diǎn)擊應(yīng)該是即時(shí)的。JavaScript 處理狀態(tài)更新,URL 變化而無需重新加載頁面。

但如果 Next.js App Router 配置不當(dāng),每次點(diǎn)擊都可能往返服務(wù)器。情況如下:

// app/products/[id]/page.js
// 每次訪問產(chǎn)品頁面時(shí),這都會在服務(wù)器上運(yùn)行
export default async function ProductPage({ params }) {
  // 每次導(dǎo)航都向服務(wù)器發(fā)起網(wǎng)絡(luò)請求
  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>
  );
}

在這種設(shè)置下,每次點(diǎn)擊都要經(jīng)過同樣緩慢的流程:

  • 點(diǎn)擊產(chǎn)品鏈接
  • 瀏覽器向服務(wù)器發(fā)出請求
  • 服務(wù)器獲取產(chǎn)品數(shù)據(jù)(數(shù)據(jù)庫調(diào)用)
  • 服務(wù)器渲染 HTML
  • 服務(wù)器將 HTML 發(fā)送到瀏覽器
  • 瀏覽器顯示頁面

這有六個(gè)步驟,每個(gè)都可能有延遲。乘以十次產(chǎn)品瀏覽,就有十次服務(wù)器請求 —— 而不是十次即時(shí)的頁面轉(zhuǎn)換。

解決辦法:轉(zhuǎn)向客戶端路由

為避免這些往返,盡可能將路由轉(zhuǎn)移到客戶端。方法如下:

// 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(() => {
    // 在客戶端獲取數(shù)據(jù)——沒有服務(wù)器往返
    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>
  );
}

這樣,產(chǎn)品頁面之間的導(dǎo)航會立即發(fā)生 —— 數(shù)據(jù)在后臺加載,無需在服務(wù)器上重新渲染整個(gè)頁面。

何時(shí)使用服務(wù)器端 vs 客戶端渲染

快速指南:
使用服務(wù)器端渲染(SSR)當(dāng):

  • SEO 至關(guān)重要(產(chǎn)品頁面、博客文章)
  • 你要顯示用戶特定的敏感數(shù)據(jù)
  • 初始頁面加載速度比導(dǎo)航速度更重要
  • 內(nèi)容不經(jīng)常變化

使用客戶端渲染(CSR)當(dāng):

  • 用戶頻繁在相似頁面之間導(dǎo)航
  • 你可以有效地緩存數(shù)據(jù)
  • SEO 不是優(yōu)先事項(xiàng)(用戶儀表盤、管理面板)
  • 你想要即時(shí)的、類應(yīng)用的導(dǎo)航

額外解決辦法:混合渲染

有時(shí),你需要 SSR 用于初始頁面加載,但希望后續(xù)交互受益于 CSR。Next.js App Router 允許你結(jié)合兩者:

// app/products/[id]/page.js
// 為 SEO 進(jìn)行服務(wù)器渲染初始頁面
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);

  // 后續(xù)導(dǎo)航在客戶端進(jìn)行
  const router = useRouter();

  const navigateToProduct = async (newId) => {
    // 立即更新 URL(感覺是即時(shí)的)
    router.push(`/products/${newId}`);

    // 在后臺獲取新數(shù)據(jù)
    const newProduct = await fetch(`/api/products/${newId}`).then(res => res.json());
    setProduct(newProduct);
  };

  return (
    <div>
      <ProductDetails product={product} />
      <RelatedProducts onProductClick={navigateToProduct} />
    </div>
  );
}

但即使有智能路由和優(yōu)化的數(shù)據(jù)獲取,如果應(yīng)用拖著過大的 JavaScript 包,仍然會感覺遲緩。讓我們談?wù)劄槭裁磿霈F(xiàn)這個(gè)問題 —— 以及如何解決。

5. JavaScript 包體積過大

我曾在一個(gè)黑客馬拉松期間加入一個(gè) Next.js 項(xiàng)目,當(dāng)時(shí)主 JavaScript 包的大小是 2.3 MB。之前的開發(fā)者為了使用幾個(gè)函數(shù)就導(dǎo)入了整個(gè)庫。沒有代碼分割。沒有動態(tài)導(dǎo)入。只是把一個(gè)巨大的負(fù)載丟給每個(gè)用戶。

JavaScript 包體積直接影響你的交互時(shí)間(TTI)—— 衡量頁面何時(shí)完全可用的指標(biāo)。包越大,用戶盯著加載 spinner 的時(shí)間就越長。

以下是經(jīng)常導(dǎo)致包膨脹的原因:

// 第一個(gè)包膨脹器:導(dǎo)入整個(gè)庫
import _ from 'lodash'; // 導(dǎo)入整個(gè) 70KB 的庫
import * as dateFns from 'date-fns'; // 另一個(gè)大型導(dǎo)入

// 第二個(gè)包膨脹器:在各處導(dǎo)入重型組件
import { DataVisualization } from './DataVisualization'; // 500KB 組件
import { VideoPlayer } from './VideoPlayer'; // 300KB 組件
import { RichTextEditor } from './RichTextEditor'; // 400KB 組件

export default function HomePage () {
  return (
    <div>
      <h1>Welcome</h1>
      {/* 這些組件可能在初始加載時(shí)甚至不可見 */}
      <DataVisualization />
      <VideoPlayer />
      <RichTextEditor />
    </div>
  );
}

這種方法會向每個(gè)用戶加載所有內(nèi)容 —— 即使他們從未與這些組件交互。幸運(yùn)的是,有更好的方法。

解決辦法:代碼分割和動態(tài)導(dǎo)入

Next.js 默認(rèn)支持智能代碼分割。但要充分利用它,你需要使用動態(tài)導(dǎo)入來僅在需要時(shí)加載代碼。

基于路由的代碼分割

默認(rèn)情況下,Next.js 按路由分割代碼。但你可以使用 next/dynamic 進(jìn)一步優(yōu)化:

// pages/dashboard.js - 僅當(dāng)用戶訪問 /dashboard 時(shí)加載
import dynamic from 'next/dynamic';

// 僅在需要時(shí)加載重型組件
const AnalyticsChart = dynamic(() => import('../components/AnalyticsChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // 對僅客戶端組件跳過服務(wù)器端渲染
});

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>
  );
}

通過這種模式,用戶只在請求時(shí)才下載圖表或?qū)С鲞壿?,而不是之前?/p>

基于組件的代碼分割

如果你有在路由之間共享但僅在特定情況下需要的組件,也可以延遲加載它們:

// components/ConditionalFeatures.js
import dynamic from 'next/dynamic';

// 僅當(dāng)用戶有高級訂閱時(shí)加載
const PremiumChart = dynamic(() => import('./PremiumChart'), {
  loading: () => <div>Loading premium features...</div>
});

// 僅當(dāng)用戶點(diǎn)擊“高級設(shè)置”時(shí)加載
const AdvancedSettings = dynamic(() => import('./AdvancedSettings'));

export function ConditionalFeatures({ user, showAdvanced }) {
  return (
    <div>
      {user.isPremium && <PremiumChart />}
      {showAdvanced && <AdvancedSettings />}
    </div>
  );
}

這確保你的用戶不會為他們甚至無法訪問的功能付出性能代價(jià)。

額外解決辦法:使用 @next/bundle-analyzer 分析包

要查看是什么占用了你的包體積,使用官方的包分析器:

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
});

module.exports = withBundleAnalyzer({
  // 你的 Next.js 配置
});

運(yùn)行 ANALYZE=true npm run build 查看 JavaScript 的可視化地圖 —— 每個(gè)過大的庫、每個(gè)龐大的組件。這就像性能問題的 X 光片。

通過動態(tài)導(dǎo)入、條件加載和包分析,你可以毫不費(fèi)力地將初始包縮小 50-70%。

6. React hydration(水合)也可能是問題所在

即使你小心處理 JavaScript 包,React 應(yīng)用中還有一個(gè)性能殺手 —— 水合。服務(wù)器向?yàn)g覽器發(fā)送 HTML 后,React 需要 “水合” 它,即附加事件監(jiān)聽器,并使虛擬 DOM 與服務(wù)器渲染的標(biāo)記協(xié)調(diào)。這個(gè)過程可能會阻塞交互性,影響性能。

問題是這樣的:

// 傳統(tǒng) Next.js 頁面,存在水合瓶頸
export default function ProductPage({ products }) {
  return (
    <div>
      <Header /> {/* 必須先水合,用戶才能交互 */}
      <ProductGrid products={products} /> {/* 大型組件樹 */}
      <FilterSidebar /> {/* 復(fù)雜的交互組件 */}
      <Footer /> {/* 不需要 JS 的靜態(tài)內(nèi)容 */}

      {/* 所有內(nèi)容同時(shí)水合,阻塞交互性 */}
    </div>
  );
}

在水合期間,瀏覽器的主線程會被阻塞,而 React 處理整個(gè)組件樹。對于復(fù)雜頁面,這在低端設(shè)備上可能需要數(shù)百毫秒甚至幾秒,造成用戶能看到 UI 但無法交互的令人沮喪的延遲。

解決辦法:使用 React 服務(wù)器組件和部分水合

Next.js App Router 帶來了 React 服務(wù)器組件,從根本上改變了這種動態(tài),讓你可以選擇應(yīng)用的哪些部分需要客戶端 JavaScript:

// app/products/page.js - 服務(wù)器組件(不向客戶端發(fā)送 JS)
import { ProductGrid } from './components/ProductGrid';
import { ClientSideFilter } from './components/ClientSideFilter';

// 這個(gè)組件在服務(wù)器上運(yùn)行,只發(fā)送 HTML
export default async function ProductPage () {
  // 數(shù)據(jù)獲取在服務(wù)器上進(jìn)行
  const products = await fetchProducts();

  return (
    <div>
      <h1>Products</h1>
      {/* 靜態(tài)部分僅作為 HTML 存在 */}
      <ProductGrid products={products} />

      {/* 僅交互部分需要水合 */}
      <ClientSideFilter products={products} />
    </div>
  );
}

// components/ClientSideFilter.js
'use client'; // 標(biāo)記為需要水合

export function ClientSideFilter({ products }) {
  const [filters, setFilters] = useState({});
  // 交互組件邏輯...
}

這種方法帶來幾個(gè)主要性能優(yōu)勢:

  • 默認(rèn)零 JavaScript—— 服務(wù)器組件只向?yàn)g覽器發(fā)送 HTML,除非用 'use client' 明確標(biāo)記
  • 選擇性水合 —— 只有交互組件消耗客戶端 JavaScript
  • 流式渲染 —— 頁面的各個(gè)部分可以獨(dú)立加載并變得可交互
  • 減小包體積 —— 服務(wù)器組件的代碼永遠(yuǎn)不會發(fā)送到客戶端

實(shí)施智能水合技術(shù)是個(gè)好開始,但如果你的應(yīng)用不斷重新獲取相同的數(shù)據(jù),就像失憶了一樣,用戶仍然會感到延遲。讓我們談?wù)劸彺妗?/p>

7. 沒有在請求間有效緩存數(shù)據(jù)

緩存就像給你的應(yīng)用一個(gè)好記性。它防止應(yīng)用每次都要重新獲取信息。但我見過很多 Next.js 應(yīng)用把每個(gè)請求都當(dāng)作第一次處理 —— 特別是對于權(quán)限、用戶數(shù)據(jù)或博客文章等內(nèi)容。

糟糕的緩存不僅會減慢應(yīng)用速度 —— 還會浪費(fèi)服務(wù)器資源。最常見的緩存錯(cuò)誤往往很基礎(chǔ):

重復(fù)獲取相同數(shù)據(jù):

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // 每次組件掛載時(shí)運(yùn)行——沒有緩存!
  useEffect(() => {
    fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
  }, [userId]);

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

每次頁面加載都拉取新數(shù)據(jù):

export async function getServerSideProps({ params }) {
  // 每次請求都訪問數(shù)據(jù)庫
  const posts = await db.posts.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' }
  });

  return { props: { posts } };
}

這就是我所說的 “系統(tǒng)失憶癥”—— 應(yīng)用在用戶刷新或點(diǎn)擊離開時(shí)就忘記了所有學(xué)到的東西。

解決辦法:盡可能使用 SSG 和 SWR

有效的緩存在不同層面發(fā)揮作用:API 路由、頁面渲染,甚至數(shù)據(jù)庫查詢。讓我們看看如何讓它為你工作:

使用 ISR 進(jìn)行服務(wù)器端緩存

如果你的數(shù)據(jù)不是每秒都變化,就不要每秒都重新獲取。使用增量靜態(tài)再生(ISR)來提供預(yù)構(gòu)建的頁面,并偶爾刷新它們:

// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);

  return {
    props: { post },
    revalidate: 3600, // 最多每小時(shí)再生一次
  };
}

export async function getStaticPaths() {
  // 為熱門帖子生成路徑
  const popularPosts = await fetchPopularPosts();

  return {
    paths: popularPosts.map((post) => ({
      params: { slug: post.slug }
    })),
    fallback: 'blocking' // 按需生成其他頁面
  };
}

這能保持內(nèi)容新鮮和快速,同時(shí)最小化服務(wù)器負(fù)載。

額外解決辦法:在 API 上應(yīng)用智能緩存控制頭

對于消耗大的 API 操作,使用 unstable_cache 緩存服務(wù)器端邏輯:

// pages/api/posts.js
import { unstable_cache } from 'next/cache';

const getCachedPosts = unstable_cache(
  async () => {
    // 消耗大的數(shù)據(jù)庫查詢
    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);
}

現(xiàn)在你的服務(wù)器不必為相同的查詢過度工作,用戶也能獲得更快的體驗(yàn)。

如果緩存得當(dāng),你的應(yīng)用會感覺像提前知道了用戶的下一步行動。但即使有完美的緩存,還有一個(gè)陷阱會拖慢一切 —— 未優(yōu)化的圖像。

8. 媒體資源拖慢應(yīng)用

我曾審計(jì)過一個(gè) Next.js 應(yīng)用,其中單個(gè)英雄圖像有 4.2MB—— 而且在每個(gè)頁面上都加載。為了讓你有概念,這比大多數(shù)完整應(yīng)用的整個(gè) JavaScript 包還要大。

問題不僅僅是文件大小。處理不當(dāng)?shù)膱D像會導(dǎo)致布局偏移、延遲頁面渲染、在解碼時(shí)阻塞主線程,并使最大內(nèi)容繪制(LCP)遠(yuǎn)遠(yuǎn)超出可接受范圍。這就像看電影時(shí)視頻不斷緩沖 —— 技術(shù)上能看,但體驗(yàn)很糟糕。

我經(jīng)??吹降腻e(cuò)誤是這樣的:

使用原始 標(biāo)簽:

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/* 沒有優(yōu)化,沒有懶加載,導(dǎo)致布局偏移 */}
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

急切加載所有內(nèi)容:

export default function Gallery({ images }) {
  return (
    <div className="gallery">
      {images.map((image, index) => (
        // 所有 50 張圖像同時(shí)加載,即使用戶只看到 6 張
        <img key={index} src={image.url} alt={image.caption} />
      ))}
    </div>
  );
}

這種策略向用戶交付了遠(yuǎn)超他們實(shí)際需要的內(nèi)容,破壞了性能和用戶體驗(yàn)。

解決辦法:使用 next/image 和響應(yīng)式尺寸

Next.js 提供了一個(gè)圖像組件,處理響應(yīng)式尺寸、懶加載和格式轉(zhuǎn)換(如 WebP/AVIF)。它更快、更易訪問,并節(jié)省大量帶寬。以下是有效使用它的方法:

基本優(yōu)化

// 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} // 立即加載精選產(chǎn)品
        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、防止布局偏移,并幫助用戶更快開始交互。

響應(yīng)式英雄圖像

對于在不同屏幕上顯示不同尺寸的圖像:

// 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 屬性確保瀏覽器為每個(gè)屏幕尺寸選擇最佳版本,在小設(shè)備上節(jié)省帶寬。

帶懶加載的智能畫廊

對于圖像畫廊,實(shí)現(xiàn)漸進(jìn)式加載:

// 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} // 優(yōu)先加載前 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>
  );
}

這樣,用戶只下載他們看到的內(nèi)容 —— 提高性能并減少移動設(shè)備上的內(nèi)存使用。

總而言之 —— 如果你在 Next.js 項(xiàng)目中不使用 組件,你就錯(cuò)過了巨大的性能提升。優(yōu)化你的圖像,用戶會立即感受到差異。

為什么在移動設(shè)備上優(yōu)化更為重要

性能問題影響所有人 —— 但移動用戶受到的影響最嚴(yán)重。這就是為什么你的 Next.js 應(yīng)用需要特別友好的移動體驗(yàn):

  • 網(wǎng)絡(luò)速度慢 —— 許多移動用戶仍然使用 3G 或不穩(wěn)定的 4G 網(wǎng)絡(luò)。在寬帶下 200ms 加載完的 500KB JavaScript 包,在移動網(wǎng)絡(luò)上可能需要超過 2 秒。
  • CPU 較弱 —— 移動設(shè)備的處理能力遠(yuǎn)低于桌面。在你的 MacBook 上 300ms 運(yùn)行完的 JavaScript,在廉價(jià)安卓手機(jī)上可能需要 1.5 秒 —— 只是為了水合一個(gè)頁面。
  • 內(nèi)存限制嚴(yán)格 —— 移動瀏覽器更容易崩潰和頻繁垃圾回收,特別是當(dāng)你的應(yīng)用依賴大的包或重型圖像時(shí)。

這對你的 Next.js 性能策略意味著:

  • 最小化包體積 —— 這不是奢侈品,而是必需品
  • 優(yōu)化圖像 —— 臃腫的英雄圖像不僅減慢頁面速度,還可能消耗用戶的實(shí)際流量
  • 使用智能加載狀態(tài) —— 在較慢的網(wǎng)絡(luò)上,感知性能更重要

專業(yè)提示 —— 如果你的應(yīng)用在廉價(jià)手機(jī)上通過 3G 網(wǎng)絡(luò)運(yùn)行良好,那么在其他地方都會流暢運(yùn)行。

衡量關(guān)鍵指標(biāo):如何優(yōu)化性能

Next.js 中的性能優(yōu)化不是選擇一個(gè)解決方案 —— 而是識別問題所在,并有條不紊地解決。事實(shí)是,性能工作不是一個(gè)勾選框,而是開發(fā)速度和用戶體驗(yàn)之間持續(xù)的平衡。

每個(gè)新功能、每個(gè)額外的依賴項(xiàng)、每個(gè)在截止日期壓力下走的捷徑,都可能慢慢侵蝕你已經(jīng)取得的進(jìn)展。最好的方法是不要把性能當(dāng)作事后才考慮的事情。從一開始就考慮它。

不要盲目優(yōu)化。使用以下工具:

  • Next.js 內(nèi)置分析,用于核心網(wǎng)絡(luò)指標(biāo)
  • Lighthouse CI,用于 CI/CD 管道中的自動化性能測試
  • 真實(shí)用戶監(jiān)控(RUM),了解實(shí)際用戶體驗(yàn)
  • 包分析器,及早發(fā)現(xiàn)依賴膨脹

我們在本指南中涵蓋的大部分內(nèi)容 —— 緩存、圖像、代碼分割和 SSR 策略 —— 可以解決大約 80% 的 Next.js 性能問題。剩下的 20% 通常涉及更復(fù)雜的優(yōu)化,如邊緣渲染、CDN 策略、查詢優(yōu)化,有時(shí)甚至是全面的架構(gòu)調(diào)整。

但不要從邊緣情況開始。先關(guān)注大的改進(jìn)點(diǎn)。

結(jié)論

性能的棘手之處在于:應(yīng)用實(shí)際有多快和用戶感覺它有多快之間存在差距。你的應(yīng)用可能在技術(shù)上 2 秒內(nèi)加載完成 —— 但如果用戶在 1.8 秒內(nèi)都盯著空白屏幕,那感覺會非常慢。感知和指標(biāo)同樣重要。

記住這一點(diǎn)。如果感覺快,那就是快 —— 至少對用戶來說是這樣。

所以要牢記這一點(diǎn)進(jìn)行開發(fā)。添加加載狀態(tài),顯示占位符,給用戶視覺反饋。這樣,當(dāng)有人訪問你的應(yīng)用時(shí),他們不僅會看到速度 —— 還會感受到速度。

LogRocket:全面監(jiān)控生產(chǎn)環(huán)境中的 Next.js 應(yīng)用

調(diào)試 Next 應(yīng)用可能很困難,特別是當(dāng)用戶遇到難以重現(xiàn)的問題時(shí)。如果你有興趣監(jiān)控和跟蹤狀態(tài)、自動顯示 JavaScript 錯(cuò)誤、跟蹤緩慢的網(wǎng)絡(luò)請求和組件加載時(shí)間,可以試試 LogRocket。

以上就是Next.js應(yīng)用變慢的8個(gè)原因及解決辦法的詳細(xì)內(nèi)容,更多關(guān)于Next.js應(yīng)用變慢的原因及解決的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論