Webpack Externals 配置实战:优化打包体积与加载性能
Externals 是啥?先看一段让我省了 200KB 的配置
上周我重构一个老项目,打包体积大得离谱,光是 React 和 Lodash 就占了快 300KB。产品经理还在催加载速度,我一拍脑袋:这不就是 externals 的用武之地吗?
直接上核心配置,Webpack 里加这么几行:
// webpack.config.js
module.exports = {
// ...
externals: {
react: 'React',
'react-dom': 'ReactDOM',
lodash: '_'
}
}
然后在 HTML 里用 CDN 引入这些库:
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/lodash@4.17.21/lodash.min.js"></script>
打包完一看,bundle 体积直接少了 200KB+,首屏加载快了近 1 秒。亲测有效,尤其是对那些基础库几乎不变的项目,建议直接用这种方式。
这个场景最好用:微前端 or 多页面共享依赖
我之前做过一个微前端项目,主应用和子应用都用 React,但各自打包,结果用户加载时重复下载 React。后来统一用 externals 把 React 提到主应用的 HTML 里,子应用打包时排除掉,体验立马顺滑了。
还有多页面应用(MPA),比如后台系统有几十个页面,每个页面都 import 了 Axios 和 Vue。这时候把它们配成 externals,所有页面共用一份全局变量,省带宽又省缓存空间。
配置也简单,除了上面的对象写法,还可以用函数形式做更精细的控制:
// webpack.config.js
module.exports = {
externals: (context, request, callback) => {
// 所有 @jztheme/ 开头的包都走 externals
if (/^@jztheme//.test(request)) {
return callback(null, window.JZTheme.${request.split('/')[1]});
}
callback();
}
}
这样你就能把内部组件库也外置,前提是这些库确实被全局挂载了。不过这种高级用法要小心,别把不该外置的也干掉了。
踩坑提醒:这三点一定注意
externals 虽好,但坑也不少。我踩过好几次,这里总结三个最痛的点:
- CDN 版本和本地开发版本必须一致。有一次我本地用 React 18.2,CDN 引了 18.1,结果某个 Hook 行为不一致,调试了半天才发现是版本问题。现在我会在 package.json 里锁死版本,CDN 链接也手动指定具体版本号。
- 开发环境和生产环境行为不同。开发时如果没配 externals,一切正常;一上线发现某些模块 undefined。所以建议从项目初期就统一配置,或者用环境变量控制:
// webpack.config.js
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
externals: isProd ? {
react: 'React',
'react-dom': 'ReactDOM'
} : {}
}
- Tree Shaking 会失效。一旦你把整个 Lodash 配成 externals,就算你只用了
_.debounce,也会把整个 Lodash 库加载进来。所以对于 Lodash 这种工具库,我后来改用按需引入 + CDN 单独文件,或者干脆不外置。React/Vue 这种整体性很强的框架才适合 externals。
高级技巧:动态加载 + externals 结合
有些项目不是所有页面都需要 React,比如营销页用纯静态,后台用 React。这时候可以结合动态加载,只在需要时插入 CDN 脚本。
我写了一个小工具函数,配合 externals 使用:
// utils/loadExternal.js
function loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(script[src="${src}"])) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
export async function ensureReact() {
if (window.React && window.ReactDOM) return;
await loadScript('https://unpkg.com/react@18/umd/react.production.min.js');
await loadScript('https://unpkg.com/react-dom@18/umd/react-dom.production.min.js');
}
然后在 React 组件加载前调用:
import { ensureReact } from './utils/loadExternal';
async function renderApp() {
await ensureReact();
// 此时 window.React 已存在,externals 配置生效
const App = await import('./App');
ReactDOM.render(<App />, document.getElementById('root'));
}
这样既享受了 externals 的体积优势,又避免了不必要的资源加载。不过要注意错误处理,CDN 挂了得有降级方案,比如切备用地址或提示用户。
别乱用 externals,这些情况反而更糟
不是所有依赖都适合 externals。我见过有人把业务组件库也配进去,结果每次发版都要手动更新 CDN,缓存还经常出问题,最后回滚了。
以下情况建议别用 externals:
- 依赖频繁更新,CDN 缓存策略不好控制
- 依赖体积小(比如 <10KB),外置带来的 HTTP 请求开销可能超过收益
- 依赖没有可靠的 CDN 源,或者公司内网无法访问公共 CDN
另外,如果你用的是 Vite、Rollup 这类构建工具,externals 的配置方式完全不同。Vite 里要用 build.rollupOptions.external,而且只能用于 library 模式。所以别看到 externals 就往上套,先看你的构建工具支不支持。
结尾唠叨两句
以上是我折腾 externals 后的真实总结,有更优的实现方式欢迎评论区交流。说实话,这个功能在现代前端工程里用得越来越少了——HTTP/2 多路复用、代码分割、长效缓存策略成熟之后,externals 的优势没那么明显了。但在特定场景(比如老项目优化、微前端、强依赖 CDN 的环境),它依然是个利器。
这个技巧的拓展用法还有很多,比如结合 Service Worker 做离线缓存,或者用 externals 实现运行时模块替换。后续会继续分享这类博客,毕竟前端这行,不就是一边踩坑一边填坑嘛。

暂无评论