代码分割与懒加载
代码分割与懒加载是优化首屏加载速度的核心技术,通过按需加载减少初始资源体积。
核心概念
代码分割
将代码拆分为多个chunk,按需加载而非一次性加载全部代码。
懒加载
延迟加载非关键资源,在需要时才加载对应模块。
收益分析
- 减少首屏加载时间:只加载必要代码
- 降低初始带宽消耗:按需请求
- 提高缓存命中率:小块更容易命中缓存
代码分割策略
入口分割
JavaScript
// webpack.config.js
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js',
vendor: ['lodash', 'axios']
},
output: {
filename: '[name].[contenthash].js'
}
};
// 生成多个独立入口chunk
// main.js、admin.js、vendor.js
动态导入分割
JavaScript
// 使用import()动态导入
async function loadModule() {
const module = await import('./heavyModule.js');
module.doSomething();
}
// 条件加载
if (condition) {
import('./feature.js').then(module => {
module.init();
});
}
// 按路由分割
const routes = {
home: () => import('./pages/Home.js'),
admin: () => import('./pages/Admin.js'),
dashboard: () => import('./pages/Dashboard.js')
};
SplitChunksPlugin
JavaScript
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有chunk分割
cacheGroups: {
// 第三方库单独打包
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 20
},
// 公共模块提取
commons: {
minChunks: 2, // 被引用2次以上提取
name: 'commons',
chunks: 'all',
priority: 10
},
// UI组件库
ui: {
test: /[\\/]src[\\/]components[\\/]/,
name: 'ui',
chunks: 'all',
priority: 15
}
}
}
}
};
懒加载实现
组件懒加载(React)
JavaScript
import React, { lazy, Suspense } from 'react';
// lazy声明懒加载组件
const Dashboard = lazy(() => import('./Dashboard'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
);
}
// 条件渲染懒加载
function ConditionalComponent({ isAdmin }) {
if (!isAdmin) return null;
const Admin = lazy(() => import('./Admin'));
return (
<Suspense fallback={<div>Loading admin...</div>}>
<Admin />
</Suspense>
);
}
路由懒加载
JavaScript
// React Router
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const routes = [
{ path: '/', element: <Suspense fallback={<Loading />}><Home /></Suspense> },
{ path: '/about', element: <Suspense fallback={<Loading />}><About /></Suspense> }
];
// Vue Router
const routes = [
{
path: '/admin',
component: () => import('./views/Admin.vue')
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
];
图片懒加载
JavaScript
// 原生图片懒加载
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />
// IntersectionObserver实现
const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
}, { rootMargin: '100px' });
images.forEach(img => observer.observe(img));
// 组件封装
function LazyImage({ src, placeholder, alt }) {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
imgRef.current.src = src;
observer.disconnect();
}
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, [src]);
return <img ref={imgRef} src={loaded ? src : placeholder} alt={alt} onLoad={() => setLoaded(true)} />;
}
预加载策略
JavaScript
// prefetch:空闲时预加载
import(/* webpackPrefetch: true */ './nextPage.js');
// preload:并行预加载(当前路由必需)
import(/* webpackPreload: true */ './criticalModule.js');
// 手动预加载
function prefetchPage(path) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = `/static/js/${path}.js`;
document.head.appendChild(link);
}
// 鼠标悬停预加载
<Link
onMouseEnter={() => prefetchPage('/admin')}
href="/admin"
>
Admin Panel
</Link>
Vite代码分割
Vite配置
JavaScript
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['lodash', 'axios'],
ui: ['react', 'react-dom']
}
}
}
}
});
// 动态manualChunks函数
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('lodash')) return 'lodash';
if (id.includes('react')) return 'react';
return 'vendor';
}
}
Vite懒加载
JavaScript
// 与Webpack语法一致
const module = await import('./module.js');
// 组件懒加载
const LazyComponent = defineAsyncComponent(() =>
import('./components/Lazy.vue')
);
按需加载优化
骨架屏
JavaScript
// 加载占位
function LoadingSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
);
}
// 使用
<Suspense fallback={<LoadingSkeleton />}>
<LazyComponent />
</Suspense>
加载状态管理
JavaScript
function LazyLoader({ importFn, children }) {
const [Component, setComponent] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
importFn()
.then(module => setComponent(() => module.default))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [importFn]);
if (error) return <ErrorComponent error={error} />;
if (loading) return <LoadingSkeleton />;
return children(Component);
}
chunk分析
JavaScript
// Webpack Bundle Analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin.BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
]
};
// Vite分析
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [visualizer({ open: false })]
});
最佳实践
分割策略
JavaScript
// ✅ 好的分割策略
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000, // 最小20KB才分割
maxSize: 244000, // 最大244KB
cacheGroups: {
// 核心库单独打包
react: { test: /react/, name: 'react' },
// 工具库单独打包
lodash: { test: /lodash/, name: 'lodash' }
}
}
}
// ❌ 过度分割
// 每个小模块单独打包 → 请求过多
加载优先级
text
核心资源 → 同步加载
首屏必需 → preload
次要页面 → prefetch
非必需 → 懒加载(用户触发)
预加载要适度,prefetch过多会抢占带宽影响首屏。
要点总结
- 入口分割:多入口应用天然分割为多个chunk
- 动态导入:import()实现按需加载
- SplitChunks:提取公共模块、第三方库
- React.lazy:配合Suspense实现组件懒加载
- prefetch/preload:智能预加载提升体验
- 分析工具:Bundle Analyzer定位分割问题
存放路径:articles/JS/专家/高级性能分析/代码分割与懒加载.md
📝 发现内容有误?点击此处直接编辑