Next.js 資源最佳化

在現代網頁開發中,資源載入的效能直接影響使用者的體驗與 SEO 排名。Next.js 提供了一系列內建的最佳化組件,幫助開發者輕鬆達成 Core Web Vitals(核心網頁指標) 的優異表現,特別是針對 LCP (最大內容繪製)CLS (累積版面配置偏移) 這兩個關鍵指標。

本篇文章將深入探討 next/imagenext/fontnext/script 的進階用法與最佳實踐。

圖片最佳化 (next/image)

圖片通常是網頁中佔用頻寬最大的資源。Next.js 的 Image 組件擴充了標準的 HTML <img> 標籤,提供了自動化的圖片優化功能。

為什麼需要 next/image

使用傳統 <img> 標籤時,開發者需手動處理:

  • Responsive Images:為不同裝置提供不同尺寸的圖片 (srcset)。
  • CLS Prevention:手動計算並保留圖片空間,防止版面跳動。
  • Lazy Loading:實作 Intersection Observer 來延遲載入圖片。
  • Modern Formats:將圖片轉檔為 WebP 或 AVIF。

next/image 自動解決了上述所有問題。

基礎與遠端圖片

1. 本地圖片 (Local Images)

Next.js 自動偵測本地圖片的寬高,這是最簡單的用法。

import Image from 'next/image';
import profilePic from './me.png'; // 靜態引入

export default function Page() {
  return (
    <Image
      src={profilePic}
      alt="作者照片"
      placeholder="blur" // 自動產生模糊預覽圖
    />
  );
}

2. 遠端圖片 (Remote Images)

若圖片來自外部 URL,你需要手動指定 widthheight,並在 next.config.js 設定允許的網域。

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'assets.example.com',
      },
    ],
  },
};

進階技巧:Custom Loaders

預設情況下,Next.js 會使用自己的 Server 優化圖片。但如果你使用 Cloudinary、Imgix 等專業圖片 CDN,建議使用 loader 直接生成對應的 URL,這能減輕 Next.js 伺服器的負擔

'use client';

import Image from 'next/image';

// 定義一個 loader 函數
const myLoader = ({ src, width, quality }) => {
  return `https://example.com/${src}?w=${width}&q=${quality || 75}`;
};

export default function Page() {
  return (
    <Image
      loader={myLoader} // 使用自定義 loader
      src="me.png"
      alt="Picture of the author"
      width={500}
      height={500}
    />
  );
}

進階技巧:fillsizes 詳解

當圖片需要「RWD 響應式」或「填滿容器」時,使用 fill 屬性。但這時必須正確設定 sizes,否則瀏覽器可能會下載過大的圖片。

sizes 的意義

sizes 告訴瀏覽器:「在不同的螢幕寬度下,這張圖片預計會佔據多少視窗寬度?」

錯誤範例:不寫 sizes(預設為 100vw)。

  • 若你在手機上顯示一張半寬的卡片圖,但 sizes 預設 100vw,瀏覽器會以為圖片跟螢幕一樣寬,進而下載了 2 倍大的圖片,浪費流量。

正確範例

import Image from 'next/image';

export default function Card() {
  return (
    <div style={{ position: 'relative', height: '300px' }}>
      <Image
        src="/photo.jpg"
        alt="風景圖"
        fill // 填滿父容器
        style={{ objectFit: 'cover' }} // CSS: 裁切以填滿
        // 解讀:
        // (max-width: 768px) 100vw -> 手機版時,圖片佔滿全寬
        // (max-width: 1200px) 50vw -> 平板版時,圖片佔一半寬度
        // 33vw                     -> 桌面版時,圖片約佔 1/3 寬度
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  );
}

事件處理 (onLoad)

當你需要知道圖片何時載入完成(例如:移除自定義的骨架屏),可以使用 onLoad

'use client';

import Image from 'next/image';
import { useState } from 'react';

