How to tree-shake in React
Tree-shaking in React removes unused code from production bundles by analyzing ES module imports and eliminating dead code paths. With over 12 years of React experience since 2014 and as the creator of CoreUI, I’ve optimized bundle sizes through proper tree-shaking. Modern bundlers like Webpack and Vite automatically tree-shake ES modules, but proper import patterns are crucial for effectiveness. This approach significantly reduces JavaScript payload by including only code actually used in the application.
Use ES module imports, named imports from libraries, and sideEffects configuration for effective tree-shaking in React.
ES modules for tree-shaking:
// ❌ Bad - CommonJS, cannot tree-shake
const { Button } = require('./components')
// ✅ Good - ES modules, can tree-shake
import { Button } from './components'
// ❌ Bad - imports everything
import * as Components from './components'
// ✅ Good - imports only what's needed
import { Button, Input } from './components'
Named imports from libraries:
// ❌ Bad - imports entire lodash
import _ from 'lodash'
const result = _.debounce(fn, 300)
// ✅ Good - imports only debounce
import { debounce } from 'lodash-es'
const result = debounce(fn, 300)
// ✅ Better - direct path import
import debounce from 'lodash-es/debounce'
// React icons example
// ❌ Bad
import * as Icons from 'react-icons/fa'
<Icons.FaUser />
// ✅ Good
import { FaUser } from 'react-icons/fa'
<FaUser />
Package.json sideEffects:
{
"name": "my-react-app",
"sideEffects": [
"*.css",
"*.scss"
]
}
Component exports for tree-shaking:
// components/index.js
// ❌ Bad - re-exports everything
export * from './Button'
export * from './Input'
export * from './Select'
// ✅ Good - explicit named exports
export { Button } from './Button'
export { Input } from './Input'
export { Select } from './Select'
// Even better - individual files
// import { Button } from './components/Button'
Tree-shakeable utility functions:
// utils/index.js
export const formatDate = (date) => {
return new Date(date).toLocaleDateString()
}
export const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
export const truncate = (str, length) => {
return str.length > length ? str.substring(0, length) + '...' : str
}
// App.jsx - only formatDate is bundled
import { formatDate } from './utils'
Webpack configuration:
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // Mark unused exports
minimize: true, // Remove dead code
sideEffects: true // Respect package.json sideEffects
}
}
Vite tree-shaking:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
}
}
}
})
// Tree-shaking works automatically with ES modules
Conditional code elimination:
// Environment-based tree-shaking
if (process.env.NODE_ENV === 'development') {
// This code is removed in production build
console.log('Development mode')
}
// Feature flags
const ENABLE_ANALYTICS = true
if (ENABLE_ANALYTICS) {
import('./analytics').then(module => module.init())
}
// If ENABLE_ANALYTICS is false, analytics code is tree-shaken
Material-UI tree-shaking:
// ❌ Bad - imports all of MUI
import { Button, TextField } from '@mui/material'
// ✅ Good - direct imports
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Or use babel plugin
// .babelrc
{
"plugins": [
[
"babel-plugin-import",
{
"libraryName": "@mui/material",
"libraryDirectory": "",
"camel2DashComponentName": false
},
"core"
]
]
}
Analyze tree-shaking:
# Build and analyze
npm run build
# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
# package.json
{
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'"
}
}
# Check what's included
npm install -g source-map-explorer
source-map-explorer build/static/js/main.*.js
Dead code example:
// utils.js
export const usedFunction = () => {
return 'This is used'
}
export const unusedFunction = () => {
return 'This is never imported'
}
// App.jsx
import { usedFunction } from './utils'
// unusedFunction is tree-shaken from final bundle
React component tree-shaking:
// components/Button.jsx
export function PrimaryButton(props) {
return <button className="primary" {...props} />
}
export function SecondaryButton(props) {
return <button className="secondary" {...props} />
}
export function DangerButton(props) {
return <button className="danger" {...props} />
}
// App.jsx - only PrimaryButton is bundled
import { PrimaryButton } from './components/Button'
function App() {
return <PrimaryButton>Click</PrimaryButton>
}
Best Practice Note
Use ES modules instead of CommonJS for tree-shakeable code. Import specific named exports, not entire namespaces. Configure sideEffects in package.json for pure modules. Use direct imports from large libraries like lodash-es. Enable usedExports in webpack optimization. Avoid default exports when possible for better tree-shaking. Use webpack-bundle-analyzer to verify tree-shaking effectiveness. This is how we tree-shake CoreUI React applications—ES modules, named imports, and proper bundler configuration reducing bundle size by 30-50% through dead code elimination.



