經過 Day 17 的微服務架構建置,我們的系統具備了良好的擴展性與維護性。今天我們要轉向前端效能優化,從打包策略、資源載入、到渲染效能,全面提升使用者體驗。我們將基於現有的 React + Vite 架構,實現企業級的效能優化方案。
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { splitVendorChunkPlugin } from 'vite';
export default defineConfig({
plugins: [
react(),
splitVendorChunkPlugin()
],
build: {
target: 'es2015',
minify: 'terser',
cssMinify: true,
reportCompressedSize: false,
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks: {
// 核心框架單獨打包
'vendor-react': ['react', 'react-dom'],
'vendor-router': ['react-router-dom'],
// UI 組件庫單獨打包
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
// 工具函式庫單獨打包
'vendor-utils': ['lodash-es', 'date-fns', 'zod'],
// 圖表與可視化
'vendor-charts': ['recharts', 'lucide-react']
},
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/').pop()
: 'chunk';
return `js/${facadeModuleId}-[hash].js`;
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const extType = info[info.length - 1];
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
return `images/[name]-[hash][extname]`;
}
if (/css/i.test(extType)) {
return `css/[name]-[hash][extname]`;
}
return `assets/[name]-[hash][extname]`;
}
}
},
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info'],
passes: 2
},
mangle: {
safari10: true
},
format: {
comments: false
}
}
}
});
// src/router/LazyRoutes.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
// 使用 React.lazy 實現路由級別的程式碼分割
const Dashboard = lazy(() => import('@/pages/Dashboard'));
const Members = lazy(() => import('@/pages/Members'));
const Courses = lazy(() => import('@/pages/Courses'));
const Analytics = lazy(() => import('@/pages/Analytics'));
const Settings = lazy(() => import('@/pages/Settings'));
// 預載入組件工廠
const withPreload = <T extends object>(
importFn: () => Promise<{ default: React.ComponentType<T> }>
) => {
const Component = lazy(importFn);
// 提供預載入方法
Component.preload = importFn;
return Component;
};
// 高優先級路由立即載入
const DashboardWithPreload = withPreload(() => import('@/pages/Dashboard'));
const MembersWithPreload = withPreload(() => import('@/pages/Members'));
export const AppRoutes = () => {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<DashboardWithPreload />} />
<Route path="/members" element={<MembersWithPreload />} />
<Route path="/courses" element={<Courses />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
};
// 路由預載入 Hook
export const useRoutePreloader = () => {
const preloadRoute = (routeName: string) => {
switch (routeName) {
case 'members':
MembersWithPreload.preload();
break;
case 'dashboard':
DashboardWithPreload.preload();
break;
}
};
return { preloadRoute };
};
// src/utils/ResourcePreloader.ts
interface PreloadResource {
href: string;
as: 'script' | 'style' | 'font' | 'image';
type?: string;
crossorigin?: 'anonymous' | 'use-credentials';
integrity?: string;
}
export class ResourcePreloader {
private static instance: ResourcePreloader;
private preloadedResources = new Set<string>();
private observer: IntersectionObserver | null = null;
static getInstance(): ResourcePreloader {
if (!ResourcePreloader.instance) {
ResourcePreloader.instance = new ResourcePreloader();
}
return ResourcePreloader.instance;
}
// 關鍵資源預載入
preloadCriticalResources(): void {
const criticalResources: PreloadResource[] = [
{
href: '/fonts/inter-var.woff2',
as: 'font',
type: 'font/woff2',
crossorigin: 'anonymous'
},
{
href: '/js/vendor-react.js',
as: 'script'
},
{
href: '/css/app.css',
as: 'style'
}
];
criticalResources.forEach(resource => {
this.preloadResource(resource);
});
}
// 路由切換時預載入
preloadRouteResources(routeName: string): void {
const routeResources: Record<string, PreloadResource[]> = {
members: [
{ href: '/js/members-chunk.js', as: 'script' },
{ href: '/api/members?limit=50', as: 'fetch' }
],
courses: [
{ href: '/js/courses-chunk.js', as: 'script' },
{ href: '/js/vendor-charts.js', as: 'script' }
]
};
const resources = routeResources[routeName];
if (resources) {
resources.forEach(resource => this.preloadResource(resource));
}
}
// 視窗內圖片懶載入
lazyLoadImages(): void {
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const src = img.dataset.src;
if (src) {
img.src = src;
img.removeAttribute('data-src');
this.observer?.unobserve(img);
}
}
});
},
{
rootMargin: '50px 0px',
threshold: 0.01
}
);
document.querySelectorAll('img[data-src]').forEach(img => {
this.observer?.observe(img);
});
}
}
private preloadResource(resource: PreloadResource): void {
if (this.preloadedResources.has(resource.href)) {
return;
}
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource.href;
link.as = resource.as;
if (resource.type) link.type = resource.type;
if (resource.crossorigin) link.crossOrigin = resource.crossorigin;
if (resource.integrity) link.integrity = resource.integrity;
document.head.appendChild(link);
this.preloadedResources.add(resource.href);
}
}
// src/utils/ConnectionOptimizer.ts
export class ConnectionOptimizer {
private static readonly EXTERNAL_DOMAINS = [
'api.mitake.com.tw',
'fonts.googleapis.com',
'cdnjs.cloudflare.com'
];
static initializeConnections(): void {
// DNS 預解析
this.EXTERNAL_DOMAINS.forEach(domain => {
this.addDNSPrefetch(domain);
});
// 重要資源域名預連線
this.addPreconnect('https://fonts.googleapis.com');
this.addPreconnect('https://api.mitake.com.tw');
// 次要資源域名 DNS 預解析
this.addDNSPrefetch('cdnjs.cloudflare.com');
this.addDNSPrefetch('www.google-analytics.com');
}
private static addDNSPrefetch(hostname: string): void {
const link = document.createElement('link');
link.rel = 'dns-prefetch';
link.href = `//${hostname}`;
document.head.appendChild(link);
}
private static addPreconnect(url: string): void {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = url;
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
}
// 模組預載入
static preloadModules(): void {
if ('modulepreload' in HTMLLinkElement.prototype) {
const criticalModules = [
'/src/main.tsx',
'/src/App.tsx',
'/src/router/index.tsx'
];
criticalModules.forEach(module => {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = module;
document.head.appendChild(link);
});
}
}
}
現代 Web 應用中,選擇正確的壓縮算法對效能影響巨大。讓我們深入比較主流壓縮算法:
算法 | 壓縮率 | 壓縮速度 | 解壓速度 | 瀏覽器支援 | CPU 消耗 | 適用場景 |
---|---|---|---|---|---|---|
Gzip | 70-80% | 快 | 快 | 99.9% | 低 | 通用,相容性要求高 |
Brotli | 75-85% | 中等 | 快 | 95%+ | 中等 | 現代瀏覽器,追求最佳壓縮 |
Zstd | 80-90% | 快 | 極快 | 有限 | 低 | 實驗性,未來趨勢 |
LZ4 | 60-70% | 極快 | 極快 | 無 | 極低 | 即時壓縮需求 |
// vite.config.ts - 多層壓縮策略
import { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression';
export default defineConfig({
plugins: [
// Gzip 壓縮 - 相容性佳,作為後備方案
compression({
algorithm: 'gzip',
ext: '.gz',
threshold: 1024,
compressionOptions: {
level: 9, // 最高壓縮級別
memLevel: 9, // 記憶體使用級別
strategy: 0, // 預設策略,適合大多數情況
windowBits: 15, // 窗口大小,影響壓縮率與記憶體使用
chunkSize: 16384 // 處理塊大小
},
deleteOriginFile: false,
filter: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i
}),
// Brotli 壓縮 - 更佳壓縮率,現代瀏覽器首選
compression({
algorithm: 'brotliCompress',
ext: '.br',
threshold: 1024,
compressionOptions: {
level: 11, // Brotli 壓縮級別 (0-11)
mode: 0, // 通用模式 (0: 通用, 1: 文字, 2: 字型)
windowBits: 22, // 窗口大小,影響記憶體使用
quality: 11, // 品質級別,影響壓縮時間與率
lgwin: 22, // LZ77 窗口大小的對數
lgblock: 0 // 輸入塊大小的對數 (0 為自動)
},
deleteOriginFile: false,
filter: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i
}),
// 自訂壓縮策略
{
name: 'adaptive-compression',
generateBundle(options, bundle) {
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName];
if (file.type === 'chunk' || file.type === 'asset') {
const content = typeof file.source === 'string'
? file.source
: new TextDecoder().decode(file.source);
// 根據檔案類型選擇最佳壓縮策略
if (fileName.endsWith('.js')) {
// JavaScript 檔案:優先 Brotli
this.adaptiveCompression(content, fileName, 'javascript');
} else if (fileName.endsWith('.css')) {
// CSS 檔案:Brotli 效果最佳
this.adaptiveCompression(content, fileName, 'css');
} else if (fileName.endsWith('.json')) {
// JSON 檔案:Gzip 已足夠
this.adaptiveCompression(content, fileName, 'json');
}
}
});
}
}
]
});
// src/utils/CompressionAnalyzer.ts
interface CompressionResult {
algorithm: string;
originalSize: number;
compressedSize: number;
compressionRatio: number;
compressionTime: number;
decompressionTime: number;
}
export class CompressionAnalyzer {
static async analyzeCompressionEfficiency(
content: string,
algorithms: string[] = ['gzip', 'brotli']
): Promise<CompressionResult[]> {
const results: CompressionResult[] = [];
const originalSize = new Blob([content]).size;
for (const algorithm of algorithms) {
const result = await this.testCompression(content, algorithm, originalSize);
results.push(result);
}
return results.sort((a, b) => b.compressionRatio - a.compressionRatio);
}
private static async testCompression(
content: string,
algorithm: string,
originalSize: number
): Promise<CompressionResult> {
const encoder = new TextEncoder();
const data = encoder.encode(content);
// 壓縮測試
const compressStart = performance.now();
let compressedData: Uint8Array;
switch (algorithm) {
case 'gzip':
compressedData = await this.gzipCompress(data);
break;
case 'brotli':
compressedData = await this.brotliCompress(data);
break;
default:
throw new Error(`Unsupported algorithm: ${algorithm}`);
}
const compressEnd = performance.now();
const compressionTime = compressEnd - compressStart;
// 解壓測試
const decompressStart = performance.now();
await this.decompress(compressedData, algorithm);
const decompressEnd = performance.now();
const decompressionTime = decompressEnd - decompressStart;
const compressedSize = compressedData.length;
const compressionRatio = ((originalSize - compressedSize) / originalSize) * 100;
return {
algorithm,
originalSize,
compressedSize,
compressionRatio: Math.round(compressionRatio * 100) / 100,
compressionTime: Math.round(compressionTime * 100) / 100,
decompressionTime: Math.round(decompressionTime * 100) / 100
};
}
private static async gzipCompress(data: Uint8Array): Promise<Uint8Array> {
const stream = new CompressionStream('gzip');
const writer = stream.writable.getWriter();
writer.write(data);
writer.close();
const chunks: Uint8Array[] = [];
const reader = stream.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
chunks.forEach(chunk => {
result.set(chunk, offset);
offset += chunk.length;
});
return result;
}
private static async brotliCompress(data: Uint8Array): Promise<Uint8Array> {
// 使用 Web Streams API 的 Brotli 壓縮
if ('CompressionStream' in window) {
const stream = new CompressionStream('deflate-raw');
// Brotli 在某些瀏覽器中可能不直接支援,這裡使用 deflate 作為示例
return this.processStream(stream, data);
}
// 降級到 Web Workers 中的 Brotli 實作
return new Promise((resolve, reject) => {
const worker = new Worker('/workers/brotli-worker.js');
worker.postMessage({ data, action: 'compress' });
worker.onmessage = (e) => {
if (e.data.error) {
reject(new Error(e.data.error));
} else {
resolve(e.data.result);
}
worker.terminate();
};
});
}
private static async decompress(
data: Uint8Array,
algorithm: string
): Promise<Uint8Array> {
if ('DecompressionStream' in window) {
const stream = new DecompressionStream(algorithm === 'gzip' ? 'gzip' : 'deflate-raw');
return this.processStream(stream, data);
}
throw new Error('Decompression not supported in this environment');
}
private static async processStream(
stream: CompressionStream | DecompressionStream,
data: Uint8Array
): Promise<Uint8Array> {
const writer = stream.writable.getWriter();
writer.write(data);
writer.close();
const chunks: Uint8Array[] = [];
const reader = stream.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
chunks.forEach(chunk => {
result.set(chunk, offset);
offset += chunk.length;
});
return result;
}
// 即時壓縮效能監控
static startCompressionMonitoring(): void {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.name.includes('compression')) {
console.log(`Compression ${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
});
observer.observe({ entryTypes: ['measure'] });
}
}
}
// src/utils/ContentEncodingNegotiator.ts
export class ContentEncodingNegotiator {
private static readonly COMPRESSION_PREFERENCE = ['br', 'gzip', 'deflate'];
static getBestEncoding(acceptEncoding?: string): string {
if (!acceptEncoding) {
return 'gzip'; // 默認降級
}
const supportedEncodings = acceptEncoding
.split(',')
.map(encoding => encoding.trim().toLowerCase());
// 根據偏好順序選擇最佳編碼
for (const preferred of this.COMPRESSION_PREFERENCE) {
if (supportedEncodings.includes(preferred)) {
return preferred;
}
}
return 'identity'; // 無壓縮
}
// 檢測瀏覽器壓縮支援能力
static detectCompressionSupport(): Promise<{
gzip: boolean;
brotli: boolean;
zstd: boolean;
}> {
return new Promise(async (resolve) => {
const support = {
gzip: false,
brotli: false,
zstd: false
};
// 測試 Gzip 支援
try {
const gzipStream = new CompressionStream('gzip');
support.gzip = !!gzipStream;
} catch (e) {
support.gzip = false;
}
// 測試 Brotli 支援(通過檢測 Accept-Encoding header)
try {
const response = await fetch('/api/compression-test', {
headers: {
'Accept-Encoding': 'br'
}
});
support.brotli = response.headers.get('content-encoding') === 'br';
} catch (e) {
support.brotli = false;
}
// 測試 Zstd 支援(實驗性)
try {
const response = await fetch('/api/compression-test', {
headers: {
'Accept-Encoding': 'zstd'
}
});
support.zstd = response.headers.get('content-encoding') === 'zstd';
} catch (e) {
support.zstd = false;
}
resolve(support);
});
}
// 動態選擇最佳壓縮策略
static async getOptimalCompressionStrategy(
contentType: string,
contentSize: number
): Promise<{
algorithm: string;
level: number;
shouldCompress: boolean;
}> {
// 小檔案不壓縮,避免額外開銷
if (contentSize < 1024) {
return {
algorithm: 'identity',
level: 0,
shouldCompress: false
};
}
const support = await this.detectCompressionSupport();
// 根據內容類型選擇策略
switch (contentType) {
case 'application/javascript':
case 'text/javascript':
return {
algorithm: support.brotli ? 'br' : 'gzip',
level: support.brotli ? 11 : 9,
shouldCompress: true
};
case 'text/css':
return {
algorithm: support.brotli ? 'br' : 'gzip',
level: support.brotli ? 11 : 9,
shouldCompress: true
};
case 'application/json':
return {
algorithm: support.gzip ? 'gzip' : 'deflate',
level: 6, // JSON 壓縮不需要最高級別
shouldCompress: true
};
case 'text/html':
return {
algorithm: support.brotli ? 'br' : 'gzip',
level: support.brotli ? 8 : 6, // HTML 壓縮適中級別即可
shouldCompress: true
};
default:
return {
algorithm: 'gzip',
level: 6,
shouldCompress: contentSize > 2048
};
}
}
}
// src/components/OptimizedImage.tsx
import { useState, useEffect } from 'react';
interface OptimizedImageProps {
src: string;
alt: string;
width?: number;
height?: number;
className?: string;
loading?: 'lazy' | 'eager';
priority?: boolean;
}
export const OptimizedImage: React.FC<OptimizedImageProps> = ({
src,
alt,
width,
height,
className = '',
loading = 'lazy',
priority = false
}) => {
const [imageSrc, setImageSrc] = useState<string>('');
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
// 檢測 WebP 支援
const supportsWebP = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
};
// 智能圖片格式選擇
const getOptimizedSrc = (originalSrc: string): string => {
const isWebPSupported = supportsWebP();
const baseUrl = originalSrc.replace(/\.(jpg|jpeg|png)$/i, '');
if (isWebPSupported) {
return `${baseUrl}.webp`;
}
return originalSrc;
};
// 響應式圖片大小
const getResponsiveSrc = (src: string, width?: number): string => {
if (!width) return src;
const devicePixelRatio = window.devicePixelRatio || 1;
const targetWidth = Math.round(width * devicePixelRatio);
// 假設有圖片 CDN 服務
return `${src}?w=${targetWidth}&q=80&f=auto`;
};
const optimizedSrc = getResponsiveSrc(getOptimizedSrc(src), width);
setImageSrc(optimizedSrc);
// 優先載入重要圖片
if (priority) {
const img = new Image();
img.onload = () => setIsLoaded(true);
img.src = optimizedSrc;
}
}, [src, width, priority]);
return (
<picture>
<source srcSet={`${imageSrc}?format=webp`} type="image/webp" />
<source srcSet={`${imageSrc}?format=avif`} type="image/avif" />
<img
src={imageSrc}
alt={alt}
width={width}
height={height}
className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
loading={loading}
decoding="async"
onLoad={() => setIsLoaded(true)}
/>
</picture>
);
};
// build/critical-css-plugin.ts
import { Plugin } from 'vite';
import { generateCriticalCSS } from '@vitejs/plugin-critical';
export const criticalCSSPlugin = (): Plugin => {
return {
name: 'critical-css',
generateBundle(options, bundle) {
// 提取首屏關鍵 CSS
const criticalCSS = generateCriticalCSS({
inline: true,
minify: true,
extract: true,
dimensions: [
{ width: 375, height: 667 }, // Mobile
{ width: 1200, height: 800 } // Desktop
]
});
// 內聯關鍵 CSS 到 HTML
Object.keys(bundle).forEach(fileName => {
const file = bundle[fileName];
if (file.type === 'asset' && fileName.endsWith('.html')) {
file.source = file.source.toString().replace(
'</head>',
`<style>${criticalCSS}</style></head>`
);
}
});
}
};
};
// src/hooks/usePerformanceOptimization.ts
import { useCallback, useMemo, useRef, useEffect } from 'react';
export const usePerformanceOptimization = () => {
const renderTimeRef = useRef<number>(0);
// 節流 Hook
const useThrottle = <T extends (...args: any[]) => any>(
callback: T,
delay: number
): T => {
const lastRun = useRef(Date.now());
return useCallback(
((...args) => {
if (Date.now() - lastRun.current >= delay) {
callback(...args);
lastRun.current = Date.now();
}
}) as T,
[callback, delay]
);
};
// 防抖 Hook
const useDebounce = <T extends (...args: any[]) => any>(
callback: T,
delay: number
): T => {
const timeoutRef = useRef<NodeJS.Timeout>();
return useCallback(
((...args) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => callback(...args), delay);
}) as T,
[callback, delay]
);
};
// 虛擬化列表 Hook
const useVirtualList = (
items: any[],
itemHeight: number,
containerHeight: number
) => {
const [scrollTop, setScrollTop] = useState(0);
const visibleItems = useMemo(() => {
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
return items.slice(startIndex, endIndex).map((item, index) => ({
...item,
index: startIndex + index,
top: (startIndex + index) * itemHeight
}));
}, [items, itemHeight, containerHeight, scrollTop]);
return {
visibleItems,
totalHeight: items.length * itemHeight,
setScrollTop
};
};
// 效能監控
const measureRenderTime = (componentName: string) => {
useEffect(() => {
const startTime = performance.now();
renderTimeRef.current = startTime;
return () => {
const endTime = performance.now();
const renderTime = endTime - startTime;
if (renderTime > 16) { // 超過一幀的時間
console.warn(`${componentName} render took ${renderTime.toFixed(2)}ms`);
}
};
});
};
return {
useThrottle,
useDebounce,
useVirtualList,
measureRenderTime
};
};
// src/utils/WebVitalsMonitor.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB, Metric } from 'web-vitals';
export class WebVitalsMonitor {
private static metrics: Map<string, number> = new Map();
static initialize(): void {
// Largest Contentful Paint
getLCP((metric: Metric) => {
this.reportMetric('LCP', metric.value);
this.optimizeLCP(metric.value);
});
// First Input Delay
getFID((metric: Metric) => {
this.reportMetric('FID', metric.value);
this.optimizeFID(metric.value);
});
// Cumulative Layout Shift
getCLS((metric: Metric) => {
this.reportMetric('CLS', metric.value);
this.optimizeCLS(metric.value);
});
// First Contentful Paint
getFCP((metric: Metric) => {
this.reportMetric('FCP', metric.value);
});
// Time to First Byte
getTTFB((metric: Metric) => {
this.reportMetric('TTFB', metric.value);
});
}
private static reportMetric(name: string, value: number): void {
this.metrics.set(name, value);
// 發送到分析服務
if (typeof gtag !== 'undefined') {
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(value),
non_interaction: true,
});
}
// 控制台警告
const thresholds = {
LCP: 2500,
FID: 100,
CLS: 0.1,
FCP: 1800,
TTFB: 800
};
if (value > thresholds[name as keyof typeof thresholds]) {
console.warn(`${name} is ${value}ms (threshold: ${thresholds[name as keyof typeof thresholds]}ms)`);
}
}
private static optimizeLCP(value: number): void {
if (value > 2500) {
// 動態載入非關鍵資源
this.deferNonCriticalResources();
// 優化圖片載入
this.optimizeImageLoading();
}
}
private static optimizeFID(value: number): void {
if (value > 100) {
// 分割長時間執行的任務
this.scheduleTaskChunking();
}
}
private static optimizeCLS(value: number): void {
if (value > 0.1) {
// 為動態內容預留空間
this.preventLayoutShifts();
}
}
private static deferNonCriticalResources(): void {
// 延遲載入第三方腳本
const scripts = document.querySelectorAll('script[data-defer]');
scripts.forEach(script => {
setTimeout(() => {
const newScript = document.createElement('script');
newScript.src = script.getAttribute('data-defer')!;
newScript.async = true;
document.head.appendChild(newScript);
}, 3000);
});
}
private static optimizeImageLoading(): void {
// 實施漸進式圖片載入
const images = document.querySelectorAll('img:not([loading])');
images.forEach(img => {
img.setAttribute('loading', 'lazy');
img.setAttribute('decoding', 'async');
});
}
private static scheduleTaskChunking(): void {
// 使用 scheduler API 分割任務
if ('scheduler' in window && 'postTask' in window.scheduler) {
// 現代瀏覽器使用 Scheduler API
window.scheduler.postTask(() => {
// 執行低優先級任務
}, { priority: 'background' });
} else {
// 降級到 requestIdleCallback
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// 執行低優先級任務
});
}
}
}
private static preventLayoutShifts(): void {
// 為動態內容設置 aspect-ratio
const dynamicContainers = document.querySelectorAll('[data-dynamic-content]');
dynamicContainers.forEach(container => {
const element = container as HTMLElement;
if (!element.style.aspectRatio) {
element.style.aspectRatio = '16/9'; // 預設比例
}
});
}
static getMetrics(): Map<string, number> {
return new Map(this.metrics);
}
}
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ResourcePreloader } from './utils/ResourcePreloader';
import { ConnectionOptimizer } from './utils/ConnectionOptimizer';
import { WebVitalsMonitor } from './utils/WebVitalsMonitor';
// 效能優化初始化
const initializePerformanceOptimizations = () => {
// 連線優化
ConnectionOptimizer.initializeConnections();
ConnectionOptimizer.preloadModules();
// 資源預載入
const preloader = ResourcePreloader.getInstance();
preloader.preloadCriticalResources();
preloader.lazyLoadImages();
// Web Vitals 監控
WebVitalsMonitor.initialize();
};
// 立即執行效能優化
initializePerformanceOptimizations();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
今天我們實現了全面的前端效能優化策略:
這些優化措施將顯著提升應用載入速度與互動響應性,為使用者提供流暢的 SaaS 體驗。
明天我們將深入後端效能優化,包括資料庫查詢優化、快取策略、以及伺服器端效能調校。