Engineering & Insights • January 5, 2026

行動版 PageSpeed Insights 優化:完整指南

行動版 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

調整內容:

  1. 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)
  1. 非同步載入字體樣式表
<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:載入完成後自動切換為 stylesheet
  • noscript:為禁用 JavaScript 的瀏覽器提供降級方案
  1. 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

調整內容:

  1. 手動程式碼分割(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 大小
  • 內容雜湊:確保所有資源檔案名稱包含內容雜湊,便於長期快取

影響:

  • ✅ 改善初始載入時間
  • ✅ 提升快取命中率
  • ✅ 減少重複下載
  • ✅ 確保資源有正確的快取生命週期
  1. CSS 程式碼分割
cssCodeSplit: true;

說明:

  • 為每個頁面生成獨立的 CSS 檔案
  • 只載入當前頁面需要的樣式

影響:

  • ✅ 減少初始 CSS 大小
  • ✅ 改善 FCP 和 LCP
  1. 資源內聯閾值
assetsInlineLimit: 4096; // 4KB

說明:

  • 小於 4KB 的資源會內聯到 HTML 中
  • 減少 HTTP 請求數量

影響:

  • ✅ 減少 HTTP 請求數
  • ✅ 降低網路延遲
  1. 依賴預優化
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.lazySuspense 動態導入非關鍵組件
  • 程式碼分割:每個延遲載入的組件會被打包成獨立的 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 分數預期

類別優化前優化後改善
Performance65-7585-95⬆️ +20-30
Accessibility90-9590-95➡️ 維持
Best Practices85-9090-95⬆️ +5-10
SEO85-9095-100⬆️ +10-15

最新優化(v2.0)的具體改善

根據最新的 Lighthouse 回饋:

問題優化前優化後改善
快取生命週期10 分鐘1 年節省 203 KiB
強制自動重排59ms~0ms⬇️ 100%
關鍵路徑延遲時間上限685ms~400ms⬇️ 42%
初始 Bundle 大小完整載入減少 40-50%改善載入速度

6. 🎯 優化重點總結

關鍵優化項目

  1. 移除 Render-Blocking 資源

    • 移除 CSS @import
    • 實作非同步字體載入
    • 使用 preloadpreconnect
  2. 修復強制重排問題

    • 使用 requestAnimationFrame 批次 DOM 操作
    • 避免同步讀取 DOM 屬性
    • 修復 Navbar 滾動定位和 Hash Scroll 的強制重排(減少 59ms)
  3. 優化資源快取

    • 實作 Service Worker 快取策略(GitHub Pages)
    • 設定長期快取(靜態資源 1 年,HTML 1 小時)
    • 實作程式碼分割和內容雜湊
    • 節省 203 KiB 的重複載入
  4. 優化關鍵路徑載入

    • 使用 React.lazy 動態導入非關鍵組件
    • 減少初始 bundle 大小 40-50%
    • 縮短關鍵路徑延遲時間(從 685ms 降至約 400ms,改善 42%)
  5. 改善 SEO

    • 整合 astro-seo
    • 自動生成 Open Graph 標籤

7. 📝 技術細節

瀏覽器相容性

  • ✅ Chrome/Edge 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ 提供降級方案(noscript、JavaScript fallback)

行動裝置優化

  • ✅ 針對行動網路優化(減少請求數、壓縮資源)
  • ✅ 使用 preconnect 減少 DNS 查詢時間
  • ✅ 非同步載入非關鍵資源

8. 🔄 後續優化建議

  1. 圖片優化

    • 使用 WebP/AVIF 格式
    • 實作響應式圖片(srcset
    • 使用圖片 CDN
  2. JavaScript 優化

    • 實作 Code Splitting(路由層級)
    • 使用 client:visibleclient:idle 延遲載入非關鍵組件
    • 考慮使用 Web Workers 處理複雜計算
  3. 字體優化

    • 使用 font-display: swap(已實作)
    • 考慮使用系統字體作為 fallback
    • 實作字體子集化(subsetting)
  4. Service Worker

    • 實作離線快取策略
    • 預載入關鍵資源

9. 📚 參考資源


最後更新: 2024 年 12 月

文件版本: 2.0

最新優化(v2.0):

  • ✅ 修復 Hash Scroll 的強制重排問題
  • ✅ 實作 Service Worker 快取策略(針對 GitHub Pages)
  • ✅ 優化關鍵路徑載入(React.lazy 動態導入)
  • ✅ 確保所有資源使用內容雜湊