🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

從 58MB 到 2.6MB:我是如何將 React 官網性能提升 95% 的

從 58MB 到 2.6MB:React 官網性能優化實戰全記錄

一次完整的 React + Vite 專案性能優化之旅,將首屏加載時間從 4 分鐘降到 13 秒,節省 95% 流量成本。

📖 目錄


引言:一個觸目驚心的發現

最近團隊同事比較忙,正好公司要做新的官網,我之前開發專案結尾就開始了官網的開發,這部分的一些首屏優化之前並沒有做過,寫程式的過程中也沒有太考慮這方面的優化,開發進度一大半的時候,注意到 network 面板的請求資料大小,後就看到了這樣的資料:

總請求:75 個
傳輸大小:58 MB
首屏加載時間(3G):4 分 17 秒 😱

4 分鐘! 這意味著一個使用移動網路的用戶需要等待超過 4 分鐘才能看到我們的首頁。

這不可接受。於是開始了一場性能優化之旅。

優化後的資料

總請求:54 個 (-28%)
傳輸大小:2.57 MB (-95.6%)
首屏加載時間(3G):13.5 秒 ⚡ (快了 94.7%)

這篇文章將完整記錄這次優化的全過程,包括遇到的問題、解決方案、技術原理,以及可以直接重用的工具和程式碼。


專案背景

技術棧

  • 前端框架: React 19 + TypeScript 5
  • 構建工具: Vite 7
  • 路由: React Router v7
  • 樣式: Tailwind CSS v4
  • 組件庫: shadcn/ui (基於 Radix UI)
  • 國際化: i18next + react-i18next
  • 包管理: pnpm
  • 程式碼品質: Biome

專案規模

  • 頁面數量: 8 個主要頁面(Home, Innies, Foundry, Company, Community, Careers, JobDetail, NewsDetail)
  • 組件數量: 50+ 個組件
  • 圖片資源: 18 個 PNG + 1 個 GIF
  • 程式碼行數: ~5000 行 TypeScript/TSX

第一步:性能診斷

發現問題

使用 Chrome DevTools Performance 面板進行初步分析:

# 打開 Chrome DevTools (F12)
# Network 標籤 → 勾選 "Disable cache" → 刷新頁面

image.png

發現的問題

  1. 巨大的圖片檔案 💥

    • case-aerospace.png: 6.4 MB
    • case-fusion.png: 6.0 MB
    • industry-fusion.png: 5.8 MB
    • placeholder.gif: 7.7 MB (一個背景動圖!)
  2. 所有頁面同步加載 💥

    • 即使用戶只訪問首頁,也要下載所有 8 個頁面的程式碼
    • 首屏加載了 CareersPage, CommunityPage, CompanyPage 等不需要的組件
  3. 沒有加載優先級 💥

    • 關鍵首屏圖片和非首屏圖片一視同仁
    • 沒有使用 fetchPriority 或 loading="lazy"

量化目標

制定明確的優化目標:

指標 當前 目標 達成標準
首屏資源 58 MB < 5 MB Good
網路請求 75 < 60 Good
3G 加載時間 257 秒 < 20 秒 Excellent
圖片格式 PNG WebP Modern
路由策略 全同步 懶加載 Optimized

第二步:圖片優化 - PNG 轉 WebP

為什麼 WebP?

WebP 是 Google 開發的現代圖片格式,相比 PNG:

  • 壓縮率提升 70-95%
  • 視覺品質幾乎無損
  • 瀏覽器支持率 97%+

技術原理

  • PNG 使用 DEFLATE 壓縮演算法(1996 年)
  • WebP 使用 VP8/VP9 壓縮演算法(2010 年)
  • WebP 支持預測編碼、變換編碼、熵編碼等先進技術

實施方案

1. 安裝依賴
pnpm add -D sharp
2. 創建轉換腳本

創建 scripts/convert-images-to-webp.mjs:

import sharp from 'sharp';
import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';

