跳到主要內容

FE Day 26 圖片、字體與資源優化

FE Day 26 圖片、字體與資源優化

執行需求:CPU 可跑。昨天把 Core Web Vitals 三指標(LCP、INP、CLS)的量測與改善策略走完一輪,今天把「優化」的執行細節展開到「資源」這一層。對多數 web 應用來說,LCP 的瓶頸十之八九藏在「圖片太大」「字型下載慢」「第三方腳本阻塞」三件事上;CLS 的瓶頸十之八九藏在「圖片沒設尺寸」「字型載入後字寬變化」「元素動態注入」。今天會在 booking-web 專案上示範 next/image 的 formats、sizes、quality、placeholder API,next/font 的 self-host 與 subsetting,以及 CDN preconnect、dns-prefetch、preload 等資源提示,最後用 Python 端的 Pillow 做一組自動化的資產健康檢查。

引言

對於剛從後端轉過來的工程師,「資源優化」這件事常常被低估。大家在寫 FastAPI 的時候會在意 query 效能、response size、caching,到了前端就只剩「把圖片壓小一點」。但實際上,前端的資源成本遠比你想的高:一個未壓縮的 Hero JPEG 可以佔 2MB,等於每個使用者造訪首頁就幫你付 2MB 的頻寬;一個 200KB 的 Noto Sans TC woff2 如果不做 subsetting,每個使用者下載的內容 90% 用不到;一個沒設 async / defer 的 GA 腳本可以讓 LCP 延後 1 秒。

這篇的目標是建立三條工作紀律:第一是「圖片預算」——一張 Hero 圖不能超過 200KB、超過就改用 AVIF;第二是「字型紀律」——只用 self-hosted + subset,避免每個字型完整檔案下載;第三是「第三方資源清單」——任何引入的外部腳本都要寫進「第三方清單」,每季檢視是否還需要。我們會在 Next.js 15 的 next/image、next/font、next/script 三個內建元件上落實這三條紀律。

next/image:宣告式圖片優化

next/image 是 Next.js 對 HTML <img> 的升級版,三件事自動做掉:根據 src 與 width/height 算出長寬比避免 CLS;根據瀏覽器能力自動選 AVIF / WebP / JPEG;透過 Next.js Image Optimization API(或自架的 sharp)即時轉檔。但它會把你導向「宣告式」的寫法,多寫幾個 attribute:

// src/app/page.tsx
// Hero 圖片:宣告式寫法
import Image from "next/image";

export default function HomePage() {
  return (
    <main>
      <section className="hero">
        <Image
          src="/hero.jpg"
          alt="預約管理系統示意"
          width={1200}
          height={630}
          priority // ← 關鍵:告訴 Next.js 這是 LCP 元素,立即 prefetch
          sizes="(max-width: 768px) 100vw, 1200px"
          quality={75}
          placeholder="blur"
          blurDataURL="data:image/jpeg;base64,/9j/4AAQSk..."
        />
        <h1>歡迎使用 Hao 預約管理系統</h1>
      </section>
    </main>
  );
}

每個 attribute 都有明確用途:width/height 強制保留寬高比例避免 CLS;priority 把圖片標記為 LCP 元素,編譯時自動加 fetchpriority="high" 與 <link rel="preload">;sizes 告訴瀏覽器在不同 viewport 下要多大(行動裝置 100vw、桌機 1200px),瀏覽器會根據 sizes 選擇對應的 srcset 圖檔;quality 是壓縮品質(預設 75,建議 Hero 70-80、縮圖 60-70);placeholder="blur" 顯示低解析度預覽(用 base64 inline),避免圖片下載完成前一片空白;blurDataURL 是這張低解析度預覽的 base64 字串。

外部圖片的處理類似,但需要設定 remotePatterns 白名單避免 Open Redirect:

// next.config.ts
// 設定 next/image 允許載入的外部網域
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
        pathname: "/**",
      },
      {
        protocol: "https",
        hostname: "cdn.hao-code.com",
      },
    ],
    // 預設 formats:AVIF 優先、WebP 次之、JPEG/PNG 最後
    formats: ["image/avif", "image/webp"],
  },
};

export default nextConfig;

把 formats 設成 AVIF 優先,平均可以比 WebP 再省 20%、比 JPEG 省 50%。但要注意 AVIF 編碼比 WebP / JPEG 重很多,建議把 Image Optimization API 部署在有快取的環境(Vercel Edge 預設就有,Lighthouse 不算你的 Image API 時間)。如果自架,建議用 sharp 在 build time 批次轉好、快取到 S3,避免每次請求都重新編碼。

