@mui/material 是 React 生态里使用率极高的组件库,但它的默认引入方式和样式方案会让 Webpack 打包结果变得臃肿。很多同学在生产构建后才发现 bundle 体积多出了五六百 KB,其中一部分是组件本身的代码,另一部分则是样式运行时带来的开销。这篇文章从样式方案、按需引入、Tree Shaking 配置三个层面,把 @mui/material 在 Webpack 下的打包优化讲透。

一、先搞清楚 @mui/material 的样式方案
Material-UI 从 v5 开始默认使用 emotion 作为样式引擎,取代了 v4 时代的 JSS 方案。这两者的打包行为差异很大,理解这一点是优化的前提。
v4 使用 @material-ui/styles 提供的 JSS 运行时方案,样式是在浏览器里动态生成的,也就是说样式逻辑的 JavaScript 代码会被打进 bundle,运行时才生成 CSS 字符串并插入页面。这种方式的好处是支持动态主题和嵌套样式覆盖,代价是运行时体积和首屏渲染开销。v5 切换到 emotion 后,同样属于 CSS-in-JS 运行时方案,但性能更好,并且额外提供了零运行时迁移的路径。
如果你的项目对首屏性能极其敏感,可以考虑 @mui/material-nextjs 这类服务端集成方案,或者直接使用官方维护的 Pigment CSS(原名 Zero Runtime CSS)。Pigment CSS 在构建期就把样式抽取成静态 CSS,打包产物里不再包含样式运行时,体积收益非常明显。不过对于大多数常规项目,emotion 方案配合按需引入已经够用,不必为了几 KB 去引入额外的构建复杂度。
还需要注意一点:不要在同一项目里混用两套样式引擎。有些老项目升级时会同时保留 @mui/styled-engine 和旧的 @material-ui/styles,这会导致两份运行时都被打进包里,体积直接翻倍。
二、按需引入的正确写法
@mui/material v5 之后的版本基于 ES Module 构建,理论上你直接写顶层导入,Tree Shaking 也能工作:
import { Button, TextField, Box } from '@mui/material';这种写法依赖打包器能够正确分析模块依赖关系。在 Webpack 生产模式下,配合 optimization.sideEffects: true,未使用的组件会被摇掉。但如果你观察打包结果发现体积没有明显下降,大概率是某个环节破坏了静态分析,比如通过 Babel 转译把 ES Module 转成了 CommonJS,或者引入了带副作用的入口文件。
更稳妥的方式是使用路径级导入,直接指向具体组件的源码路径:
// 直接从组件路径导入,完全绕过桶文件的副作用分析 import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField';
这种写法的收益在于确定性。即使你的 Webpack 配置有问题,或者某个依赖链引入了副作用,路径级导入也不会把整个组件库拖进包里。官方文档也明确推荐这种写法用于性能敏感的场景。如果觉得手写路径麻烦,可以配置 Babel 插件 babel-plugin-import(v5 之后其实不太需要了,但老项目升级过渡期可以一用)。
图标库 @mui/icons-material 的优化空间更大。图标数量上千个,顶层导入如果 Tree Shaking 失效,代价是灾难性的,务必使用路径导入:
import DeleteIcon from '@mui/icons-material/Delete'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
三、让 Tree Shaking 真正生效的关键配置
Tree Shaking 生效需要三个条件同时满足:代码以 ES Module 形式提供、构建处于生产模式、以及 package.json 中正确声明了 sideEffects。@mui/material 的包本身已经在 package.json 里声明了副作用范围,所以你要做的是确保自己的 Webpack 配置没有破坏这一机制。
首先检查 webpack 配置中的 mode 是否设置为 production,这会自动开启 optimization.usedExports 和压缩。其次确认 babel-loader 或 ts-loader 没有关闭 ES Module 转换。一个常见的坑是在 babel 配置里把 @mui/material 加进了 include 转译范围,并且 @babel/preset-env 的 modules 选项设为了 commonjs:
// webpack 配置中,让 node_modules 里的 ES Module 保持原样
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
sideEffects: true
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
// 注意:不要把 node_modules 中带 ES Module 的包转成 CommonJS
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};如果你使用 TypeScript,还需要在 tsconfig.json 中把 moduleResolution 设为 node、module 设为 esnext 或 preserve,避免编译期就降级模块格式。
验证 Tree Shaking 是否生效,最直接的办法是使用 webpack-bundle-analyzer 生成体积报告,观察产物里是否存在你从未引入的组件(比如 DataGrid、DatePicker 这类重组件)。如果报告里整个 @mui/material 目录都在,说明摇树失败,需要回头排查副作用配置和转译规则。
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({ analyzerMode: 'static' })
]
};四、进阶优化与常见误区
除了摇树,还可以从分割维度继续压缩首屏体积。Material-UI 官方文档站点本身没有使用 Webpack 的 DllPlugin 或externals 方案,而是依赖代码分割:把重组件通过 React.lazy 延迟加载,让主包只包含首屏需要的组件。这种思路对业务项目同样适用,尤其是用了 DataGrid、Date Pickers 这些 Pro 组件时,务必单独分包。
另一个误区是手动覆盖样式时引入了整包。有些同学习惯 import { styled } from '@mui/material/styles' 之后又从别的地方导入工具函数,如果写法不当,可能间接拉入 @mui/system 的全部导出。建议统一使用 @mui/material/styles 这个入口获取 styled、createTheme 等能力,保持导入路径的一致性。
最后提醒一点:优化要基于测量,不要凭感觉。每次调整配置后跑一次 bundle analyzer 对比,重点关注 gzipped 体积而不是原始体积,因为线上传输的是压缩后的结果。一般而言,合理配置后 @mui/material 在业务中的增量成本可以控制在 80KB 到 150KB(gzip 后)之间,如果超出这个区间,说明大概率还有优化的空间没挖到。
Webpack打包优化Material-UITree Shaking修改时间:2026-09-11 15:42:43