const CONFIG = {
  quality: 80,           // WebP 品質 (0-100)
  maxWidth: 1920,       // 最大寬度
  skipIfExists: false,   // 強制重新轉換
  verbose: true          // 詳細輸出
};

async function convertToWebP(inputPath, outputPath) {
  const image = sharp(inputPath);
  const metadata = await image.metadata();

  // 如果圖片過大,自動縮放
  let resizeOptions = {};
  if (metadata.width > CONFIG.maxWidth) {
    resizeOptions = {
      width: CONFIG.maxWidth,
      withoutEnlargement: true
    };
  }

  await image
    .resize(resizeOptions)
    .webp({ quality: CONFIG.quality })
    .toFile(outputPath);

  return { width: metadata.width, height: metadata.height };
}

async function scanDirectory(dir) {
  const files = await readdir(dir);

  for (const file of files) {
    const fullPath = join(dir, file);
    const stats = await stat(fullPath);

    if (stats.isDirectory()) {
      await scanDirectory(fullPath);
    } else if (file.endsWith('.png')) {
      const outputPath = fullPath.replace('.png', '.webp');

      console.log(`Converting: ${file}`);
      await convertToWebP(fullPath, outputPath);

      const originalSize = stats.size;
      const newSize = (await stat(outputPath)).size;
      const reduction = ((1 - newSize / originalSize) * 100).toFixed(1);

      console.log(`  ${(originalSize / 1024 / 1024).toFixed(2)} MB → ${(newSize / 1024 / 1024).toFixed(2)} MB (-${reduction}%)`);
    }
  }
}

// 執行轉換
await scanDirectory('public/images');
3. 運行轉換
node scripts/convert-images-to-webp.mjs

輸出結果

Converting: hero-bg.png
  0.58 MB → 0.08 MB (-86.8%)
Converting: case-aerospace.png
  6.40 MB → 0.20 MB (-96.8%)
Converting: case-fusion.png
  6.00 MB → 0.10 MB (-98.3%)
Converting: industry-fusion.png
  5.80 MB → 0.20 MB (-96.6%)

Total: 50.73 MB → 2.37 MB (-95.3%)
4. 更新組件程式碼

使用 <picture> 元素提供 WebP + PNG fallback:

// ❌ 優化前
<img src="/images/hero-bg.png" alt="Hero Background" />

// ✅ 優化後
<picture>
  <source type="image/webp" srcSet="/images/hero-bg.webp" />
  <img src="/images/hero-bg.png" alt="Hero Background" />
</picture>

為什麼需要 fallback?

  • WebP 瀏覽器支持率 97%+
  • 老舊瀏覽器(IE11, Safari < 14)會自動降級到 PNG
  • 漸進增強策略,確保所有用戶都能看到內容

成果

檔案 優化前 優化後 壓縮率
hero-bg 598 KB 79 KB -86.8%
case-aerospace 6.4 MB 202 KB -96.8%
case-fusion 6.0 MB 99 KB -98.3%
industry-fusion 5.8 MB 196 KB -96.6%
總計 50.7 MB 2.4 MB -95.3%

第三步:視頻優化 - GIF 轉 MP4/WebM

GIF 的問題

我們的背景動圖 placeholder.gif 有 7.7 MB,分析後發現:

  • GIF 格式過時:發明於 1987 年
  • 沒有幀間壓縮:每一幀都是完整圖像
  • 256 色限制:需要抖動處理
  • 體積巨大:7.7 MB 對於一個 1.8 秒的動畫來說太大了

實際影響

GIF 檔案: 7.7 MB
18 幀 × 400 KB/幀 = 7.2 MB
傳輸時間 (3G): 60 秒

對於一個只有 1.8 秒的背景動畫,7.7 MB 的體積是完全不可接受的。

解決方案:轉換為現代視頻格式(MP4/WebM)

實施方案

1. 安裝 ffmpeg
brew install ffmpeg
2. 轉換為 MP4 (H.264)
ffmpeg -i public/images/placeholder.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libx264 \
  -crf 28 \
  -preset medium \
  public/images/placeholder.mp4