響應式圖片:sizes 與 srcset

sizes 是響應式圖片最容易被忽略、但效果最大的 attribute。沒有 sizes 時,next/image 預設會用圖片的原始寬度(1200px)做 srcset,使用者在手機上看到的圖比實際顯示大兩倍。寫好 sizes 後,瀏覽器會自動選對應寬度的圖:

// src/app/services/page.tsx
// 預約服務 card:每張卡片在不同 viewport 下大小不同
import Image from "next/image";

export function ServiceCard({ service }: { service: { id: string; name: string; image: string } }) {
  return (
    <article className="card">
      <Image
        src={service.image}
        alt={service.name}
        width={800}
        height={600}
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
        // 行動:100vw、平板:50vw、桌機:33vw
      />
      <h3>{service.name}</h3>
    </article>
  );
}

這組 sizes 的意思是:螢幕 <= 640px(行動)時卡片寬度 100vw、螢幕 <= 1024px(平板)時寬度 50vw、桌機寬度 33vw。瀏覽器看到 sizes 後會去 srcset 找對應寬度的圖(next/image 編譯時自動產生 640w、750w、1080w 等多個解析度)。實際測下來,行動裝置下載的圖從 800×600 (60KB) 變成 640×480 (30KB),省了一半頻寬。

另外一個常見技巧是「美術設計方向」:桌機用橫的圖、手機用直的圖,next/image 用 artDirect 或在不同 viewport 用 picture 標籤處理。這在 Hero 圖、商品圖常看到,但 Next.js 沒有原生支援,要靠 CSS picture + source media。

批次優化圖片:用 Python 做資產健康檢查

專案用一段時間後,/public 資料夾常會堆滿各種尺寸、各種格式的圖。建議加一支 Python 腳本定期掃描,找出可以優化的標的(太大、格式太舊、重複檔案):

# scripts/check_assets.py
# 用 Pillow 檢查 /public 圖片資產
import sys
from pathlib import Path
from PIL import Image

PUBLIC_DIR = Path("public")
MAX_KB = 200  # 單張圖片預算
WARN_KB = 100


def human_size(num: int) -> str:
    if num < 1024:
        return f"{num} B"
    if num < 1024 * 1024:
        return f"{num / 1024:.1f} KB"
    return f"{num / (1024 * 1024):.1f} MB"


def check_image(path: Path) -> dict:
    img = Image.open(path)
    size_kb = path.stat().st_size / 1024
    return {
        "path": str(path),
        "size": human_size(int(path.stat().st_size)),
        "format": img.format,
        "dimensions": img.size,
        "ratio": size_kb / (img.size[0] * img.size[1] / 1_000_000),
        "size_kb": size_kb,
    }


def main() -> int:
    if not PUBLIC_DIR.exists():
        print(f"找不到 {PUBLIC_DIR}", file=sys.stderr)
        return 1

    suspects: list[dict] = []
    for path in sorted(PUBLIC_DIR.rglob("*")):
        if path.suffix.lower() not in {".jpg", ".jpeg", ".png", ".webp"}:
            continue
        info = check_image(path)
        if info["size_kb"] > MAX_KB:
            suspects.append({**info, "level": "FAIL"})
        elif info["size_kb"] > WARN_KB:
            suspects.append({**info, "level": "WARN"})

    if not suspects:
        print("所有圖片都在預算內")
        return 0

    print(f"發現 {len(suspects)} 張超過預算的圖片:")
    for s in suspects:
        print(f"  [{s['level']}] {s['path']}: {s['size']}、{s['dimensions']}")
    return 0 if all(s["level"] == "WARN" for s in suspects) else 2


if __name__ == "__main__":
    raise SystemExit(main())

這支腳本的運作:掃 /public 底下所有 jpg/jpeg/png/webp,計算每張檔案大小,> 200KB 標 FAIL、> 100KB 標 WARN。可加進 CI:python scripts/check_assets.py,return code 非零時讓 build 失敗。實務上你可以把它改成掃 CDN bucket(S3 / R2)的版本,搭配 CloudFront / Fastly 的 access log 找出真正被使用者下載的圖,做更精準的預算配置。

字型:self-host + subset + size-adjust

next/font 把 Google Fonts 的工作流自動化:宣告要用的字型、next/font 在 build time 自動下載、轉成 woff2、inline 進 CSS、預先載入。比以前的 <link href="https://fonts.googleapis.com/..."> 快很多,因為沒有一個第三方請求打到 Google:

