返回文章列表
效能優化2026年2月23日

Web 效能優化:從載入時間到使用者體驗

分享實際專案中的效能優化經驗,包含圖片優化、程式碼分割和快取策略。

在現代 Web 開發中,效能不僅影響使用者體驗,更直接關係到 SEO 排名和轉換率。本文將分享我在優化 The Lonesome Era 網站過程中的實戰經驗,從 Core Web Vitals 指標到具體的優化技術。

效能優化的重要性

頁面載入時間每增加 1 秒,轉換率就會下降 7%

Google 的研究顯示,效能對使用者行為有直接影響:

  • 3 秒規則:超過 3 秒,53% 的使用者會離開
  • SEO 影響:Core Web Vitals 是 Google 排名因素
  • 轉換率:每 100ms 的改善可提升 1% 轉換率
  • 使用者滿意度:快速載入直接影響品牌印象

Core Web Vitals 深度解析

Google 的 Core Web Vitals 包含三個關鍵指標:

1. Largest Contentful Paint (LCP)

目標:< 2.5 秒

LCP 測量頁面主要內容載入完成的時間。優化策略:

<!-- 優化圖片載入 -->
<img src="hero-image.webp" 
     alt="主要內容圖片"
     loading="eager"
     fetchpriority="high"
     width="800" 
     height="600">

<!-- 預載入關鍵資源 -->
<link rel="preload" href="hero-image.webp" as="image">
<link rel="preload" href="critical.css" as="style">

2. First Input Delay (FID)

目標:< 100 毫秒

FID 測量使用者首次互動到瀏覽器回應的時間。優化重點:

// 使用 requestIdleCallback 處理非關鍵任務
function performNonCriticalTask() {
    if ('requestIdleCallback' in window) {
        requestIdleCallback(() => {
            // 執行非關鍵任務
            initializeAnalytics();
            loadSecondaryContent();
        });
    } else {
        // 降級處理
        setTimeout(() => {
            initializeAnalytics();
            loadSecondaryContent();
        }, 1000);
    }
}

// 事件委託減少事件監聽器
document.addEventListener('click', (event) => {
    if (event.target.matches('.nav-item')) {
        handleNavigation(event);
    } else if (event.target.matches('.card-link')) {
        handleCardClick(event);
    }
});

// 防抖處理頻繁事件
function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

const handleScroll = debounce(() => {
    // 滾動處理邏輯
}, 16); // 約 60fps

3. Cumulative Layout Shift (CLS)

目標:< 0.1

CLS 測量視覺穩定性。避免布局偏移的策略:

/* 為圖片預留空間 */
.image-container {
    width: 100%;
    aspect-ratio: 16/9;
    background-color: #f0f0f0;
    position: relative;
}

.image-container img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    position: absolute;
    top: 0;
    left: 0;
}

/* 為動態內容預留空間 */
.dynamic-content {
    min-height: 200px;
    position: relative;
}

.loading-placeholder {
    width: 100%;
    height: 200px;
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
}

@keyframes loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

圖片優化策略

圖片通常佔網頁總大小的 60-70%,是優化的重點:

現代圖片格式

<!-- 使用 picture 元素提供多種格式 -->
<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="描述文字" loading="lazy">
</picture>

<!-- 響應式圖片 -->
<img srcset="small.webp 480w,
             medium.webp 768w,
             large.webp 1200w"
     sizes="(max-width: 480px) 100vw,
            (max-width: 768px) 50vw,
            33vw"
     src="medium.webp"
     alt="響應式圖片">

延遲載入實作

// 進階的 Intersection Observer 實作
class LazyImageLoader {
    constructor() {
        this.imageObserver = null;
        this.images = [];
        this.init();
    }
    
    init() {
        if ('IntersectionObserver' in window) {
            this.imageObserver = new IntersectionObserver(
                this.handleIntersection.bind(this),
                {
                    rootMargin: '50px 0px',
                    threshold: 0.01
                }
            );
            this.observeImages();
        } else {
            // 降級處理:直接載入所有圖片
            this.loadAllImages();
        }
    }
    