參數說明

  • -movflags faststart: 支持漸進式加載
  • -pix_fmt yuv420p: 兼容性最好的色彩空間
  • -crf 28: 品質因子,28 是品質與體積的最佳平衡
  • -preset medium: 編碼速度與品質的平衡
3. 轉換為 WebM (VP9)
ffmpeg -i public/images/placeholder.gif \
  -c:v libvpx-vp9 \
  -crf 35 \
  -b:v 0 \
  -deadline good \
  -cpu-used 2 \
  public/images/placeholder.webm

結果

GIF:  7.7 MB
MP4:  162 KB (-97.8%)
WebM: 170 KB (-97.7%)
4. 更新組件程式碼

關鍵發現:不要在 <video> 內添加 <img> fallback!

// ❌ 錯誤做法 - 會同時加載 GIF 和 Video
<video autoPlay loop muted playsInline>
  <source src="/images/placeholder.webm" type="video/webm" />
  <source src="/images/placeholder.mp4" type="video/mp4" />
  <img src="/images/placeholder.gif" alt="Fallback" />
</video>

// ✅ 正確做法 - 只加載 Video
<video autoPlay loop muted playsInline>
  <source src="/images/placeholder.webm" type="video/webm" />
  <source src="/images/placeholder.mp4" type="video/mp4" />
  {/* 不添加 <img> fallback */}
</video>

為什麼不需要 GIF fallback?

  • 99%+ 瀏覽器支持 HTML5 video
  • WebM 支持率:97%+ (Chrome, Firefox, Edge)
  • MP4 支持率:99%+ (所有現代瀏覽器)
  • 添加 <img> fallback 會導致瀏覽器預加載 GIF

技術深度:為什麼視頻比 GIF 小這麼多?

GIF 的工作原理

幀1: 完整圖像 (400 KB)
幀2: 完整圖像 (400 KB)
幀3: 完整圖像 (400 KB)
...
總計: 18 幀 × 400 KB = 7.2 MB

H.264/VP9 的工作原理

I 幀 (關鍵幀): 完整圖像 (22 KB)
P 幀 (預測幀): 只存儲變化部分 (12 KB)
B 幀 (雙向預測): 只存儲差異 (5 KB)
總計: 1 I 幀 + 8 P 幀 + 9 B 幀 = 162 KB

壓縮比:7.2 MB / 162 KB = 45:1


第四步:路由懶加載

問題分析

優化前的路由配置:

// App.tsx
import { HomePage } from '@/pages/HomePage';
import { CareersPage } from '@/pages/CareersPage';
import { CommunityPage } from '@/pages/CommunityPage';
import { CompanyPage } from '@/pages/CompanyPage';
import { FoundryPage } from '@/pages/FoundryPage';
import { InniesPage } from '@/pages/InniesPage';
import { JobDetailPage } from '@/pages/JobDetailPage';
import { NewsDetailPage } from '@/pages/NewsDetailPage';

// 所有頁面打包在一起,首次訪問就下載所有程式碼
<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/careers" element={<CareersPage />} />
  <Route path="/community" element={<CommunityPage />} />
  {/* ... */}
</Routes>

問題

  • 首次訪問首頁,也會下載 CareersPage, CommunityPage 等程式碼
  • 打包後的 bundle 包含所有頁面(~1.2 MB)
  • 網路請求增加 21 個(所有頁面組件 + Section 組件)

優化方案

使用 React.lazy() 實現路由懶加載:

// App.tsx
import { lazy, Suspense } from 'react';

// ✅ 首頁必須同步加載(避免黑屏)
import { HomePage } from '@/pages/HomePage';

// ✅ 其他頁面懶加載
const CareersPage = lazy(() => import('@/pages/CareersPage'));
const CommunityPage = lazy(() => import('@/pages/CommunityPage'));
const CompanyPage = lazy(() => import('@/pages/CompanyPage'));
const FoundryPage = lazy(() => import('@/pages/FoundryPage'));
const InniesPage = lazy(() => import('@/pages/InniesPage'));
const JobDetailPage = lazy(() => import('@/pages/JobDetailPage'));
const NewsDetailPage = lazy(() => import('@/pages/NewsDetailPage'));