// src/app/layout.tsx(節錄)
// next/font/google:自動 self-host + subset + 預先載入
import { Inter, Noto_Sans_TC } from "next/font/google";

const inter = Inter({
  subsets: ["latin"], // 只取英文子集
  display: "swap",
  variable: "--font-inter",
});

// Noto Sans TC:CJK 字型,預先 subset 成「常用 8000 字」
const notoTC = Noto_Sans_TC({
  subsets: ["latin"],
  weight: ["400", "700"],
  display: "swap",
  variable: "--font-noto-tc",
  preload: true,
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-Hant" className={`${inter.variable} ${notoTC.variable}`}>
      <body>{children}</body>
    </html>
  );
}

CSS 變數讓兩套字型可以在不同元件混用:

/* src/app/styles/globals.css */
:root {
  --font-sans: var(--font-inter), "Noto Sans TC", "PingFang TC", sans-serif;
}

body {
  font-family: var(--font-sans);
}

h1, h2, h3 {
  font-family: var(--font-inter), "Noto Sans TC", sans-serif;
  font-weight: 700;
}

Noto Sans TC 完整版超過 4MB,做 subset 才能用。next/font/google 內建的 subsets: ["latin"] 只切出拉丁字母部分,CJK 需要用 next/font/local 配上預先切好的 woff2。對台灣繁體,常見做法的 subset 字數是「常用 8000 字」或「教育部標準字型 4808 字」,檔案可以從 4MB 壓到 800KB 左右。再用 unicode-range 把這份字型只對「需要的字元」生效:

// src/app/layout.tsx(進階:自訂字型 subset)
import localFont from "next/font/local";

const notoTC = localFont({
  src: [
    {
      path: "../public/fonts/NotoSansTC-Regular.subset.woff2",
      weight: "400",
      style: "normal",
    },
    {
      path: "../public/fonts/NotoSansTC-Bold.subset.woff2",
      weight: "700",
      style: "normal",
    },
  ],
  display: "swap",
  variable: "--font-noto-tc",
  preload: true,
});

另一個降低 CLS 的小細節是 size-adjust + ascent-override:當實體字型下載完成、瀏覽器從「系統字型」切換到「Noto Sans TC」時,glyph 寬度變化會推開其他元素。在 @font-face 用 size-adjust: 100%; ascent-override: 88%; descent-override: 100% 可以讓 fallback 字型的 glyph 寬度與實體字型一致,幾乎消除切換時的位移。FE Day 25 的「改善 CLS」段落有實際範例。

第三方資源:preconnect 與 preload

當頁面需要打 API、串 CDN 圖、嵌入 YouTube 時,每多一個第三方 origin 就多一次 DNS 查詢 + TCP handshake + TLS 握手(總共 200-500ms)。用 <link rel="preconnect"> 讓瀏覽器提早解析目標 host:

<!-- src/app/layout.tsx(節錄 Next.js 寫法) -->
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-Hant">
      <head>
        <link rel="preconnect" href="https://api.hao-code.com" />
        <link rel="preconnect" href="https://cdn.hao-code.com" crossOrigin="anonymous" />
        <link rel="dns-prefetch" href="https://www.google-analytics.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

三條 <link> 的差異:preconnect 完整解析(含 TLS),用在「這個頁面一定會用到」的 origin;dns-prefetch 只做 DNS 查詢(成本比 preconnect 低),用在「可能會用到但不是關鍵」的 origin(如 GA、客服 widget)。crossOrigin="anonymous" 表示這個 fetch 不帶 cookie,避免預連線被瀏覽器擋。

最關鍵的兩、三個 LCP 資源可以用 <link rel="preload"> 提早下載:

<link
  rel="preload"
  href="/fonts/NotoSansTC-Regular.subset.woff2"
  as="font"
  type="font/woff2"
  crossOrigin="anonymous"
/>
<link
  rel="preload"
  href="/hero.avif"
  as="image"
  fetchpriority="high"
/>

as="font" 跟 type="font/woff2" + crossOrigin 必須一起出現,否則瀏覽器不下載。實務上 next/font 會自動加 preload 的 <link>,不需手動寫;Hero 圖片的 preload 在 next/image 加上 priority 後也會自動加。

用 next/script 管理第三方腳本