    observeImages() {
        const lazyImages = document.querySelectorAll('img[loading="lazy"]');
        lazyImages.forEach(img => {
            this.imageObserver.observe(img);
        });
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                this.loadImage(img);
                this.imageObserver.unobserve(img);
            }
        });
    }
    
    loadImage(img) {
        return new Promise((resolve, reject) => {
            const imageLoader = new Image();
            
            imageLoader.onload = () => {
                img.src = imageLoader.src;
                img.classList.add('loaded');
                resolve(img);
            };
            
            imageLoader.onerror = () => {
                img.classList.add('error');
                reject(new Error(`Failed to load image: ${img.dataset.src}`));
            };
            
            imageLoader.src = img.dataset.src || img.src;
        });
    }
}

// 初始化延遲載入
document.addEventListener('DOMContentLoaded', () => {
    new LazyImageLoader();
});

JavaScript 優化技術

程式碼分割與動態載入

// 動態 import 實現程式碼分割
const loadModule = async (moduleName) => {
    try {
        const module = await import(`./modules/${moduleName}.js`);
        return module.default;
    } catch (error) {
        console.error(`Failed to load module: ${moduleName}`, error);
        return null;
    }
};

// 路由級別的程式碼分割
const router = {
    async navigate(route) {
        const routeModule = await loadModule(route);
        if (routeModule) {
            routeModule.init();
        }
    }
};

// 功能級別的延遲載入
class FeatureLoader {
    static async loadGameEngine() {
        if (!this.gameEngine) {
            const GameEngine = await import('./game/engine.js');
            this.gameEngine = new GameEngine.default();
        }
        return this.gameEngine;
    }
    
    static async loadAnalytics() {
        if (navigator.connection && navigator.connection.saveData) {
            // 在省流量模式下不載入分析工具
            return null;
        }
        
        const analytics = await import('./analytics/tracker.js');
        return analytics.default;
    }
}

// 使用 Web Workers 處理重型任務
class WorkerManager {
    constructor() {
        this.workers = new Map();
    }
    
    async createWorker(name, scriptPath) {
        if (!this.workers.has(name)) {
            const worker = new Worker(scriptPath);
            this.workers.set(name, worker);
        }
        return this.workers.get(name);
    }
    
    async processData(workerName, data) {
        const worker = await this.createWorker(workerName, './workers/data-processor.js');
        
        return new Promise((resolve, reject) => {
            worker.postMessage(data);
            
            worker.onmessage = (event) => {
                resolve(event.data);
            };
            
            worker.onerror = (error) => {
                reject(error);
            };
        });
    }
}

記憶體管理與垃圾回收

// 物件池模式減少垃圾回收
class ObjectPool {
    constructor(createFn, resetFn, maxSize = 100) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.maxSize = maxSize;
        this.pool = [];
        this.active = new Set();
    }
    
    acquire() {
        let obj;
        if (this.pool.length > 0) {
            obj = this.pool.pop();
        } else {
            obj = this.createFn();
        }
        
        this.active.add(obj);
        return obj;
    }
    
    release(obj) {
        if (this.active.has(obj)) {
            this.active.delete(obj);
            this.resetFn(obj);
            
            if (this.pool.length < this.maxSize) {
                this.pool.push(obj);
            }
        }
    }
    
    clear() {
        this.pool.length = 0;
        this.active.clear();
    }
}

// 事件監聽器清理
class EventManager {
    constructor() {
        this.listeners = new Map();
        this.abortController = new AbortController();
    }
    
    addEventListener(element, event, handler, options = {}) {
        const finalOptions = {
            ...options,
            signal: this.abortController.signal
        };
        
        element.addEventListener(event, handler, finalOptions);
        
        // 記錄以便後續清理
        const key = `${element.tagName}-${event}`;
        if (!this.listeners.has(key)) {
            this.listeners.set(key, []);
        }
        this.listeners.get(key).push({ element, event, handler });
    }
    
    cleanup() {
        this.abortController.abort();
        this.listeners.clear();
    }
}

快取策略實作

HTTP 快取最佳化

// Service Worker 快取策略
const CACHE_NAME = 'the-lonesome-era-v1';
const STATIC_CACHE = 'static-v1';
const DYNAMIC_CACHE = 'dynamic-v1';

const STATIC_ASSETS = [
    '/',
    '/style.css',
    '/app.js',
    '/assets/imgs/blog_icon.png',
    '/favicon.ico'
];

