行動版 PageSpeed Insights 優化:完整指南
行動版 PageSpeed Insights 優化調整報告
📊 優化總覽
為什麼要做這件事? 在當今的數位環境中,網頁效能直接影響用戶體驗、搜尋引擎排名,甚至轉換率。根據 Google 的研究,頁面載入時間每增加 1 秒,行動裝置的跳出率就會增加 20%。當我的個人作品集網站在 PageSpeed Insights 的行動版分數只有 65-75 分時,我意識到這不僅影響了用戶體驗,更可能讓潛在的雇主或客戶對我的技術能力產生質疑。作為一名前端工程師,如果連自己的作品集都無法達到優秀的效能標準,如何說服他人我能為他們的專案帶來價值?這就是為什麼我決定深入優化,目標是將效能分數提升到 85-95 分,並將所有優化過程詳細記錄下來,希望能幫助其他開發者避免同樣的效能陷阱。
本報告詳細記錄了針對行動版網頁效能分數(PageSpeed Insights)所做的所有優化調整。
1. 🚫 移除 Render-Blocking 資源
問題
Google Fonts 使用 @import 或同步 <link> 載入,會阻塞頁面渲染,導致 First Contentful Paint (FCP) 和 Largest Contentful Paint (LCP) 延遲。
解決方案
1.1 移除 CSS 中的 @import 語句
檔案: src/styles/globals.css
調整前:
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap');
調整後:
- 完全移除
@import語句 - 改為在 HTML
<head>中非同步載入
影響:
- ✅ 減少 Render-Blocking CSS
- ✅ 改善 FCP 分數
- ✅ 降低 Total Blocking Time (TBT)
1.2 實作非同步字體載入策略
檔案: src/components/layout/base-head.astro
調整內容:
- Preconnect 與 DNS Prefetch
<!-- 提前建立連線,減少 DNS 查詢時間 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
說明:
preconnect:提前建立 TCP 連線和 TLS 握手dns-prefetch:提前解析 DNS,適用於不支援preconnect的舊瀏覽器crossorigin:允許跨域資源共享(CORS)
- 非同步載入字體樣式表
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;300;400;500;600;700;800;900&family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap"
rel="stylesheet"
/>
</noscript>
說明:
rel="preload":告訴瀏覽器這是重要資源,但不阻塞渲染as="style":指定資源類型為樣式表onload:載入完成後自動切換為stylesheetnoscript:為禁用 JavaScript 的瀏覽器提供降級方案
- JavaScript Fallback
<script is:inline>
(function() {
var link = document.querySelector('link[rel="preload"][as="style"][href*="fonts.googleapis.com"]');
if (link && !link.onload) {
var fallback = document.createElement('link');
fallback.rel = 'stylesheet';
fallback.href = link.href;
document.head.appendChild(fallback);
}
})();
</script>
說明:
- 為不支援
onload事件的舊瀏覽器提供降級方案 is:inline:確保腳本在頁面載入時立即執行
影響:
- ✅ 字體載入不再阻塞頁面渲染
- ✅ 改善 FCP 和 LCP 指標
- ✅ 降低 Cumulative Layout Shift (CLS)
1.3 預載入關鍵字體檔案
檔案: src/components/layout/base-head.astro
<link
rel="preload"
href="/fonts/calsans-semibold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
說明:
- 預載入首屏使用的關鍵字體(CalSans)
as="font":指定資源類型為字體crossorigin:允許跨域載入- 使用
woff2格式(最佳壓縮比)
影響:
- ✅ 減少字體載入延遲
- ✅ 改善 LCP 分數
- ✅ 降低 FOIT (Flash of Invisible Text)
2. 🔄 修復強制自動重排(Forced Reflow)
問題
在讀取 DOM 屬性(如 getBoundingClientRect())之前進行 DOM 寫入操作,會觸發強制同步重排,導致主執行緒阻塞。
解決方案
2.1 優化 Navbar 滾動定位
檔案: src/components/Navbar.tsx
調整前:
const element = document.getElementById(href);
if (element) {
e.preventDefault();
const offset = 80;
const elementPosition = element.getBoundingClientRect().top; // ❌ 可能觸發強制重排
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
調整後:
const element = document.getElementById(href);
if (element) {
e.preventDefault();
const offset = 80;
requestAnimationFrame(() => {
// ✅ 將 DOM 讀取操作延遲到下一個重繪週期
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
});
}
說明:
requestAnimationFrame:將 DOM 讀取操作延遲到下一個瀏覽器重繪週期- 允許瀏覽器先完成所有待處理的 DOM 寫入操作
- 避免強制同步重排
影響:
- ✅ 減少主執行緒阻塞時間
- ✅ 降低 Total Blocking Time (TBT)
- ✅ 改善 Interaction to Next Paint (INP)
2.3 優化 Hash Scroll 定位
檔案: src/components/Navbar.tsx
調整前:
useEffect(() => {
const { hash } = window.location;
if (hash) {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
setTimeout(() => {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top; // ❌ 可能觸發強制重排
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
setActiveSection(id as SectionId);
}, 100);
}
}
}, []);
調整後:
useEffect(() => {
const { hash } = window.location;
if (hash) {
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
// Use requestAnimationFrame to avoid forced reflow
requestAnimationFrame(() => {
setTimeout(() => {
requestAnimationFrame(() => {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition =
elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
setActiveSection(id as SectionId);
});
}, 100);
});
}
}
}, []);
說明:
- 使用雙層
requestAnimationFrame確保 DOM 操作在正確的時機執行 - 第一個
requestAnimationFrame:等待當前渲染週期完成 - 第二個
requestAnimationFrame:確保getBoundingClientRect()不會觸發強制重排
影響:
- ✅ 減少額外的 59ms 強制重排時間
- ✅ 改善頁面載入時的滾動定位流暢度
- ✅ 降低 TBT
2.2 優化載入器移除邏輯
檔案: index.tsx
調整前:
useEffect(() => {
const loader = document.getElementById('initial-loader');
if (loader) {
loader.style.opacity = '0'; // ❌ DOM 寫入
setTimeout(() => {
loader.remove(); // ❌ 可能觸發重排
}, 500);
}
}, []);
調整後:
useEffect(() => {
requestAnimationFrame(() => {
// ✅ 批次處理 DOM 操作
const loader = document.getElementById('initial-loader');
if (loader) {
loader.style.opacity = '0';
requestAnimationFrame(() => {
// ✅ 將移除操作延遲到下一個重繪週期
setTimeout(() => {
loader.remove();
}, 500);
});
}
});
}, []);
說明:
- 使用雙層
requestAnimationFrame確保 DOM 操作在正確的時機執行 - 第一個
requestAnimationFrame:等待當前渲染週期完成 - 第二個
requestAnimationFrame:確保移除操作不會觸發強制重排
影響:
- ✅ 減少不必要的重排和重繪
- ✅ 改善頁面載入流暢度
- ✅ 降低 TBT 和 CLS
3. 📦 優化資源載入與快取策略
3.1 實作 Service Worker 快取策略(GitHub Pages)
檔案: public/sw.js (新建)
說明:
由於 GitHub Pages 不支援 _headers 檔案(那是 Netlify 的功能),我們實作了 Service Worker 來處理快取策略。
內容:
// Service Worker for caching static assets on GitHub Pages
const CACHE_NAME = 'shueny-portfolio-v1';
const STATIC_CACHE_DURATION = 31536000; // 1 year in seconds
const HTML_CACHE_DURATION = 3600; // 1 hour in seconds
// Assets that should be cached for a long time (content-hashed)
const STATIC_ASSETS = [
'/_astro/',
'/fonts/',
'/images/',
'.js',
'.css',
'.woff',
'.woff2',
'.ttf',
'.png',
'.jpg',
'.svg',
'.webp',
];
// Cache-First strategy for static assets
// Network-First strategy for HTML
檔案: src/components/layout/base-head.astro
註冊 Service Worker:
<script is:inline>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered:', registration.scope);
})
.catch((error) => {
console.log('Service Worker registration failed:', error);
});
});
}
</script>
說明:
- Cache-First 策略:靜態資源(JS/CSS/字體/圖片)優先從快取讀取,快取 1 年
- Network-First 策略:HTML 檔案優先從網路讀取,快取 1 小時
- 自動清理舊的快取版本
- 為不支援 Service Worker 的瀏覽器提供降級方案
影響:
- ✅ 節省 203 KiB 的重複載入(根據 Lighthouse 回饋)
- ✅ 改善重複造訪效能
- ✅ 降低伺服器負載
- ✅ 改善快取生命週期分數
3.2 優化 Vite 建置配置
檔案: astro.config.mjs
調整內容:
- 手動程式碼分割(Manual Chunk Splitting)
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
if (id.includes('react') || id.includes('react-dom')) {
return 'react-vendor'; // React 相關套件獨立打包
}
if (id.includes('lucide-react')) {
return 'lucide-vendor'; // 圖示庫獨立打包
}
return 'vendor'; // 其他第三方套件
}
},
// 確保所有資源使用內容雜湊
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
}
說明:
- 將第三方套件分離成獨立的 chunk
- React 相關套件獨立打包,便於快取
- 圖示庫獨立打包,減少主 bundle 大小
- 內容雜湊:確保所有資源檔案名稱包含內容雜湊,便於長期快取
影響:
- ✅ 改善初始載入時間
- ✅ 提升快取命中率
- ✅ 減少重複下載
- ✅ 確保資源有正確的快取生命週期
- CSS 程式碼分割
cssCodeSplit: true;
說明:
- 為每個頁面生成獨立的 CSS 檔案
- 只載入當前頁面需要的樣式
影響:
- ✅ 減少初始 CSS 大小
- ✅ 改善 FCP 和 LCP
- 資源內聯閾值
assetsInlineLimit: 4096; // 4KB
說明:
- 小於 4KB 的資源會內聯到 HTML 中
- 減少 HTTP 請求數量
影響:
- ✅ 減少 HTTP 請求數
- ✅ 降低網路延遲
- 依賴預優化
optimizeDeps: {
include: ['react', 'react-dom'],
}
說明:
- 預先優化 React 相關依賴
- 減少開發和建置時間
影響:
- ✅ 改善開發體驗
- ✅ 優化建置產物
3.3 優化關鍵路徑載入(Critical Path Optimization)
檔案: src/components/App.tsx
問題:
根據 Lighthouse 回饋,關鍵路徑延遲時間上限為 685ms,主要瓶頸是 react-vendor.CPagX_gL.js 在 685ms 才載入完成。
解決方案:
將非關鍵組件改為動態導入(React.lazy),減少初始 bundle 大小。
調整前:
import React from 'react';
import Navbar from './Navbar';
import Hero from './Hero';
import Services from './Services';
import ImpactDashboard from './ImpactDashboard';
import ProjectsGrid from './ProjectsGrid';
import About from './About';
import ExperienceList from './ExperienceList';
import Contact from './Contact';
import Footer from './Footer';
import CookieConsent from './CookieConsent';
import TechStack from './TechStack';
const App: React.FC = () => {
return (
<LanguageProvider>
<Navbar />
<main>
<Hero />
<Services />
<TechStack />
<ImpactDashboard />
<ProjectsGrid />
<About />
<ExperienceList />
<Contact />
</main>
<Footer />
<CookieConsent />
</LanguageProvider>
);
};
調整後:
import React, { Suspense, lazy } from 'react';
import Navbar from './Navbar';
import Hero from './Hero';
import Services from './Services';
// Lazy load non-critical components to reduce initial bundle size
const TechStack = lazy(() => import('./TechStack'));
const ImpactDashboard = lazy(() => import('./ImpactDashboard'));
const ProjectsGrid = lazy(() => import('./ProjectsGrid'));
const About = lazy(() => import('./About'));
const ExperienceList = lazy(() => import('./ExperienceList'));
const Contact = lazy(() => import('./Contact'));
const Footer = lazy(() => import('./Footer'));
const CookieConsent = lazy(() => import('./CookieConsent'));
const App: React.FC = () => {
return (
<LanguageProvider>
<Navbar />
<main>
<Hero />
<Services />
<Suspense fallback={<LoadingFallback />}>
<TechStack />
</Suspense>
<Suspense fallback={<LoadingFallback />}>
<ImpactDashboard />
</Suspense>
{/* ... 其他組件 */}
</main>
<Suspense fallback={null}>
<Footer />
</Suspense>
<Suspense fallback={null}>
<CookieConsent />
</Suspense>
</LanguageProvider>
);
};
說明:
- 關鍵組件同步載入:Navbar、Hero、Services 保持同步載入,確保首屏內容快速顯示
- 非關鍵組件延遲載入:使用
React.lazy和Suspense動態導入非關鍵組件 - 程式碼分割:每個延遲載入的組件會被打包成獨立的 chunk,只在需要時載入
- 載入狀態:使用
Suspense提供載入中的 fallback UI
影響:
- ✅ 減少初始 bundle 大小(預估減少 40-50%)
- ✅ 縮短關鍵路徑延遲時間(從 685ms 降至約 400ms,改善 42%)
- ✅ 改善 FCP 和 LCP 指標
- ✅ 降低初始載入時間
4. 🔍 SEO 與 Meta 標籤優化
4.1 整合 astro-seo
檔案: src/components/layout/base-head.astro
調整內容:
- 整合
astro-seo套件 - 自動生成 Open Graph 和 Twitter Card 標籤
- 支援動態 canonical URL
影響:
- ✅ 改善 SEO 分數
- ✅ 提升社群媒體分享體驗
- ✅ 符合搜尋引擎最佳實踐
5. 📈 預期效能改善
核心 Web Vitals 指標
| 指標 | 優化前 | 優化後 | 改善 |
|---|---|---|---|
| FCP (First Contentful Paint) | ~2.5s | ~1.2s | ⬇️ 52% |
| LCP (Largest Contentful Paint) | ~3.5s | ~2.0s | ⬇️ 43% |
| TBT (Total Blocking Time) | ~600ms | ~200ms | ⬇️ 67% |
| CLS (Cumulative Layout Shift) | ~0.15 | ~0.05 | ⬇️ 67% |
| FID (First Input Delay) | ~150ms | ~50ms | ⬇️ 67% |
PageSpeed Insights 分數預期
| 類別 | 優化前 | 優化後 | 改善 |
|---|---|---|---|
| Performance | 65-75 | 85-95 | ⬆️ +20-30 |
| Accessibility | 90-95 | 90-95 | ➡️ 維持 |
| Best Practices | 85-90 | 90-95 | ⬆️ +5-10 |
| SEO | 85-90 | 95-100 | ⬆️ +10-15 |
最新優化(v2.0)的具體改善
根據最新的 Lighthouse 回饋:
| 問題 | 優化前 | 優化後 | 改善 |
|---|---|---|---|
| 快取生命週期 | 10 分鐘 | 1 年 | 節省 203 KiB |
| 強制自動重排 | 59ms | ~0ms | ⬇️ 100% |
| 關鍵路徑延遲時間上限 | 685ms | ~400ms | ⬇️ 42% |
| 初始 Bundle 大小 | 完整載入 | 減少 40-50% | 改善載入速度 |
6. 🎯 優化重點總結
關鍵優化項目
-
✅ 移除 Render-Blocking 資源
- 移除 CSS
@import - 實作非同步字體載入
- 使用
preload和preconnect
- 移除 CSS
-
✅ 修復強制重排問題
- 使用
requestAnimationFrame批次 DOM 操作 - 避免同步讀取 DOM 屬性
- 修復 Navbar 滾動定位和 Hash Scroll 的強制重排(減少 59ms)
- 使用
-
✅ 優化資源快取
- 實作 Service Worker 快取策略(GitHub Pages)
- 設定長期快取(靜態資源 1 年,HTML 1 小時)
- 實作程式碼分割和內容雜湊
- 節省 203 KiB 的重複載入
-
✅ 優化關鍵路徑載入
- 使用 React.lazy 動態導入非關鍵組件
- 減少初始 bundle 大小 40-50%
- 縮短關鍵路徑延遲時間(從 685ms 降至約 400ms,改善 42%)
-
✅ 改善 SEO
- 整合
astro-seo - 自動生成 Open Graph 標籤
- 整合
7. 📝 技術細節
瀏覽器相容性
- ✅ Chrome/Edge 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ 提供降級方案(
noscript、JavaScript fallback)
行動裝置優化
- ✅ 針對行動網路優化(減少請求數、壓縮資源)
- ✅ 使用
preconnect減少 DNS 查詢時間 - ✅ 非同步載入非關鍵資源
8. 🔄 後續優化建議
-
圖片優化
- 使用 WebP/AVIF 格式
- 實作響應式圖片(
srcset) - 使用圖片 CDN
-
JavaScript 優化
- 實作 Code Splitting(路由層級)
- 使用
client:visible或client:idle延遲載入非關鍵組件 - 考慮使用 Web Workers 處理複雜計算
-
字體優化
- 使用
font-display: swap(已實作) - 考慮使用系統字體作為 fallback
- 實作字體子集化(subsetting)
- 使用
-
Service Worker
- 實作離線快取策略
- 預載入關鍵資源
9. 📚 參考資源
- Web.dev - Render-Blocking Resources
- Web.dev - Avoid Forced Synchronous Layouts
- MDN - requestAnimationFrame
- Web.dev - Preconnect to Required Origins
- Astro SEO Documentation
最後更新: 2024 年 12 月
文件版本: 2.0
最新優化(v2.0):
- ✅ 修復 Hash Scroll 的強制重排問題
- ✅ 實作 Service Worker 快取策略(針對 GitHub Pages)
- ✅ 優化關鍵路徑載入(React.lazy 動態導入)
- ✅ 確保所有資源使用內容雜湊