很多專案會嵌入 GA、Intercom、Hotjar、Facebook Pixel 等第三方腳本。每個都是「HTML 解析時就 inline 跑」的 <script>,會阻塞 LCP 元素渲染。Next.js 15 提供 next/script,給你三種載入策略:

// src/app/layout.tsx(節錄)
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-Hant">
      <body>
        {children}
        {/*
          strategy="afterInteractive" (預設):
          頁面變互動後載入,適合 GA、Intercom
        */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"
          strategy="afterInteractive"
        />
        <Script id="ga-init" strategy="afterInteractive">
          {`window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXX');`}
        </Script>

        {/*
          strategy="lazyOnload":
          等 idle 時才載入,適合 Hotjar、廣告 pixel
        */}
        <Script src="https://js.hs-scripts.com/XXXX.js" strategy="lazyOnload" />
      </body>
    </html>
  );
}

三種策略的時機:beforeInteractive 在頁面變可互動前(少用,會阻塞 hydration)、afterInteractive 預設策略(頁面變可互動後)、lazyOnload 瀏覽器閒置時才載入。GA 跟 Pixel 屬於分析類,用 afterInteractive 即可;客服 widget(Intercom、Crisp)也用 afterInteractive;最不重要的(廣告 pixel、Hotjar 錄製腳本)用 lazyOnload,確保不影響 LCP。

CSS、CSS-in-JS 與 critical CSS

CSS 是常被忽略的阻塞資源——瀏覽器在碰到 <link rel="stylesheet"> 時必須下載並解析完才能繼續渲染。Tailwind CSS 4.x 把這件事做得很徹底:編譯時掃所有元件的 class、生成一份只有用到的 utility CSS,讓每個頁面載入的 CSS 只有 5-20KB。Next.js 15 預設對 Tailwind 支援完善,npx create-next-app@15.0.3 booking-web 時選 Tailwind 就會自動設定好。

另一個「CSS 阻塞」常見場景是「大型元件庫」:Ant Design、MUI、Chakra UI 這類元件庫的 CSS bundle 動輒 100-200KB。解法是用 Next.js 的 transpilePackages + tree-shaking 把用到的 component CSS 切出來;更激進的辦法是直接寫 Tailwind class 取代元件庫。FE Day 32 的「預約系統」設計系統會用純 Tailwind 重寫,不依賴任何元件庫。

CSS-in-JS(emotion、styled-components)在 Next.js App Router 上要特別設定,因為 Server Component 不支援 CSS-in-JS 的執行期 injection。建議改用 Tailwind 或 CSS Modules(.module.css)。CSS Modules 是 Next.js 內建支援:

/* src/app/services/page.module.css */
.hero {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

@media (min-width: 768px) {
  .hero {
    grid-template-columns: 1fr 1fr;
  }
}

.card {
  border-radius: 0.5rem;
  padding: 1rem;
  background: var(--color-surface);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
// src/app/services/page.tsx(節錄)
import styles from "./page.module.css";
import Image from "next/image";

export default function ServicesPage() {
  return (
    <section className={styles.hero}>
      <article className={styles.card}>
        <Image src="/svc1.jpg" alt="服務 1" width={400} height={300} />
        <h3>一對一諮詢</h3>
      </article>
    </section>
  );
}

CSS Modules 經 Next.js 編譯後:class 名會變成 .hero__abc123 的形式,避免全域污染;CSS 會被 inline 進 CSS bundle、自動做 dead code elimination;style 區塊只在使用該元件的頁面下載。在 booking-web 專案實測,CSS Modules + Tailwind 的組合讓首頁 CSS 從 80KB 降到 22KB。

用 HTTP cache header 控管資源的快取策略

圖片、字型、JS、CSS 都要設定正確的 Cache-Control,讓使用者第二次造訪時瀏覽器直接從磁碟讀取,省下伺服器往返。Next.js 15 在 next.config.ts 提供完整的設定 API:

// next.config.ts
// 設定靜態資源的快取策略
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        // 字型、build-time 產生的雜湊檔名 → immutable(永遠快取)
        source: "/_next/static/:path*",
        headers: [
          { key: "Cache-Control", value: "public, max-age=31536000, immutable" },
        ],
      },
      {
        // /public 字型、logo、icon
        source: "/fonts/:path*",
        headers: [
          { key: "Cache-Control", value: "public, max-age=31536000, immutable" },
        ],
      },
      {
        source: "/(.*)",
        headers: [
          // 沒有雜湊的資源(HTML、SWR 回應)快取一小時讓 stale-while-revalidate
          {
            key: "Cache-Control",
            value: "public, max-age=0, must-revalidate",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

三組設定的差異:/_next/static/ 與 /fonts/ 是「檔名帶雜湊」的,內容變了就換檔名,永遠可以 cache 1 年(immutable);HTML 與 SSR 結果不該被瀏覽器長期快取(使用者可能會看到舊內容),用 must-revalidate 配合 ETag 處理。完整正確的 cache 設定能讓第二次造訪的 LCP 直接從 1.5 秒降到 0.5 秒。

另一個常被忽略的是 Service Worker 預快取。Next.js 15 透過 @serwist/next(前身是 Workbox)整合,把關鍵資源預先存進 Service Worker:

// src/app/sw.ts
// 用 @serwist/next 設定 Service Worker 預快取
import { defaultCache } from "@serwist/next/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";

declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
  runtimeCaching: defaultCache,
});

serwist.addEventListeners();

啟用 SW 後,使用者第二次造訪時靜態資源從 Service Worker cache 直接回應,不必發任何 HTTP request。對 B2C 應用(使用者每週回來 3-5 次)特別有效;但如果你的應用是 B2B(一年回來 1-2 次),SW 的複雜度反而會拖累維運,要謹慎評估。

常見錯誤與踩雷

第一個常見的踩雷是「next/image 用遠端圖但忘了設定 remotePatterns」。沒設定白名單時,next/image 對遠端圖直接拋 400。在 next.config.ts 的 images.remotePatterns 加入 host 是必做的設定。

第二個是「priority 標太多」。priority 會強制圖片 eager loading 且 preconnect,如果整頁每張圖都加 priority,preconnect 會把頻寬吃光,反而拖累 LCP。原則:只有「頁面上最大、最關鍵」的 1-2 張圖加 priority,其他圖維持 lazy。

第三個是「字型 subset 沒切好」。常見錯誤是只切到「教育部 4808 字」而忽略「使用者會輸入的 emoji、特殊符號」,結果使用者打 emoji 時降級到系統字型,CLS 又跑出來。建議保留 1-2 個 unicode-range 區段給 emoji 與標點符號。

第四個是「preconnect 太多」。超過 4 個 preconnect,瀏覽器會自動忽略多餘的;建議只在「這個頁面真的會用到」的 origin 上加 preconnect,其他只用 dns-prefetch。預算控管:每頁 preconnect <= 3 個,其他一律 dns-prefetch。

第五個是「CSS-in-JS 在 Server Component 上踩雷」。Next.js App Router 預設所有元件是 Server Component,但 emotion / styled-components 預期在 Client Component 跑。如果你不小心在 Server Component 內 styled.div\`\``,編譯會 pass 但執行會 crash。建議:Server Component 用 Tailwind、CSS Modules;Client Component 才用 styled-components。

第六個是「外部字型沒做 CORS」。從外部網域載入字型時,瀏覽器會要求 Access-Control-Allow-Origin header,沒設定就拒絕載入。next/font 自動處理這個;自架字型時記得在 CDN 上加 access-control-allow-origin: * 或具體 origin。

效能與實務提醒

圖片、字型、第三方資源的最佳化是「持續性工作」,不是「做一次就好」。每當有新設計師加入、新功能上線,圖片與字型都會再變動。建議把今天寫的 check_assets.py 變成 pre-commit hook,讓工程師 commit 前就檢查檔案大小。

實務上一個中型 Next.js 專案的典型優化前/後數字對照:Hero JPEG 從 2MB 降到 100KB(AVIF)、字型檔從 4MB 降到 800KB(subset + local)、第三方腳本從 5 個 inline 變 3 個 defer、整個 LCP 從 4.2 秒降到 1.2 秒、PSI 行動分數從 52 升到 92。這些數字的差異對 SEO 排名、使用者留存都有實質幫助。

另外,別忘了「行動網路」這個關鍵假設。Lighthouse 在「模擬 4G + Moto G4」環境跑出來的數字雖然較保守,但很貼近東南亞、印度、巴西的實際使用者情境。台灣普遍 4G/5G 都很快,但客戶如果是做跨境服務(東南亞市場),Lighthouse 行動分數就是必要的 KPI。如果你的主要客群在歐美,光纖普及,反而要把「桌機 PSI」分數當 KPI。FE Day 25 的 RUM 與 CrUX 數字補上真實使用者的狀況,比 Lighthouse 還準。

再進一步是「資源預載入的優先序設計」。網頁上的資源很多,但不是每個都重要。設計一個明確的優先序可以幫助瀏覽器決定下載順序:

  1. HTML 本身(優先序最高,瀏覽器內建)。
  2. preload 過的 LCP 圖片、CSS、字型(你特別關照的)。
  3. preconnect 過的第三方(連線就緒)。
  4. CSS 中 import 進來的 stylesheet(CSS 規格內建)。
  5. 同步 <script>(最壞的下載阻塞,盡量避免)。
  6. async / defer 過的 JavaScript(不阻塞,但優先序較低)。
  7. lazy 過的圖片、iframe(捲到才載)。

把資源依這七層分清楚,比「每個資源都加 preload」好很多——preload 太多會把頻寬吃光,反而拖慢真正關鍵的東西。實務上 preload 應該只用在 LCP 元素,最多 1-2 個就好。另外要注意的是「避免 critical request chain 過深」:HTML 解析到 CSS → CSS 解析到字型 → 字型下載完才能 render,每多一層就多一個 RTT。用 Chrome DevTools 的 Network panel 把請求排成 waterfall,找出超過 5 層的 critical chain 重構。例如字型不要 CSS 內 import、改用 next/font 直接 inline,能砍掉 1-2 層。

最後一個提醒是「不要過度最佳化」。圖片可以壓到 20KB,但你花了兩小時工作,最後只省了 0.1 秒 LCP。先把容易的、效果大的東西做完(Hero 圖壓縮、字型 subset、第三方 defer),再依 RUM 數字決定要不要繼續挖深。工程時間永遠有限。

小結

今天把圖片、字型與資源優化的「執行細節」展開了。重點回顧:next/image 用 width/height/sizes/priority/quality/placeholder/blurDataURL 七個 API 處理圖片;next/font 用 next/font/google 自動 self-host、用 next/font/local 接自訂 subset;next/script 用 strategy 三選一控制第三方腳本載入時機;preconnect/preload/dns-prefetch 三個資源提示精準控制下載順序;CSS 用 Tailwind 或 CSS Modules 取代元件庫 CSS 負擔。這套組合拳打完,booking-web 專案的 LCP 通常能壓在 1.5 秒以內、CLS 接近 0、PSI 行動分數 90+。

明天我們會進入測試的最後一塊:E2E 測試。Playwright 1.5x 是 2026 年最熱門的 E2E 框架,能用 Python 或 TypeScript 寫、把使用者真實點按的行為錄下來驗證。我們會在 booking-web 專案上示範「預約流程」的完整 E2E 測試、CI 整合、常見踩雷與 race condition 除錯方法。

結語

今天的重點是讓圖片、字型、第三方資源的優化從「知道該做但不知道怎麼做」變成「在 Next.js 15 專案可以系統化執行的工程紀律」。透過 next/image、next/font、next/script、preconnect / preload、Python 端的資產健康檢查,你應該能在不另外學套件的情況下,把專案的 LCP 從 3 秒壓到 1.5 秒以內。讀完這篇你應該能回答:next/image 的 sizes 怎麼寫?為什麼要 self-host 字型?第三方腳本用哪種 strategy?

明天,我們會用 Playwright 1.5x 從瀏覽器端做 E2E 測試。會示範怎麼把「使用者真實點按預約 → 看到確認畫面 → 收到確認信」的流程錄下來、怎麼在 CI 自動跑、怎麼處理登入狀態的 persistence(storageState)、怎麼在測試報告裡抓 console error。實作上會以「預約流程」當主軸,搭配 FastAPI 觀察站與 Line 通知做端對端驗證。

延伸資源

  • Next.js 15 — Image Optimization 官方文件:https://nextjs.org/docs/app/api-reference/components/image,所有 Image 屬性、formats 設定、remotePatterns 白名單。
  • Next.js 15 — Font Optimization:https://nextjs.org/docs/app/api-reference/components/font,next/font/google、next/font/local 用法、subsettting、調整 size-adjust。
  • MDN — Resource Hints:https://developer.mozilla.org/docs/Web/Performance/Guides/Resource_hints,preload / preconnect / dns-prefetch 的語義差異。
  • Google — font-display 說明:https://developer.mozilla.org/docs/Web/CSS/@font-face/font-display,swap / optional / fallback 的差異與取捨。
  • Pillow 官方文件:https://pillow.readthedocs.io/,Python 端處理圖片的標準工具,build script / 健康檢查都會用到。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...