// Loading 組件
function LoadingSpinner() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white" />
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/careers" element={<CareersPage />} />
        <Route path="/community" element={<CommunityPage />} />
        {/* ... */}
      </Routes>
    </Suspense>
  );
}

關鍵問題:為什麼 HomePage 不能懶加載?

很多人會問:既然其他頁面都懶加載了,為什麼 HomePage 不行?

答案:用戶體驗

// ❌ 如果 HomePage 也懶加載
const HomePage = lazy(() => import('@/pages/HomePage'));

// 用戶訪問首頁時的流程:
// 1. 加載 HTML
// 2. 加載 React vendor chunks
// 3. 開始執行 React 程式碼
// 4. 發現 HomePage 是懶加載,開始下載 HomePage chunk
// 5. 黑屏 + 加載指示器 (300-500ms)
// 6. HomePage 下載完成
// 7. 渲染 HomePage 內容

// ✅ HomePage 同步加載
import { HomePage } from '@/pages/HomePage';

// 用戶訪問首頁時的流程:
// 1. 加載 HTML
// 2. 加載 React vendor chunks (已包含 HomePage)
// 3. 開始執行 React 程式碼
// 4. 立即渲染 HomePage 內容 (無黑屏)

結論

  • ✅ 首屏頁面必須同步加載
  • ✅ 非首屏頁面可以懶加載
  • ✅ 用戶體驗 > 技術純粹性

成果

網路請求對比
優化前:

✅ HomePage.tsx
✅ CareersPage.tsx (不需要!)
✅ CommunityPage.tsx (不需要!)
✅ CompanyPage.tsx (不需要!)
✅ FoundryPage.tsx (不需要!)
✅ InniesPage.tsx (不需要!)
✅ JobDetailPage.tsx (不需要!)
✅ NewsDetailPage.tsx (不需要!)
✅ CompanyCTA.tsx (不需要!)
✅ CompanyHero.tsx (不需要!)
... 15+ 個不需要的 Section 組件

優化後:

✅ HomePage.tsx (首頁需要)
✅ HeroSection.tsx (首頁需要)
✅ ProductShowcase.tsx (首頁需要)
... 僅首頁相關的 Section 組件

💤 CareersPage.tsx (懶加載)
💤 CommunityPage.tsx (懶加載)
... 其他頁面按需加載

效果

  • 首屏請求減少:75 → 54 (-28%)
  • JavaScript 減少:~1.2 MB → ~690 KB (-42.5%)

第五步:加載策略優化

fetchPriority 和 loading="lazy"

瀏覽器默認會平等對待所有圖片,但實際上:

  • 首屏圖片(Hero 背景)應該優先加載
  • 非首屏圖片(輪播圖、底部內容)可以延遲加載
1. 關鍵首屏圖片
// Hero 背景圖 - LCP 元素
<picture>
  <source type="image/webp" srcSet="/images/hero-bg.webp" />
  <img src="/images/hero-bg.png" alt="Hero" fetchPriority="high" />  // ✅ 高優先級
  // ❌ 不要使用 loading="lazy"
</picture>

為什麼?

  • Hero 背景通常是 LCP (Largest Contentful Paint) 元素
  • fetchPriority="high" 提升加載優先級,改善 LCP 指標
  • 絕不能使用 loading="lazy",會延遲 LCP
2. 非首屏圖片
// 輪播圖片 - 非首屏
<picture>
  <source type="image/webp" srcSet={image.replace('.png', '.webp')} />
  <img src={image} alt="Carousel" loading="lazy" decoding="async" />  // ✅ 延遲加載
</picture>

效果

  • 首屏只加載可見圖片
  • 節省帶寬:避免加載用戶看不到的圖片
  • 加快首屏:減少並發請求數

