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 還準。
再進一步是「資源預載入的優先序設計」。網頁上的資源很多,但不是每個都重要。設計一個明確的優先序可以幫助瀏覽器決定下載順序:
- HTML 本身(優先序最高,瀏覽器內建)。
- preload 過的 LCP 圖片、CSS、字型(你特別關照的)。
- preconnect 過的第三方(連線就緒)。
- CSS 中 import 進來的 stylesheet(CSS 規格內建)。
- 同步
<script>(最壞的下載阻塞,盡量避免)。 - async / defer 過的 JavaScript(不阻塞,但優先序較低)。
- 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 / 健康檢查都會用到。
留言
張貼留言