export default function Page() {
  const [isLoaded, setIsLoaded] = useState(false);

  return (
    <Image
      src="/large-image.jpg"
      alt="Large Image"
      width={1000}
      height={500}
      onLoad={(e) => setIsLoaded(true)} // 圖片載入後觸發
      className={isLoaded ? 'opacity-100' : 'opacity-0'} // 簡單的淡入效果
    />
  );
}

字體最佳化 (next/font)

next/font 會在 build time 下載字體檔案並 self-host,完全移除外部 Google Fonts 請求,消除隱私疑慮並提升速度。

使用多種字體與 Tailwind CSS

在實務上,我們常需要同時使用「標題字體」與「內文字體」。結合 Tailwind CSS Variable 是最優雅的解法。

1. 設定字體 (app/layout.tsx)

import { Inter, Playfair_Display } from 'next/font/google';

// 內文字體
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
});

// 標題字體
const playfair = Playfair_Display({
  subsets: ['latin'],
  variable: '--font-playfair',
  display: 'swap',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${playfair.variable}`}>
      <body>{children}</body>
    </html>
  );
}

2. 設定 Tailwind (tailwind.config.js)

module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'], // 對應 Inter
        serif: ['var(--font-playfair)'], // 對應 Playfair Display
      },
    },
  },
};

3. 在組件中使用

<h1 className="font-serif text-4xl">這會是 Playfair Display</h1>
<p className="font-sans">這會是 Inter</p>

Variable Fonts (可變字體)

Next.js 預設支援 Variable Fonts。如果不指定 weight,Next.js 會載入包含所有粗細的可變字體檔案,這通常比載入多個固定粗細的檔案更有效率。

// 不指定 weight,預設載入 Variable Font
const inter = Inter({ subsets: ['latin'] });

若字體不支援 Variable Font(如某些特殊 Google Fonts),則必須明確指定 weight 陣列:

const roboto = Roboto({
  weight: ['400', '700'],
  subsets: ['latin'],
});

腳本最佳化 (next/script)

為什麼需要 Web Workers?

有些第三方腳本(如 Google Tag Manager, 監控軟體)非常沈重,會佔用主執行緒 (Main Thread),導致 UI 變得卡頓。

Next.js 提供了實驗性的 worker 策略,可以將這些腳本移到 Web Worker 中執行。這通常需要配合 @next/third-parties 套件庫使用。

安裝套件:

npm install @next/third-parties

使用 Google Tag Manager (GTM) 範例:

@next/third-parties 已經封裝好了常用的第三方服務,能自動幫你優化載入。

import { GoogleTagManager } from '@next/third-parties/google';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
      {/* 只需填入 GTM ID,Next.js 會自動處理載入最佳化 */}
      <GoogleTagManager gtmId="GTM-XYZ" />
    </html>
  );
}

next/script 的載入策略回顧

如果你使用原生的 Script 組件,務必根據重要性選擇正確的策略:

策略說明適用場景
afterInteractive(預設) 頁面 Hydration 後載入GA, Pixel, 熱圖分析
beforeInteractive頁面內容顯示前載入 (阻擋渲染)Bot 偵測, Cookie Consent
lazyOnload瀏覽器閒置時載入客服 Chatbot, 社群分享按鈕
worker在 Web Worker 執行 (實驗性)極吃效能的運算腳本

內聯腳本 (Inline Script) 注意事項

當你需要直接在 HTML 輸出一段 JS,請務必加上 id,這樣 Next.js 才能確保這段腳本只會被執行一次,即便使用者在頁面間切換。

<Script id="google-analytics">
  {`
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'GA_MEASUREMENT_ID');
  `}
</Script>

總結

  • 圖片:優先使用本地圖片自動寬高;遠端圖片善用 loader;RWD 圖片務必設定正確的 sizes 以節省流量。
  • 字體:利用 variable 屬性結合 Tailwind CSS,輕鬆管理多字體系統;Variable Fonts 是效能首選。
  • 腳本:善用 @next/third-parties 來載入 GTM 或 YouTube,或使用 lazyOnload 將非關鍵腳本延後載入,確保 LCP 不受影響。