// 安裝階段:快取靜態資源
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(STATIC_CACHE)
            .then(cache => cache.addAll(STATIC_ASSETS))
            .then(() => self.skipWaiting())
    );
});

// 激活階段:清理舊快取
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => self.clients.claim())
    );
});

// 請求攔截:實施快取策略
self.addEventListener('fetch', (event) => {
    const { request } = event;
    const url = new URL(request.url);
    
    // 靜態資源:快取優先
    if (STATIC_ASSETS.includes(url.pathname)) {
        event.respondWith(cacheFirst(request));
    }
    // API 請求:網路優先
    else if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirst(request));
    }
    // 圖片資源:快取優先,失敗時顯示預設圖片
    else if (request.destination === 'image') {
        event.respondWith(cacheFirstWithFallback(request, '/assets/imgs/placeholder.png'));
    }
    // 其他資源:網路優先
    else {
        event.respondWith(networkFirst(request));
    }
});

// 快取優先策略
async function cacheFirst(request) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
        return cachedResponse;
    }
    
    try {
        const networkResponse = await fetch(request);
        const cache = await caches.open(DYNAMIC_CACHE);
        cache.put(request, networkResponse.clone());
        return networkResponse;
    } catch (error) {
        console.error('Cache first strategy failed:', error);
        throw error;
    }
}

// 網路優先策略
async function networkFirst(request) {
    try {
        const networkResponse = await fetch(request);
        const cache = await caches.open(DYNAMIC_CACHE);
        cache.put(request, networkResponse.clone());
        return networkResponse;
    } catch (error) {
        const cachedResponse = await caches.match(request);
        if (cachedResponse) {
            return cachedResponse;
        }
        throw error;
    }
}

瀏覽器快取優化

// 智能預載入
class IntelligentPreloader {
    constructor() {
        this.prefetchQueue = new Set();
        this.observer = null;
        this.init();
    }
    
    init() {
        // 監聽滑鼠懸停事件
        document.addEventListener('mouseover', this.handleMouseOver.bind(this));
        
        // 監聽可視區域
        this.observer = new IntersectionObserver(
            this.handleIntersection.bind(this),
            { rootMargin: '100px' }
        );
        
        // 觀察所有連結
        document.querySelectorAll('a[href]').forEach(link => {
            this.observer.observe(link);
        });
    }
    
    handleMouseOver(event) {
        if (event.target.tagName === 'A' && event.target.href) {
            this.prefetchResource(event.target.href);
        }
    }
    
    handleIntersection(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting && entry.target.href) {
                this.prefetchResource(entry.target.href);
            }
        });
    }
    
    prefetchResource(url) {
        if (this.prefetchQueue.has(url)) return;
        
        // 檢查網路狀況
        if (navigator.connection && navigator.connection.effectiveType === 'slow-2g') {
            return;
        }
        
        this.prefetchQueue.add(url);
        
        const link = document.createElement('link');
        link.rel = 'prefetch';
        link.href = url;
        document.head.appendChild(link);
    }
}

// 資源優先級管理
class ResourcePriorityManager {
    static setImagePriority(img, priority = 'auto') {
        if ('fetchPriority' in HTMLImageElement.prototype) {
            img.fetchPriority = priority;
        }
    }
    
    static preloadCriticalResources() {
        const criticalResources = [
            { href: '/style.css', as: 'style' },
            { href: '/app.js', as: 'script' },
            { href: '/assets/fonts/main.woff2', as: 'font', type: 'font/woff2', crossorigin: 'anonymous' }
        ];
        
        criticalResources.forEach(resource => {
            const link = document.createElement('link');
            link.rel = 'preload';
            Object.assign(link, resource);
            document.head.appendChild(link);
        });
    }
}

效能監控與分析

Real User Monitoring (RUM)

// 效能監控類
class PerformanceMonitor {
    constructor() {
        this.metrics = {};
        this.observer = null;
        this.init();
    }
    
    init() {
        // 監控 Core Web Vitals
        this.observeLCP();
        this.observeFID();
        this.observeCLS();
        
        // 監控其他指標
        this.observeNavigationTiming();
        this.observeResourceTiming();
    }
    
