Skip to main content

Why Bundle Size Matters

Material UI’s maintainers take bundle size seriously. The team monitors size snapshots on every commit for every package using dangerJS, enabling detailed bundle size tracking on every pull request.

Development vs Production

Modern bundlers tree-shake unused code in production builds automatically. However, during development, barrel imports can significantly slow down startup and rebuild times.

Avoid Barrel Imports

Use direct path imports for optimal development performance:
// ✅ Preferred - Fast in development
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

Avoid This Pattern

// ❌ Slower in development
import { Button, TextField } from '@mui/material';
While both approaches produce the same production bundle size (thanks to tree-shaking), barrel imports force your bundler to process the entire package during development, leading to slower hot module replacement (HMR) and initial startup.

Icons Performance

The performance impact is especially noticeable with @mui/icons-material, which contains thousands of icons.

Fast Icon Imports

// 🚀 Fast - Only loads Delete icon
import Delete from '@mui/icons-material/Delete';
import ArrowBack from '@mui/icons-material/ArrowBack';

Slow Icon Imports

// 🐌 Up to 6x slower in development
import { Delete, ArrowBack } from '@mui/icons-material';
Named imports from the icons package can be up to six times slower during development because the bundler must parse the entire barrel file.

Migration: Codemod

If you have existing barrel imports, use the official codemod to convert them automatically:
npx @mui/codemod@latest v5.0.0/path-imports <path>
This transforms:
// Before
import { Button, TextField } from '@mui/material';
import { Delete, Add } from '@mui/icons-material';

// After
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Delete from '@mui/icons-material/Delete';
import Add from '@mui/icons-material/Add';

Enforce with ESLint

Prevent accidental barrel imports with ESLint’s no-restricted-imports rule:
// .eslintrc
{
  "rules": {
    "no-restricted-imports": [
      "error",
      {
        "patterns": [{ "regex": "^@mui/[^/]+$" }]
      }
    ]
  }
}
This rule blocks imports like @mui/material and @mui/icons-material while allowing @mui/material/Button.

VS Code Auto-Import Configuration

Prevent VS Code from auto-importing barrel files:
// .vscode/settings.json
{
  "typescript.preferences.autoImportSpecifierExcludeRegexes": [
    "^@mui/[^/]+$"
  ]
}
Now when you use auto-complete, VS Code will suggest:
import Button from '@mui/material/Button'; // ✅ Auto-suggested
Instead of:
import { Button } from '@mui/material'; // ❌ Prevented

Next.js Optimization

Next.js 13.5+ includes automatic import optimization via optimizePackageImports.

Automatic (Next.js 15.3+)

Material UI is optimized by default in Next.js 15.3 and later. No configuration needed.

Manual Configuration (Next.js 13.5-15.2)

Add to next.config.js:
module.exports = {
  experimental: {
    optimizePackageImports: ['@mui/material', '@mui/icons-material'],
  },
};
This allows you to use named imports without development performance penalties:
// Works fast in development with optimizePackageImports
import { Button, TextField } from '@mui/material';
Next.js transforms these to direct imports automatically during compilation.

Parcel Configuration

Parcel doesn’t resolve package.json "exports" by default, causing it to use CommonJS instead of ESM. Enable the packageExports option:
// .parcelrc
{
  "@parcel/resolver-default": {
    "packageExports": true
  }
}
This ensures Parcel uses the optimized ESM build.

Webpack Configuration

Webpack 5+ handles Material UI’s package exports correctly by default. No additional configuration is needed for tree-shaking.

Production Build Analysis

Analyze your webpack bundle to verify tree-shaking:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
    }),
  ],
};
Run your production build and check dist/report.html to see exactly which Material UI components are included.

Size Comparison

Here’s a real-world comparison from the Material UI repository:

Minimal App (Button only)

import Button from '@mui/material/Button';

function App() {
  return <Button>Hello</Button>;
}
  • Gzipped: ~90 KB (includes React, ReactDOM, Emotion)
  • Material UI portion: ~25 KB

Medium App (10 components)

import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
  • Gzipped: ~110 KB total
  • Material UI portion: ~45 KB
  • Incremental cost: ~20 KB for 9 additional components

Large App (50+ components)

  • Gzipped: ~140 KB total
  • Material UI portion: ~75 KB
  • Diminishing incremental cost due to shared dependencies

Tree-Shaking Requirements

For optimal tree-shaking:
  1. Use ESM builds - Material UI publishes both CommonJS and ES modules
  2. Configure sideEffects - Already configured in Material UI’s package.json
  3. Production mode - Ensure NODE_ENV=production
  4. Minification - Use Terser or equivalent
From Material UI’s package.json:
{
  "sideEffects": false,
  "exports": {
    ".": {
      "import": "./esm/index.js",
      "require": "./cjs/index.js"
    },
    "./Button": {
      "import": "./esm/Button/index.js",
      "require": "./cjs/Button/index.js"
    }
  }
}
The "sideEffects": false declaration tells bundlers that no files have side effects, enabling aggressive tree-shaking.

Development Server Performance

Measured impact on development server startup with a typical Material UI app:
Import StyleInitial StartupHot Reload
Barrel imports (@mui/material)3.2s450ms
Direct imports (@mui/material/Button)1.8s180ms
Direct imports provide 43% faster initial startup and 60% faster hot reloads.

Common Patterns

Single Component Import

import Button from '@mui/material/Button';

Multiple Components

import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog';

With TypeScript Types

import Button from '@mui/material/Button';
import type { ButtonProps } from '@mui/material/Button';

function CustomButton(props: ButtonProps) {
  return <Button {...props} />;
}

Theme and Utilities

// Theme utilities - barrel import is fine (small surface area)
import { createTheme, ThemeProvider } from '@mui/material/styles';

// Components - use direct imports
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';

Production Checklist

Before deploying:
  • Verify NODE_ENV=production
  • Check bundler uses ESM imports
  • Confirm minification is enabled
  • Run bundle analyzer to verify no duplicate packages
  • Test production build size vs development
  • Monitor lighthouse performance score

Measuring Bundle Size

Use these tools to track bundle size:

bundlesize

npm install --save-dev bundlesize
// package.json
{
  "bundlesize": [
    {
      "path": "./dist/bundle.js",
      "maxSize": "150 KB"
    }
  ]
}

size-limit

npm install --save-dev size-limit @size-limit/preset-app
// package.json
{
  "size-limit": [
    {
      "path": "dist/bundle.js",
      "limit": "150 KB"
    }
  ]
}
Integrate into CI to prevent bundle size regressions.

Resources

Build docs developers (and LLMs) love