第10章 构建时优化与加载策略:工程化性能保障
10.1 Tree Shaking与副作用消除
Tree Shaking是构建时优化的核心技术,它可以消除未使用的代码,减少包体积。
10.1.1 ES Module的静态分析
Tree Shaking依赖于ES Module的静态结构,这使得构建工具可以在编译时确定模块的依赖关系。
Webpack vs Rollup vs esbuild vs Turbopack的DCE算法差异与性能基准
TEXT1Tree Shaking实现对比: 2 3| 工具 | DCE算法 | 分析深度 | 副作用检测 | 构建速度 | 4|-----|---------|---------|-----------|---------| 5| Webpack | 基于AST | 中等 | 需要标记 | 较慢 | 6| Rollup | 基于AST | 深 | 自动检测 | 中等 | 7| esbuild | 基于AST | 浅 | 有限 | 极快 | 8| Turbopack | 基于AST | 深 | 自动检测 | 快 | 9| SWC | 基于AST | 中等 | 需要标记 | 快 | 10 11副作用检测方式: 12- 自动检测:分析代码确定是否有副作用 13- 需要标记:依赖package.json中的sideEffects字段 14- 有限:仅支持基本的副作用检测
TYPESCRIPT1// sideEffects配置示例 2// package.json 3{ 4 "name": "my-library", 5 "sideEffects": [ 6 "*.css", 7 "*.scss", 8 "./src/polyfill.js" 9 ] 10} 11 12// 无副作用的模块标记 13// 在文件顶部添加注释 14/*#__PURE__*/ 15export function pureFunction() { 16 return 'pure'; 17} 18 19// 有副作用的代码 20console.log('side effect'); // 这行代码不会被tree shake 21 22// 纯函数调用标记 23const result = /*#__PURE__*/pureFunction(); // 如果result未使用,会被tree shake
10.1.2 React的__PURE__注解
React使用__PURE__注解来帮助构建工具识别纯函数调用。
UglifyJS/Terser的函数标记与副作用推断
TYPESCRIPT1// React JSX转换后的代码 2// 原始JSX 3const element = <div className="app">Hello</div>; 4 5// 转换后(带PURE标记) 6const element = /*#__PURE__*/ React.createElement( 7 'div', 8 { className: 'app' }, 9 'Hello' 10); 11 12// 如果element未被使用,整个调用会被tree shake 13 14// babel-plugin-transform-react-pure-annotations源码原理 15/* 16该Babel插件会自动为以下调用添加PURE注释: 171. React.createElement 182. React.cloneElement 193. React.Children.map/foreach/etc 204. 自定义组件调用(如果组件被标记为纯函数) 21*/ 22 23// 配置示例 24// babel.config.js 25module.exports = { 26 plugins: [ 27 'babel-plugin-transform-react-pure-annotations', 28 ], 29};
10.1.3 组件库的Tree Shaking优化
组件库需要特别注意Tree Shaking配置,以确保用户只打包使用的组件。
Babel Runtime Helpers的按需引入与lodash-es的对比分析
TYPESCRIPT1// 问题:全量引入 2import _ from 'lodash'; 3const result = _.map([1, 2, 3], x => x * 2); // 打包整个lodash 4 5// 优化1:按需引入 6import map from 'lodash/map'; 7const result = map([1, 2, 3], x => x * 2); // 只打包map函数 8 9// 优化2:使用lodash-es 10import { map } from 'lodash-es'; 11const result = map([1, 2, 3], x => x * 2); // Tree shaking友好 12 13// 优化3:使用babel-plugin-lodash 14// 自动转换 import { map } from 'lodash' 为按需引入 15 16// Barrel Files的反模式与优化 17// 问题:barrel文件阻碍tree shaking 18// components/index.ts 19export * from './Button'; 20export * from './Input'; 21export * from './Select'; 22export * from './Table'; // 即使只使用Button,也会分析所有导出 23 24// 优化:直接导入 25// 不要 import { Button } from './components'; 26// 而是 import { Button } from './components/Button'; 27 28// 组件库优化配置 29// package.json 30{ 31 "name": "my-ui-library", 32 "sideEffects": [ 33 "*.css", 34 "*.less", 35 "es/**/style/*", 36 "lib/**/style/*", 37 "*.scss" 38 ], 39 "module": "es/index.js", // ES Module入口 40 "main": "lib/index.js", // CommonJS入口 41}
10.2 代码分割与模块联邦
代码分割允许将应用拆分为多个小块,按需加载,减少初始加载时间。
10.2.1 React.lazy与Suspense
React.lazy和Suspense提供了声明式的代码分割方案。
动态导入(import())的边界确定与错误边界(Error Boundary)的级联设计
TYPESCRIPT1// 基础用法 2const LazyComponent = React.lazy(() => import('./HeavyComponent')); 3 4function App() { 5 return ( 6 <Suspense fallback={<Loading />}> 7 <LazyComponent /> 8 </Suspense> 9 ); 10} 11 12// 带错误边界 13class ErrorBoundary extends React.Component< 14 { fallback: React.ReactNode; children: React.ReactNode }, 15 { hasError: boolean } 16> { 17 state = { hasError: false }; 18 19 static getDerivedStateFromError() { 20 return { hasError: true }; 21 } 22 23 render() { 24 if (this.state.hasError) { 25 return this.props.fallback; 26 } 27 return this.props.children; 28 } 29} 30 31function AppWithErrorBoundary() { 32 return ( 33 <ErrorBoundary fallback={<ErrorMessage />}> 34 <Suspense fallback={<Loading />}> 35 <LazyComponent /> 36 </Suspense> 37 </ErrorBoundary> 38 ); 39} 40 41// 预加载策略 42const LazyComponentWithPreload = Object.assign( 43 React.lazy(() => import('./HeavyComponent')), 44 { 45 preload: () => import('./HeavyComponent'), 46 } 47); 48 49// 路由分割 50const routes = [ 51 { 52 path: '/dashboard', 53 component: React.lazy(() => import('./pages/Dashboard')), 54 }, 55 { 56 path: '/settings', 57 component: React.lazy(() => import('./pages/Settings')), 58 }, 59]; 60 61// 预加载当前路由的下一个可能路由 62function useRoutePreload(currentPath: string) { 63 useEffect(() => { 64 const nextRoutes = getLikelyNextRoutes(currentPath); 65 nextRoutes.forEach(route => { 66 route.component.preload?.(); 67 }); 68 }, [currentPath]); 69}
10.2.2 Module Federation的依赖共享
Module Federation允许在运行时共享模块,是微前端架构的核心技术。
Singleton与StrictVersion的冲突解决算法与运行时版本对齐
TYPESCRIPT1// Module Federation配置 2// webpack.config.js 3const { ModuleFederationPlugin } = require('webpack').container; 4 5module.exports = { 6 plugins: [ 7 new ModuleFederationPlugin({ 8 name: 'host', 9 remotes: { 10 app1: 'app1@http://localhost:3001/remoteEntry.js', 11 app2: 'app2@http://localhost:3002/remoteEntry.js', 12 }, 13 shared: { 14 react: { 15 singleton: true, // 强制单例 16 requiredVersion: '^18.0.0', 17 strictVersion: true, // 版本必须匹配 18 }, 19 'react-dom': { 20 singleton: true, 21 requiredVersion: '^18.0.0', 22 }, 23 lodash: { 24 eager: false, // 不预加载 25 }, 26 }, 27 }), 28 ], 29}; 30 31// 共享依赖的TS类型共享与类型合并 32declare module 'app1/Component' { 33 import { ComponentType } from 'react'; 34 const Component: ComponentType<{ 35 title: string; 36 onAction: () => void; 37 }>; 38 export default Component; 39} 40 41// 使用远程组件 42const RemoteComponent = React.lazy(() => import('app1/Component')); 43 44function HostApp() { 45 return ( 46 <Suspense fallback={<Loading />}> 47 <RemoteComponent 48 title="From Host" 49 onAction={() => console.log('action')} 50 /> 51 </Suspense> 52 ); 53}
10.2.3 微前端架构的CSS隔离与JS沙箱
微前端需要解决样式和JavaScript的隔离问题。
Shadow DOM的样式封装与Qiankun的ProxySandbox原理
TYPESCRIPT1// Shadow DOM样式隔离 2function ShadowComponent({ children }: { children: React.ReactNode }) { 3 const hostRef = useRef<HTMLDivElement>(null); 4 const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null); 5 6 useEffect(() => { 7 if (hostRef.current && !shadowRoot) { 8 const shadow = hostRef.current.attachShadow({ mode: 'open' }); 9 setShadowRoot(shadow); 10 } 11 }, []); 12 13 return ( 14 <div ref={hostRef}> 15 {shadowRoot && createPortal(children, shadowRoot)} 16 </div> 17 ); 18} 19 20// Qiankun的ProxySandbox 21/* 22ProxySandbox原理: 231. 创建一个假的window对象 242. 使用Proxy拦截所有属性访问 253. 读写操作都在假window上进行 264. 卸载时直接丢弃假window 27 28优点: 29- 完全隔离 30- 性能较好 31- 支持多实例 32 33缺点: 34- 一些全局API可能无法完全隔离 35- 需要处理一些边界情况 36*/ 37 38// 快照沙箱(单例模式) 39class SnapshotSandbox { 40 private windowSnapshot: Record<string, any> = {}; 41 private modifyPropsMap: Record<string, any> = {}; 42 43 active() { 44 // 保存当前window状态 45 this.windowSnapshot = {}; 46 for (const key in window) { 47 this.windowSnapshot[key] = (window as any)[key]; 48 } 49 50 // 恢复之前的修改 51 Object.keys(this.modifyPropsMap).forEach(key => { 52 (window as any)[key] = this.modifyPropsMap[key]; 53 }); 54 } 55 56 inactive() { 57 // 记录修改 58 this.modifyPropsMap = {}; 59 for (const key in window) { 60 if ((window as any)[key] !== this.windowSnapshot[key]) { 61 this.modifyPropsMap[key] = (window as any)[key]; 62 (window as any)[key] = this.windowSnapshot[key]; 63 } 64 } 65 } 66}
10.3 资源加载优先级与关键渲染路径
优化资源加载优先级可以显著提升首屏加载性能。
10.3.1 关键CSS内联与异步加载
关键CSS内联可以减少首次渲染的阻塞时间。
Critical CSS提取与剩余CSS的非阻塞加载策略
TYPESCRIPT1// Critical CSS提取工具配置 2// critical.config.js 3const critical = require('critical'); 4 5critical.generate({ 6 base: 'dist/', 7 src: 'index.html', 8 target: { 9 css: 'critical.css', 10 html: 'index.critical.html', 11 }, 12 width: 1300, 13 height: 900, 14}); 15 16// HTML中的Critical CSS内联 17/* 18<!DOCTYPE html> 19<html> 20<head> 21 <style> 22 /* 内联的关键CSS */ 23 body { margin: 0; font-family: sans-serif; } 24 header { background: #333; color: white; } 25 /* ... */ 26 </style> 27 28 <!-- 异步加载剩余CSS --> 29 <link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> 30 <noscript><link rel="stylesheet" href="/styles/main.css"></noscript> 31</head> 32<body>...</body> 33</html> 34*/ 35 36// React中的Critical CSS 37import { renderToString } from 'react-dom/server'; 38import { ServerStyleSheet } from 'styled-components'; 39 40function renderWithStyles(Component: React.ComponentType) { 41 const sheet = new ServerStyleSheet(); 42 43 try { 44 const html = renderToString(sheet.collectStyles(<Component />)); 45 const styleTags = sheet.getStyleTags(); 46 47 return { html, styleTags }; 48 } finally { 49 sheet.seal(); 50 } 51}
10.3.2 图片懒加载与LCP优化
图片通常是页面中最大的资源,优化图片加载对LCP指标至关重要。
Intersection Observer API的阈值计算与模糊占位技术
TYPESCRIPT1// 图片懒加载Hook 2function useLazyImage( 3 src: string, 4 options?: IntersectionObserverInit 5) { 6 const [isLoaded, setIsLoaded] = useState(false); 7 const [isInView, setIsInView] = useState(false); 8 const imgRef = useRef<HTMLImageElement>(null); 9 10 useEffect(() => { 11 const observer = new IntersectionObserver( 12 ([entry]) => { 13 if (entry.isIntersecting) { 14 setIsInView(true); 15 observer.disconnect(); 16 } 17 }, 18 { 19 rootMargin: '50px', // 提前50px开始加载 20 threshold: 0, 21 ...options, 22 } 23 ); 24 25 if (imgRef.current) { 26 observer.observe(imgRef.current); 27 } 28 29 return () => observer.disconnect(); 30 }, [options]); 31 32 const handleLoad = () => { 33 setIsLoaded(true); 34 }; 35 36 return { 37 ref: imgRef, 38 src: isInView ? src : undefined, 39 isLoaded, 40 isInView, 41 onLoad: handleLoad, 42 }; 43} 44 45// 渐进式图片加载组件 46function ProgressiveImage({ 47 src, 48 placeholder, 49 alt, 50}: { 51 src: string; 52 placeholder: string; // 低分辨率占位图 53 alt: string; 54}) { 55 const { ref, src: lazySrc, isLoaded } = useLazyImage(src); 56 57 return ( 58 <div ref={ref} className="image-container"> 59 {/* 占位图 */} 60 <img 61 src={placeholder} 62 alt="" 63 className="placeholder" 64 style={{ filter: isLoaded ? 'blur(0)' : 'blur(10px)' }} 65 /> 66 67 {/* 实际图片 */} 68 {lazySrc && ( 69 <img 70 src={lazySrc} 71 alt={alt} 72 className="actual-image" 73 style={{ opacity: isLoaded ? 1 : 0 }} 74 onLoad={() => {}} 75 /> 76 )} 77 </div> 78 ); 79} 80 81// WebP/AVIF格式适配 82function ResponsiveImage({ 83 src, 84 alt, 85 width, 86 height, 87}: { 88 src: string; 89 alt: string; 90 width: number; 91 height: number; 92}) { 93 const baseName = src.replace(/\.[^/.]+$/, ''); 94 95 return ( 96 <picture> 97 {/* AVIF - 最佳压缩 */} 98 <source 99 srcSet={`${baseName}.avif`} 100 type="image/avif" 101 /> 102 {/* WebP - 良好支持 */} 103 <source 104 srcSet={`${baseName}.webp`} 105 type="image/webp" 106 /> 107 {/* JPEG fallback */} 108 <img 109 src={src} 110 alt={alt} 111 width={width} 112 height={height} 113 loading="lazy" 114 /> 115 </picture> 116 ); 117}
10.3.3 字体加载的FOIT/FOUT策略
字体加载策略影响文本的可见性和页面体验。
Font Display API与React组件的字体闪屏消除
TYPESCRIPT1// 字体加载Hook 2function useFontLoader(fontFamily: string, fontUrl: string) { 3 const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading'); 4 5 useEffect(() => { 6 const font = new FontFace(fontFamily, `url(${fontUrl})`); 7 8 font.load() 9 .then(() => { 10 document.fonts.add(font); 11 setStatus('loaded'); 12 }) 13 .catch(() => { 14 setStatus('error'); 15 }); 16 }, [fontFamily, fontUrl]); 17 18 return status; 19} 20 21// useFont Hook封装 22function useFont(fontConfig: { family: string; url: string }[]) { 23 const [loaded, setLoaded] = useState(false); 24 25 useEffect(() => { 26 const promises = fontConfig.map(({ family, url }) => { 27 const font = new FontFace(family, `url(${url})`); 28 return font.load().then(() => { 29 document.fonts.add(font); 30 }); 31 }); 32 33 Promise.all(promises) 34 .then(() => setLoaded(true)) 35 .catch(() => setLoaded(true)); // 即使失败也继续 36 }, [fontConfig]); 37 38 return loaded; 39} 40 41// 字体加载策略组件 42function FontProvider({ 43 children, 44 fonts, 45}: { 46 children: React.ReactNode; 47 fonts: { family: string; url: string }[]; 48}) { 49 const fontsLoaded = useFont(fonts); 50 51 return ( 52 <div 53 style={{ 54 fontFamily: fontsLoaded 55 ? fonts.map(f => f.family).join(', ') 56 : 'system-ui, sans-serif', 57 opacity: fontsLoaded ? 1 : 0.9, 58 transition: 'opacity 0.3s', 59 }} 60 > 61 {children} 62 </div> 63 ); 64} 65 66// CSS font-display策略 67/* 68font-display取值: 69- auto: 浏览器默认行为 70- block: 短暂不可见(FOIT),然后显示 71- swap: 立即显示后备字体(FOUT),加载后替换 72- fallback: 短暂block,然后swap 73- optional: 类似fallback,但可能不加载字体 74 75推荐策略: 76- 关键字体:fallback 77- 非关键字体:optional 78- 图标字体:block 79*/ 80 81// @font-face配置 82/* 83@font-face { 84 font-family: 'CustomFont'; 85 src: url('/fonts/custom.woff2') format('woff2'); 86 font-weight: 400; 87 font-style: normal; 88 font-display: fallback; // 推荐策略 89} 90*/
本章探讨了构建时优化和加载策略,从Tree Shaking到代码分割,从资源优先级到关键渲染路径优化。这些工程化手段是保障应用性能的基础。
在下一部分中,我们将探讨状态管理架构和数据流工程化,建立可扩展的应用状态管理方案。