懒加载:从图片到组件的渐进式加载
你有没有过这种体验?打开一个页面,滚动到中间发现图片还在转圈,或者点个按钮要等2秒才弹出组件?懒加载就是解决这种“加载冗余”的关键——只加载用户当前需要的内容。

图片懒加载:告别“全部加载”的笨办法
图片是页面体积的大头,传统的<img src="xxx">
会让浏览器一进入页面就下载所有图片,哪怕用户根本没滚动到。现在有更聪明的方式:
1. 原生方案:loading="lazy"
直接给img标签加loading="lazy"
,浏览器会自动判断图片是否进入视口再加载。优点是零代码,缺点是兼容性有限(IE不支持,Chrome 76+/Firefox 75+支持)。
示例:<img src="large-image.jpg" loading="lazy" alt="懒加载图片">
-
现代方案:Intersection Observer
这是浏览器原生API,比监听scroll
事件高效10倍(不会触发重排)。原理是监听元素与视口的交集状态,当元素进入视口时加载图片。
代码示例:const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // 把data-src赋值给src observer.unobserve(img); // 停止观察,避免重复触发 } }); }, { rootMargin: '200px 0px', // 提前200px开始加载 threshold: 0.1 // 元素10%进入视口时触发 }); // 给所有懒加载图片加监听 document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
技巧:
rootMargin
可以设置负数(比如-50px
),避免图片刚进入视口就开始加载导致闪烁;threshold
设为0可以让元素刚接触视口就加载。 -
组件懒加载:React/Vue的动态导入
除了图片,组件也能懒加载。比如React用React.lazy
+Suspense
,Vue用defineAsyncComponent
。
React示例:import React, { Suspense } from 'react'; // 动态导入组件,Webpack会自动分割代码 const LazyComment = React.lazy(() => import('./CommentComponent')); function App() { return ( <div> <h1>首页内容</h1> {/* Suspense显示加载状态 */} <Suspense fallback={<div>加载中...</div>}> <LazyComment /> </Suspense> </div> ); }
注意:
React.lazy
只支持默认导出,如果组件是命名导出,需要改成() => import('./CommentComponent').then(mod => ({ default: mod.NamedComponent }))
。
懒加载方案对比表
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
原生loading="lazy" |
零代码、浏览器原生支持 | 兼容性有限 | 简单图片懒加载 |
Intersection Observer | 高效、灵活 | 需要写少量代码 | 复杂图片/组件懒加载 |
React.lazy/Vue Async | 框架原生支持、无缝集成 | 依赖框架、需要Suspense | 单页应用(SPA)组件懒加载 |
代码分割:把bundle拆成用户需要的碎片
你有没有见过打包后的main.js
超过2MB?代码分割就是把大bundle拆成多个小chunk,用户只下载当前页面需要的代码。核心原理是动态导入(import()
语法),浏览器会把动态导入的模块当作单独的请求。
工具配置:Webpack/Vite/Rollup怎么玩?
-
Webpack:splitChunks与动态导入
Webpack的splitChunks
插件是代码分割的核心,主要配置cacheGroups
(缓存组):// webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all', // 分割所有类型的chunk(同步/异步) cacheGroups: { vendor: { // 提取第三方库(比如React、Vue) test: /[\/]node_modules[\/]/, name: 'vendor', // 生成的chunk名 priority: 10, // 优先级(数值越大越先执行) reuseExistingChunk: true // 复用已有的chunk }, common: { // 提取公共组件 minSize: 0, // 最小体积(0表示不管多大都提取) minChunks: 2, // 被引用2次以上才提取 priority: 5, reuseExistingChunk: true } } } } };
技巧:
priority
很重要,比如vendor
的优先级要高于common
,避免第三方库被分到common
里。 -
Vite:天然支持动态导入
Vite默认支持代码分割,不需要额外配置。动态导入的模块会自动生成[name].[hash].js
的chunk。如果要自定义,可以改build.rollupOptions
:// vite.config.js import { defineConfig } from 'vite'; export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { 'react-vendor': ['react', 'react-dom'], // 手动分割React相关库 'utils': ['lodash', 'axios'] // 手动分割工具库 } } } } });
-
Rollup:output.preserveModules
Rollup适合库打包,preserveModules
选项会保留模块结构,避免把所有代码合并成一个文件:// rollup.config.js export default { input: 'src/index.js', output: { dir: 'dist', format: 'es', preserveModules: true // 保留模块结构 } };
联动技巧:懒加载与代码分割的协同策略
懒加载和代码分割不是孤立的——懒加载的组件/图片,本质上是通过代码分割生成的chunk。两者协同的关键是“按需加载”:
1. 路由级代码分割:比如SPA的路由,用户访问/about
时才加载about.js
。React Router示例:
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense } from 'react';
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
function App() {
return (
<Router>
<Suspense fallback={<div>加载中...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
}
2. 交互触发的代码分割:比如点击“查看更多”按钮时,加载评论组件:
function MoreComments() {
const [comments, setComments] = useState(null);
const loadComments = async () => {
const { default: fetchComments } = await import('./fetchComments');
const data = await fetchComments();
setComments(data);
};
return (
<div>
<button onClick={loadComments}>查看更多评论</button>
{comments && <CommentList data={comments} />}
</div>
);
}
避坑指南:别让优化变成“负优化”
-
过度懒加载:交互延迟
比如把“提交按钮”的组件懒加载,用户点击时才开始下载,会导致按钮点击后延迟1-2秒才有反应。解决方案:关键交互组件(比如按钮、表单)不要懒加载。 -
代码分割太细:请求爆炸
比如把每个组件都分成单独的chunk,会导致页面加载时发送100个小请求(每个1-2KB),反而比一个大请求慢(因为HTTP/2的多路复用也扛不住太多请求)。解决方案:控制chunk数量,比如用splitChunks
的maxSize
限制每个chunk的大小(比如maxSize: 200000
,即200KB)。 -
懒加载没做 fallback:用户懵了
比如图片懒加载时,没设置占位符(比如灰色背景),用户会看到空白;组件懒加载时,没加Suspense
,会报错。解决方案:图片加alt
属性和占位符样式,组件加Suspense
的fallback
。 -
忽略SEO:懒加载内容不被爬虫抓取
比如单页应用的路由懒加载,爬虫可能爬不到懒加载的内容(因为爬虫不执行JS)。解决方案:用服务端渲染(SSR)或者预渲染(Prerendering)。
验证效果:怎么知道优化有用?
用Chrome DevTools的Performance面板和Network面板:
1. Performance面板:记录页面加载过程,看“Loading”阶段的时间有没有缩短,“Scripting”阶段的时间有没有减少。
2. Network面板:看请求的数量和大小,比如懒加载的图片/组件是不是在滚动到的时候才发送请求,代码分割后的chunk是不是只有当前页面需要的。
3. Lighthouse:运行Lighthouse测试,看“Performance”评分有没有提升(比如从60分涨到90分)。
最后问你个问题:你之前做过的懒加载/代码分割,有没有踩过上面的坑?欢迎在评论区分享你的经历~
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/301