Vite 構建優化

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React 核心庫
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          // UI 組件庫
          'ui-vendor': ['lucide-react', '@radix-ui/react-dialog', '@radix-ui/react-slot'],
          // 國際化
          'i18n-vendor': ['i18next', 'react-i18next'],
          // 工具庫
          'utils-vendor': ['clsx', 'tailwind-merge', 'class-variance-authority']
        }
      }
    },
    cssCodeSplit: true,
    minify: 'esbuild',
    target: 'es2020'
  },
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom', 'i18next', 'react-i18next']
  }
});

為什麼要手動分割 chunks?

  • 第三方庫更新頻率低,可以充分利用瀏覽器快取
  • 修改業務程式碼不會使整個 vendor bundle 失效
  • 按庫類型分組,更精細的快取控制

最終成果

性能指標對比

指標 優化前 優化後 改善 狀態
總請求數 75 54 -28%
首屏資源 58 MB 2.57 MB -95.6%
圖片視頻 31 MB (PNG/GIF) 996 KB (WebP/Video) -96.8%
JavaScript ~1.2 MB ~690 KB -42.5%
CLS 0.00 0.00 完美

實際網路請求對比

優化前的網路請求瀑布流 (75 個請求)

頁面組件 (8個,全部同步加載):

reqid=473 GET src/pages/HomePage.tsx [304]
reqid=474 GET src/pages/CareersPage.tsx [304]      ❌ 首屏不需要
reqid=475 GET src/pages/CommunityPage.tsx [304]    ❌ 首屏不需要
reqid=476 GET src/pages/CompanyPage.tsx [304]      ❌ 首屏不需要
reqid=477 GET src/pages/FoundryPage.tsx [304]      ❌ 首屏不需要
reqid=478 GET src/pages/InniesPage.tsx [304]       ❌ 首屏不需要
reqid=479 GET src/pages/JobDetailPage.tsx [304]    ❌ 首屏不需要
reqid=480 GET src/pages/NewsDetailPage.tsx [304]   ❌ 首屏不需要

Section 組件 (15+個,全部同步加載):

reqid=488 GET src/components/sections/CompanyCTA.tsx [304]      ❌ Company 頁面用
reqid=489 GET src/components/sections/CompanyHero.tsx [304]     ❌ Company 頁面用
reqid=490 GET src/components/sections/CompanyImage.tsx [304]    ❌ Company 頁面用
reqid=491 GET src/components/sections/CompanyMission.tsx [304]  ❌ Company 頁面用
reqid=492 GET src/components/sections/CompanyValues.tsx [304]   ❌ Company 頁面用
reqid=493 GET src/components/sections/CompanyVision.tsx [304]   ❌ Company 頁面用
reqid=494 GET src/components/sections/CommunityHero.tsx [304]   ❌ Community 頁面用
reqid=495 GET src/components/sections/CommunityMission.tsx [304]❌ Community 頁面用
reqid=504 GET src/components/sections/CareersHero.tsx [304]     ❌ Careers 頁面用
reqid=505 GET src/components/sections/CareersJobs.tsx [304]     ❌ Careers 頁面用

圖片資源 (PNG 格式):

reqid=520 GET images/hero-bg.png [304]            598 KB
reqid=521 GET images/case-fusion.png [304]        6.0 MB
reqid=522 GET images/case-semiconductor.png [304] 1.3 MB
reqid=523 GET images/case-aerospace.png [304]     6.4 MB
reqid=524 GET images/placeholder.gif [304]        7.7 MB  ❌ 巨大!
reqid=525 GET images/industry-aerospace.png [304] 3.3 MB
reqid=526 GET images/industry-fusion.png [304]    5.8 MB

問題總結

  • ❌ 加載了 7 個不需要的頁面組件
  • ❌ 加載了 10+ 個不需要的 Section 組件
  • ❌ 所有圖片都是 PNG 格式(體積大)
  • ❌ 一個 GIF 動圖就占 7.7 MB

優化後的網路請求瀑布流 (54 個請求)

頁面組件 (1個同步,7個懶加載):

reqid=707 GET src/pages/HomePage.tsx [304]  ✅ 首頁必需,同步加載