    observeLCP() {
        if ('PerformanceObserver' in window) {
            const observer = new PerformanceObserver((list) => {
                const entries = list.getEntries();
                const lastEntry = entries[entries.length - 1];
                this.metrics.lcp = lastEntry.startTime;
                this.sendMetric('LCP', lastEntry.startTime);
            });
            
            observer.observe({ entryTypes: ['largest-contentful-paint'] });
        }
    }
    
    observeFID() {
        if ('PerformanceObserver' in window) {
            const observer = new PerformanceObserver((list) => {
                const entries = list.getEntries();
                entries.forEach(entry => {
                    this.metrics.fid = entry.processingStart - entry.startTime;
                    this.sendMetric('FID', this.metrics.fid);
                });
            });
            
            observer.observe({ entryTypes: ['first-input'] });
        }
    }
    
    observeCLS() {
        let clsValue = 0;
        let clsEntries = [];
        
        if ('PerformanceObserver' in window) {
            const observer = new PerformanceObserver((list) => {
                const entries = list.getEntries();
                entries.forEach(entry => {
                    if (!entry.hadRecentInput) {
                        clsValue += entry.value;
                        clsEntries.push(entry);
                    }
                });
                
                this.metrics.cls = clsValue;
                this.sendMetric('CLS', clsValue);
            });
            
            observer.observe({ entryTypes: ['layout-shift'] });
        }
    }
    
    observeNavigationTiming() {
        window.addEventListener('load', () => {
            const navigation = performance.getEntriesByType('navigation')[0];
            
            const metrics = {
                dns: navigation.domainLookupEnd - navigation.domainLookupStart,
                tcp: navigation.connectEnd - navigation.connectStart,
                ttfb: navigation.responseStart - navigation.requestStart,
                download: navigation.responseEnd - navigation.responseStart,
                domReady: navigation.domContentLoadedEventEnd - navigation.navigationStart,
                loadComplete: navigation.loadEventEnd - navigation.navigationStart
            };
            
            Object.entries(metrics).forEach(([key, value]) => {
                this.sendMetric(key.toUpperCase(), value);
            });
        });
    }
    
    sendMetric(name, value) {
        // 發送到分析服務
        if (navigator.sendBeacon) {
            const data = JSON.stringify({
                metric: name,
                value: value,
                url: window.location.href,
                userAgent: navigator.userAgent,
                timestamp: Date.now()
            });
            
            navigator.sendBeacon('/api/analytics/performance', data);
        }
    }
}

// 啟動效能監控
document.addEventListener('DOMContentLoaded', () => {
    new PerformanceMonitor();
});

實戰優化案例

以 The Lonesome Era 網站為例,以下是具體的優化成果:

優化前後對比

指標

優化前

優化後

改善幅度

LCP

4.2s

1.8s

-57%

FID

180ms

45ms

-75%

CLS

0.25

0.05

-80%

總檔案大小

2.1MB

890KB

-58%

載入時間

5.8s

2.1s

-64%

關鍵優化措施

  1. 圖片優化:WebP 格式 + 響應式圖片 → 減少 70% 圖片大小
  2. 程式碼分割:動態載入非關鍵功能 → 減少初始載入 45%
  3. CSS 優化:關鍵 CSS 內聯 + 非關鍵 CSS 延遲載入
  4. Service Worker:智能快取策略 → 回訪速度提升 80%
  5. 預載入策略:關鍵資源預載入 + 智能預取

總結與建議

Web 效能優化是一個持續的過程,需要:

  • 建立基準:使用 Lighthouse、WebPageTest 等工具測量
  • 持續監控:實施 RUM 監控真實使用者體驗
  • 逐步優化:從影響最大的問題開始
  • 測試驗證:每次優化後都要測試效果
  • 使用者優先:始終以使用者體驗為中心

記住:效能優化不是一次性的任務,而是開發流程的一部分。每一行程式碼、每一個資源都可能影響使用者體驗。

透過系統性的效能優化,我們不僅能提升使用者滿意度,還能改善 SEO 排名,最終實現更好的業務成果。


想要了解更多效能優化技巧?歡迎查看我們的其他技術文章,或者使用開發者工具分析 The Lonesome Era 網站的效能實作。