Webpack Externals 配置实战:优化打包体积与加载性能

芳宁 Dev 前端 阅读 2,913
赞 16 收藏
二维码
手机扫码查看
反馈

Externals 是啥?先看一段让我省了 200KB 的配置

上周我重构一个老项目,打包体积大得离谱,光是 React 和 Lodash 就占了快 300KB。产品经理还在催加载速度,我一拍脑袋:这不就是 externals 的用武之地吗?

Webpack 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(&#039;/&#039;)[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=&quot;${src}&quot;])) {
      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 实现运行时模块替换。后续会继续分享这类博客,毕竟前端这行,不就是一边踩坑一边填坑嘛。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论