注意: CareersPage, CommunityPage, CompanyPage 等頁面組件在首屏不再加載,只有用戶訪問對應路由時才會按需下載!

Section 組件 (僅首頁相關):

reqid=715 GET src/components/sections/CTASection.tsx [304]             ✅ 首頁用
reqid=716 GET src/components/sections/HeroSection.tsx [304]            ✅ 首頁用
reqid=717 GET src/components/sections/IndustryDetailSection.tsx [304]  ✅ 首頁用
reqid=718 GET src/components/sections/IndustryFusionSection.tsx [304]  ✅ 首頁用
reqid=719 GET src/components/sections/PlaceholderSection.tsx [304]     ✅ 首頁用
reqid=720 GET src/components/sections/ProductListSection.tsx [304]     ✅ 首頁用
reqid=721 GET src/components/sections/ProductShowcase.tsx [304]        ✅ 首頁用
reqid=722 GET src/components/sections/TestimonialsSection.tsx [304]    ✅ 首頁用

注意: CompanyCTA, CompanyHero 等組件在首屏不再加載,與對應頁面一起懶加載!

圖片/視頻資源 (WebP + Video 格式):

reqid=735 GET images/hero-bg.webp [304]            79 KB   ✅ 從 598 KB
reqid=736 GET images/case-fusion.webp [304]        99 KB   ✅ 從 6.0 MB
reqid=737 GET images/case-semiconductor.webp [304] 72 KB   ✅ 從 1.3 MB
reqid=738 GET images/placeholder.webm [206]        170 KB  ✅ 從 7.7 MB
reqid=739 GET images/case-aerospace.webp [304]     202 KB  ✅ 從 6.4 MB

改善總結

  • ✅ 只加載首頁需要的組件(減少 21 個請求)
  • ✅ 所有圖片都轉換為 WebP(體積減少 95%+)
  • ✅ GIF 轉換為 WebM 視頻(體積減少 97.7%)
  • ✅ 資源總大小從 31 MB 降到 996 KB

用戶體驗改善

網路環境 優化前 優化後 提升
4G (10 Mbps) 25.8 秒 1.4 秒 ⚡ 快 94.6%
3G (1 Mbps) 4 分 17 秒 13.5 秒 ⚡ 快 94.7%
回訪用戶 5-10 秒 0.1-0.2 秒 ⚡ 快 97-98%

為什麼 WebP 這麼高效?

PNG 的壓縮過程

原始像素數據
    ↓
預測編碼 (簡單)
    ↓
DEFLATE 壓縮 (1996 年技術)
    ↓
PNG 檔案 (壓縮率: 30-50%)

WebP 的壓縮過程

原始像素數據
    ↓
預測編碼 (高級 - 16 種預測模式)
    ↓
DCT 變換編碼 (頻域壓縮)
    ↓
量化 (丟弃不重要的信息)
    ↓
熵編碼 (算術編碼)
    ↓
WebP 檔案 (壓縮率: 70-95%)

為什麼視頻比 GIF 高效?

關鍵技術對比

特性 GIF H.264 VP9
發明年代 1987 2003 2013
幀間壓縮 ❌ 無 ✅ I/P/B 幀 ✅ I/P/B 幀
運動補償 ❌ 無 ✅ 有 ✅ 有
色彩空間 256 色 1670 萬色 1670 萬色
比特率控制 ❌ 固定 ✅ 可變 ✅ 可變
壓縮效率 1x 20-30x 30-50x

I/P/B 幀解釋

  • I 幀 (Intra-frame): 關鍵幀,完整圖像
  • P 幀 (Predicted): 預測幀,只存儲與前一幀的差異
  • B 幀 (Bidirectional): 雙向預測,參考前後幀

React.lazy() 工作原理

// 1. React.lazy 接收一個返回 Promise 的函數
const LazyComponent = lazy(() => import('./Component'));

// 2. 首次渲染時,React 會調用這個函數
const promise = import('./Component');

