DEV Community

JH5
JH5

Posted on • Originally published at Medium

Google PageSpeed Insights 慘不忍睹的 Next.js 效能優化

Google PageSpeed Insights 慘不忍睹的 Next.js 效能優化

最近用Next.js打造了一個功能強大?的網站,但上線後放在 Google PageSpeed Insights 上一測,分數慘不忍睹… 尤其是最大內容繪製 (LCP) 時間竟然高達 15 秒?

這一篇將紀錄與分享一次完整的效能優化實戰,紀錄前陣子如何診斷問題與作架構調整,將 LCP 從 15.1 秒的「紅色警戒」成功降至 2 秒內的「綠色健康」範圍。

PageSpeed Insights Result

緩慢的元兇 —Client-Side Rendering

初始的 PageSpeed 報告如下:

  • Largest Contentful Paint (LCP): 15.1 s

  • Total Blocking Time (TBT): 1,190 ms

問題還滿明顯的,目前的頁面採用了典型的 CSR 模式:伺服器會先回傳一個基本的 HTML 空殼,頁面上顯示著 “載入中…”,然後所有內容都交給瀏覽器的 JavaScript 來處理。

CSR 的工作流程如下:

  1. 瀏覽器下載 HTML。

  2. 瀏覽器下載、解析並執行 JavaScript 檔案。

  3. JavaScript 發出 API 請求以獲取資料。

  4. 等待資料回傳。

  5. JavaScript 根據資料渲染出完整的資料列表。

這個漫長的鏈條導致了兩個主要問題:

  • 高 LCP:直到第 5 步完成,頁面的「最大內容」(資料列表)才被繪製出來,耗時極長。

  • 高 TBT:在第 2、3、5 步中,瀏覽器主線程被大量 JavaScript 任務阻塞,導致頁面無法即時回應使用者操作。

擁抱 (Server-Side Rendering, SSR)

為了解決這個問題,後來改用 CSR 到 SSR。

SSR 的核心思想是:將原本在客戶端進行的資料獲取和內容渲染工作,轉移到伺服器端完成。

SSR 的工作流程:

  1. 瀏覽器發出頁面請求。

  2. Next.js 伺服器接收請求,並在伺服器環境下獲取資料。

  3. 伺服器使用獲取到的資料,將頁面完整渲染成 HTML 字串。

  4. 伺服器將這個包含所有內容的完整 HTML 回傳給瀏覽器。

  5. 瀏覽器接收到 HTML 後,可以直接繪製出完整頁面。

這樣一來,LCP 的時間點被大幅提前到第 5 步,因為瀏覽器不再需要等待 JavaScript 執行和 API 請求。

Next.js App Router 中的 SSR 實作

在 Next.js 的 App Router 架構下,改成SSR也是非常直覺,頁面元件預設就是伺服器元件 (Server Components)。

修改前 (CSR 模式):

// src/app/page.tsx (Before)  
'''use client'''; // 必須標記為客戶端元件
Enter fullscreen mode Exit fullscreen mode
import { useState, useEffect } from 'react';  
import JobList from '@/components/JobList';
Enter fullscreen mode Exit fullscreen mode
export default function HomePage() {  
  const [jobs, setJobs] = useState([]);  
  const [loading, setLoading] = useState(true);
Enter fullscreen mode Exit fullscreen mode
useEffect(() => {  
    async function fetchJobs() {  
      const response = await fetch('/api/jobs');  
      const data = await response.json();  
      setJobs(data);  
      setLoading(false);  
    }  
    fetchJobs();  
  }, []);
Enter fullscreen mode Exit fullscreen mode
if (loading) {  
    return <div>載入中...</div>;  
  }
Enter fullscreen mode Exit fullscreen mode
return <JobList jobs={jobs} />;  
}
Enter fullscreen mode Exit fullscreen mode

修改後 (SSR 模式):

// src/app/page.tsx (After)  
import JobList from '@/components/JobList';  
import { fetchJobsFromFirestore } from '@/lib/firebase'; // 假設這是從 Firestore 獲取資料的函數
Enter fullscreen mode Exit fullscreen mode
// 頁面變成一個 async function  
export default async function HomePage() {  
  // 1. 直接在伺服器端獲取資料  
  const initialJobs = await fetchJobsFromFirestore({ limit: 20 });
Enter fullscreen mode Exit fullscreen mode
// 2. 將資料直接傳遞給子元件  
  return <JobList initialJobs={initialJobs} />;  
}
Enter fullscreen mode Exit fullscreen mode

修改之後,先是移除了 '''use client''',讓頁面變回預設的伺服器元件,並將資料獲取的邏輯直接放在 async 的頁面元件中,而Next.js 會在伺服器上 await 這個資料請求完成,然後才把渲染好的 HTML 送出。

其他效能優化點

除了 SSR,這次也一併改了下面幾個小地方:

  1. 圖片優化 (next/image):自動處理圖片格式 (WebP)、尺寸和延遲載入。

  2. 字體優化 (next/font):避免字體檔案載入造成的版面位移 (CLS) 和文字閃爍。

  3. 動態載入 (next/dynamic):對於不在初始可視區域內的元件(例如頁尾的複雜互動元件),可以使用動態載入,減少主要 JS 檔案的大小。

  4. 預先連線 (preconnect):在 layout.tsx 中使用 <link rel="preconnect"> 提前與重要的第三方網域(如 Google Fonts, Firebase API)建立連線,減少後續請求的延遲。

改善後的頁面再次部署到Cloud Run 後,LCP 和 TBT 兩個部分已經有很大的改善了,應該也跟大部分優化實戰推薦的,從 CSR 轉向 SSR/SSG 是解決現代前端應用 LCP 和 TBT 過高問題最有效的方法之一。

透過利用 Next.js 提供的伺服器元件和資料獲取功能,可以將繁重的工作留在伺服器,換來提供給使用者一個流暢的瀏覽體驗,同時也滿足了搜尋引擎對於網站速度的要求。

其他還是偏慢的部分補功能陸續補足時,順便再花一點時間來修理,下一步應該會丟一個Github Action來在每次部署到Cloud ,利用 PageSpeed Insights API 來測測看有沒有改壞。

# nextjs# seo

Top comments (0)