// 3. import() 返回一個 Promise
// Vite/Webpack 會自動進行程式碼分割,生成獨立的 chunk

// 4. Suspense 捕獲這個 Promise
<Suspense fallback={<Loading />}>
  <LazyComponent />
</Suspense>

// 5. Promise pending 時顯示 fallback
// 6. Promise resolved 後渲染實際組件

踩坑紀錄:優化過程中遇到的真實問題

在實際優化過程中,我們遇到了幾個關鍵問題。這些經驗教訓比成功案例更有價值。

問題 1:HomePage 懶加載導致首屏黑屏 ⚠️ HIGH

錯誤做法
一開始,我將所有頁面都改成了懶加載,包括 HomePage:

// ❌ 錯誤:所有頁面都懶加載
const HomePage = lazy(() => import('@/pages/HomePage'));
const InniesPage = lazy(() => import('@/pages/InniesPage'));
const FoundryPage = lazy(() => import('@/pages/FoundryPage'));
// ... 其他頁面

問題表現

  • 用戶訪問首頁時出現明顯的黑屏
  • 可以看到加載指示器(轉圈圈)
  • FCP (First Contentful Paint) 從 240ms 退化到 500-700ms
  • 用戶體驗顯著倒退

原因分析
用戶訪問首頁流程:

  1. 加載 HTML (10ms)
  2. 加載 React vendor chunks (50ms)
  3. React 開始執行
  4. 發現 HomePage 是 lazy 組件,開始下載 HomePage chunk
  5. ⬛⬛⬛ 黑屏等待 300-500ms ⬛⬛⬛
  6. HomePage chunk 下載完成
  7. 渲染 HomePage 內容

正確做法

// ✅ 正確:首頁同步,其他懶加載
import { HomePage } from '@/pages/HomePage';  // 同步加載

const InniesPage = lazy(() => import('@/pages/InniesPage'));
const FoundryPage = lazy(() => import('@/pages/FoundryPage'));
// ... 其他頁面懶加載

為什麼這樣做?

  • HomePage 是用戶訪問的第一個頁面,必須立即渲染
  • 同步加載 HomePage 可以避免黑屏等待
  • 其他頁面用戶可能不會訪問,懶加載可以節省帶寬

驗證方法

# 檢查 HomePage 不應該在懶加載的頁面中
grep -n "const HomePage = lazy" src/App.tsx
# 應該返回空結果

# 檢查 HomePage 應該是同步導入
grep -n "import { HomePage }" src/App.tsx
# 應該有結果

問題 2:Hero 圖片懶加載延遲 LCP ⚠️ HIGH

錯誤做法
優化時,我給所有圖片都加了 loading="lazy",包括首屏的 Hero 背景圖:

// ❌ 錯誤:Hero 背景圖也懶加載
<picture>
  <source type="image/webp" srcSet="/images/innies/hero-background.webp" />
  <img src="/images/innies/hero-background.png" loading="lazy" />  // ❌ 這是錯的!
</picture>

問題表現

  • LCP (Largest Contentful Paint) 指標顯著惡化
  • Hero 背景圖延遲加載,用戶看到白色空白
  • Performance trace 顯示 LCP 從 155ms 退化到 500ms+

原因分析
loading="lazy" 的工作原理:

// 瀏覽器邏輯
if (圖片距離視口 < 1000-2000px) {
  開始加載圖片;
} else {
  等待滾動到接近位置;
}

Hero 背景圖是首屏可見的,但 loading="lazy" 會:

  1. 延遲發起網路請求
  2. 等待 JavaScript 執行完成
  3. 然後才開始加載

正確做法

// ✅ 正確:Hero 圖片高優先級
<picture>
  <source type="image/webp" srcSet="/images/innies/hero-background.webp" />
  <img src="/images/innies/hero-background.png" fetchPriority="high" />  // ✅ 高優先級
</picture>

原文出處:https://juejin.cn/post/7566820121459294214


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝22   💬4   ❤️6
674
🥈
我愛JS
📝1   💬4   ❤️2
44
🥉
酷豪
1
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付