Modern Development Best Practices
1. Clean Code Principles SOLID
Apply SOLID principles and clean code practices to write maintainable, testable, and scalable frontend applications.
| Principle | Definition | Frontend Application | Violation Example |
|---|---|---|---|
| Single Responsibility | One reason to change | Component does one thing well | Component handles UI + API + state |
| Open/Closed | Open for extension, closed for modification | Use composition, HOCs, hooks | Modify existing code for new features |
| Liskov Substitution | Subtypes must be substitutable | Consistent component interfaces | Child breaks parent's contract |
| Interface Segregation | Small, specific interfaces | Focused props interfaces | Large props with many optionals |
| Dependency Inversion | Depend on abstractions | Inject dependencies via props/context | Hard-coded dependencies |
Example: Single Responsibility Principle
// ❌ Bad: Component doing too much
function UserDashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
useEffect(() => {
// Fetching data
fetch('/api/user').then(r => r.json()).then(setUser);
fetch('/api/posts').then(r => r.json()).then(setPosts);
fetch('/api/comments').then(r => r.json()).then(setComments);
}, []);
// Data transformation logic
const processedPosts = posts.map(post => ({
...post,
commentCount: comments.filter(c => c.postId === post.id).length
}));
// Rendering complex UI
return (
<div>
<header>{user?.name}</header>
<div>{/* Complex post list */}</div>
<div>{/* Complex comment section */}</div>
</div>
);
}
// ✅ Good: Separated responsibilities
// Custom hook for data fetching
function useUserData() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return user;
}
function usePosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts);
}, []);
return posts;
}
// Separate component for user header
function UserHeader({ user }) {
return <header>{user?.name}</header>;
}
// Separate component for posts
function PostList({ posts }) {
return <div>{posts.map(post => <PostCard key={post.id} post={post} />)}</div>;
}
// Main component just orchestrates
function UserDashboard() {
const user = useUserData();
const posts = usePosts();
return (
<div>
<UserHeader user={user} />
<PostList posts={posts} />
</div>
);
}
Example: Open/Closed Principle with composition
// ❌ Bad: Modifying component for each variation
function Button({ type, icon, loading, disabled, onClick }) {
if (loading) {
return <button disabled><Spinner /> Loading...</button>;
}
if (type === 'primary') {
return <button className="btn-primary" onClick={onClick}>{icon} Click</button>;
}
if (type === 'secondary') {
return <button className="btn-secondary" onClick={onClick}>Click</button>;
}
// Adding new types requires modifying this component
}
// ✅ Good: Open for extension via composition
function Button({ children, disabled, onClick, className }) {
return (
<button
className={`btn ${className}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Extend via composition without modifying Button
function PrimaryButton({ children, ...props }) {
return <Button className="btn-primary" {...props}>{children}</Button>;
}
function SecondaryButton({ children, ...props }) {
return <Button className="btn-secondary" {...props}>{children}</Button>;
}
function LoadingButton({ loading, children, ...props }) {
return (
<Button disabled={loading} {...props}>
{loading ? <><Spinner /> Loading...</> : children}
</Button>
);
}
function IconButton({ icon, children, ...props }) {
return (
<Button {...props}>
{icon} {children}
</Button>
);
}
// Easy to add new variants without touching base Button
function DangerButton({ children, ...props }) {
return <Button className="btn-danger" {...props}>{children}</Button>;
}
Example: Dependency Inversion with dependency injection
// ❌ Bad: Hard-coded dependencies
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// Directly coupled to specific API implementation
axios.get('https://api.example.com/users')
.then(response => setUsers(response.data));
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Good: Depend on abstraction (interface/contract)
// Abstract API service
interface UserService {
getUsers(): Promise<User[]>;
}
// Concrete implementation
class ApiUserService implements UserService {
async getUsers() {
const response = await axios.get('https://api.example.com/users');
return response.data;
}
}
// Mock implementation for testing
class MockUserService implements UserService {
async getUsers() {
return [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
}
}
// Component depends on abstraction, not concrete implementation
function UserList({ userService }: { userService: UserService }) {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
userService.getUsers().then(setUsers);
}, [userService]);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Usage with dependency injection
function App() {
const userService = new ApiUserService(); // or MockUserService for testing
return <UserList userService={userService} />;
}
// Even better: Use context for DI
const ServiceContext = createContext<UserService>(new ApiUserService());
function UserList() {
const userService = useContext(ServiceContext);
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
userService.getUsers().then(setUsers);
}, [userService]);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Clean Code Guidelines: Functions should be small (5-20 lines), do one thing. Names should be
descriptive.
Avoid magic numbers. Comments explain why, not what. DRY (Don't Repeat Yourself). KISS (Keep It Simple, Stupid).
2. Design Patterns Factory Observer
Apply proven design patterns to solve common frontend architecture challenges with reusable, maintainable solutions.
| Pattern | Purpose | Frontend Use Case | Implementation |
|---|---|---|---|
| Factory Pattern | Object creation abstraction | Component factories, form builders | Function returns components |
| Observer Pattern | Event-driven updates | State management, pub/sub | Subscribe/notify pattern |
| Singleton Pattern | Single instance | API client, config, store | Module exports instance |
| Strategy Pattern | Interchangeable algorithms | Payment methods, validators | Interface + implementations |
| Decorator Pattern | Add behavior dynamically | HOCs, wrapper components | Enhance component/function |
| Facade Pattern | Simplified interface | API wrappers, complex libs | Wrapper with simple API |
Example: Factory Pattern for dynamic component creation
// Factory function to create form fields based on config
type FieldType = 'text' | 'email' | 'number' | 'select' | 'checkbox';
interface FieldConfig {
type: FieldType;
name: string;
label: string;
options?: { value: string; label: string }[];
validation?: any;
}
function createFormField(config: FieldConfig) {
switch (config.type) {
case 'text':
case 'email':
case 'number':
return <TextInput key={config.name} {...config} />;
case 'select':
return <SelectInput key={config.name} {...config} />;
case 'checkbox':
return <CheckboxInput key={config.name} {...config} />;
default:
throw new Error(`Unknown field type: ${config.type}`);
}
}
// Form builder using factory
function DynamicForm({ fields }: { fields: FieldConfig[] }) {
return (
<form>
{fields.map(field => createFormField(field))}
<button type="submit">Submit</button>
</form>
);
}
// Usage
const formConfig: FieldConfig[] = [
{ type: 'text', name: 'name', label: 'Name' },
{ type: 'email', name: 'email', label: 'Email' },
{ type: 'select', name: 'country', label: 'Country', options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' }
]}
];
<DynamicForm fields={formConfig} />
// Chart factory example
function createChart(type: 'line' | 'bar' | 'pie', data: any) {
const chartComponents = {
line: LineChart,
bar: BarChart,
pie: PieChart
};
const ChartComponent = chartComponents[type];
return <ChartComponent data={data} />;
}
Example: Observer Pattern for event system
// Event bus implementation (Observer pattern)
class EventBus {
private events: Map<string, Set<Function>> = new Map();
subscribe(event: string, callback: Function): () => void {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(callback);
// Return unsubscribe function
return () => {
this.events.get(event)?.delete(callback);
};
}
publish(event: string, data?: any): void {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
clear(event?: string): void {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
}
}
export const eventBus = new EventBus();
// Usage: Shopping cart example
// CartButton.tsx
function CartButton() {
const addToCart = (product: Product) => {
eventBus.publish('cart:add', product);
};
return <button onClick={() => addToCart(product)}>Add to Cart</button>;
}
// CartBadge.tsx - Observes cart events
function CartBadge() {
const [count, setCount] = useState(0);
useEffect(() => {
const unsubscribe = eventBus.subscribe('cart:add', () => {
setCount(prev => prev + 1);
});
return unsubscribe; // Cleanup on unmount
}, []);
return <div className="badge">{count}</div>;
}
// State management with observer pattern
class Store {
private state: any;
private listeners = new Set<Function>();
getState() {
return this.state;
}
setState(newState: any) {
this.state = { ...this.state, ...newState };
this.notify();
}
subscribe(listener: Function) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
const store = new Store();
// React hook for store
function useStore() {
const [state, setState] = useState(store.getState());
useEffect(() => {
return store.subscribe((newState: any) => {
setState(newState);
});
}, []);
return [state, (newState: any) => store.setState(newState)];
}
Example: Strategy Pattern for payment processing
// Strategy interface
interface PaymentStrategy {
processPayment(amount: number): Promise<PaymentResult>;
validate(): boolean;
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
validate(): boolean {
return this.cardNumber.length === 16 && this.cvv.length === 3;
}
async processPayment(amount: number): Promise<PaymentResult> {
// Credit card processing logic
const response = await fetch('/api/payment/credit-card', {
method: 'POST',
body: JSON.stringify({ cardNumber: this.cardNumber, amount })
});
return response.json();
}
}
class PayPalPayment implements PaymentStrategy {
constructor(private email: string) {}
validate(): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
async processPayment(amount: number): Promise<PaymentResult> {
// PayPal processing logic
const response = await fetch('/api/payment/paypal', {
method: 'POST',
body: JSON.stringify({ email: this.email, amount })
});
return response.json();
}
}
class CryptoPayment implements PaymentStrategy {
constructor(private walletAddress: string) {}
validate(): boolean {
return this.walletAddress.length === 42;
}
async processPayment(amount: number): Promise<PaymentResult> {
// Cryptocurrency processing logic
const response = await fetch('/api/payment/crypto', {
method: 'POST',
body: JSON.stringify({ wallet: this.walletAddress, amount })
});
return response.json();
}
}
// Context that uses strategy
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
async execute(amount: number): Promise<PaymentResult> {
if (!this.strategy.validate()) {
throw new Error('Invalid payment details');
}
return this.strategy.processPayment(amount);
}
}
// React component using strategies
function CheckoutForm() {
const [paymentMethod, setPaymentMethod] = useState<'card' | 'paypal' | 'crypto'>('card');
const [processor] = useState(new PaymentProcessor(new CreditCardPayment('', '')));
const handlePayment = async (amount: number) => {
// Select strategy based on user choice
let strategy: PaymentStrategy;
switch (paymentMethod) {
case 'card':
strategy = new CreditCardPayment(cardNumber, cvv);
break;
case 'paypal':
strategy = new PayPalPayment(email);
break;
case 'crypto':
strategy = new CryptoPayment(walletAddress);
break;
}
processor.setStrategy(strategy);
const result = await processor.execute(amount);
if (result.success) {
alert('Payment successful!');
}
};
return (
<form>
<select onChange={(e) => setPaymentMethod(e.target.value as any)}>
<option value="card">Credit Card</option>
<option value="paypal">PayPal</option>
<option value="crypto">Cryptocurrency</option>
</select>
{/* Render appropriate form fields based on strategy */}
<button onClick={() => handlePayment(99.99)}>Pay Now</button>
</form>
);
}
Pattern Overuse: Don't force patterns where they're not needed. Patterns add complexity and
abstraction.
Use them to solve specific problems, not to demonstrate knowledge. Start simple, refactor to patterns when
needed.
3. Performance Budgets Lighthouse CI
Define and enforce performance budgets with automated testing to prevent performance regressions in production.
| Metric | Good Target | Tool | Impact |
|---|---|---|---|
| First Contentful Paint (FCP) | <1.8s | Lighthouse, WebPageTest | Perceived speed |
| Largest Contentful Paint (LCP) | <2.5s | Core Web Vitals | Loading performance |
| Time to Interactive (TTI) | <3.8s | Lighthouse | Interactivity |
| Total Blocking Time (TBT) | <200ms | Lighthouse | Responsiveness |
| Cumulative Layout Shift (CLS) | <0.1 | Core Web Vitals | Visual stability |
| JavaScript Bundle Size | <200KB (gzipped) | Webpack Bundle Analyzer | Load time |
Example: Lighthouse CI setup for automated performance testing
// Installation
npm install -D @lhci/cli
// lighthouserc.json
{
"ci": {
"collect": {
"startServerCommand": "npm run build && npm run preview",
"url": [
"http://localhost:4173/",
"http://localhost:4173/products",
"http://localhost:4173/checkout"
],
"numberOfRuns": 3
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
// Performance budgets
"first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"interactive": ["error", { "maxNumericValue": 3800 }],
"speed-index": ["error", { "maxNumericValue": 3400 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
// Resource budgets
"resource-summary:script:size": ["error", { "maxNumericValue": 204800 }], // 200KB
"resource-summary:stylesheet:size": ["error", { "maxNumericValue": 51200 }], // 50KB
"resource-summary:image:size": ["error", { "maxNumericValue": 512000 }], // 500KB
"resource-summary:total:size": ["error", { "maxNumericValue": 1048576 }], // 1MB
// Best practices
"uses-responsive-images": "error",
"offscreen-images": "error",
"modern-image-formats": "warn",
"uses-text-compression": "error",
"unused-javascript": "warn",
// Accessibility
"color-contrast": "error",
"heading-order": "error",
"label": "error"
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
// package.json scripts
{
"scripts": {
"lhci:collect": "lhci collect",
"lhci:assert": "lhci assert",
"lhci:upload": "lhci upload",
"lhci:full": "lhci collect && lhci assert && lhci upload"
}
}
Example: GitHub Actions CI workflow with Lighthouse
// .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: lighthouse-results
path: .lighthouseci/
- name: Comment PR
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:4173
http://localhost:4173/products
uploadArtifacts: true
temporaryPublicStorage: true
Example: Webpack performance budgets
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 244000, // 244KB
maxEntrypointSize: 244000,
hints: 'error', // or 'warning'
assetFilter: function(assetFilename) {
// Only check JS and CSS files
return /\.(js|css)$/.test(assetFilename);
}
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
// Next.js config with budgets
// next.config.js
module.exports = {
experimental: {
performanceBudgets: [
{
path: '/',
maxSize: {
total: 200 * 1024, // 200KB
javascript: 150 * 1024,
css: 50 * 1024
}
},
{
path: '/products',
maxSize: {
total: 250 * 1024
}
}
]
}
};
// bundlesize package
// package.json
{
"bundlesize": [
{
"path": "./dist/main.*.js",
"maxSize": "150 kB"
},
{
"path": "./dist/vendor.*.js",
"maxSize": "50 kB"
},
{
"path": "./dist/styles.*.css",
"maxSize": "30 kB"
}
]
}
// CI script
npm install -g bundlesize
bundlesize
Monitoring Strategy: Set budgets 20% lower than current performance to encourage optimization.
Run Lighthouse on every PR. Track Core Web Vitals in production with Real User Monitoring (RUM). Review budgets
quarterly.
4. Code Splitting Route Based
Implement route-based and component-based code splitting to reduce initial bundle size and improve load performance.
| Technique | When to Use | Benefits | Trade-offs |
|---|---|---|---|
| Route-based Splitting | Different pages/routes | Smaller initial bundle, faster FCP | Navigation delay |
| Component-based Splitting | Large components (modals, charts) | Load on demand | Complexity |
| Vendor Splitting | Third-party libraries | Better caching | More requests |
| Dynamic Import | Conditional features | Load only when needed | Network waterfall |
Example: React route-based code splitting
// App.tsx - Route-based lazy loading
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Eagerly loaded (always needed)
import { Header } from './components/Header';
import { Footer } from './components/Footer';
// Lazy loaded routes (loaded on demand)
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Admin = lazy(() => import('./pages/Admin'));
// Loading fallback
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
export function App() {
return (
<BrowserRouter>
<Header />
<main>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Suspense>
</main>
<Footer />
</BrowserRouter>
);
}
// Result: Each route is a separate chunk
// home.chunk.js, products.chunk.js, checkout.chunk.js, etc.
Example: Component-based code splitting with prefetching
// Lazy load heavy components
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const VideoPlayer = lazy(() => import('./components/VideoPlayer'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
// Prefetch on hover for better UX
function ProductPage() {
const [showChart, setShowChart] = useState(false);
const handleMouseEnter = () => {
// Prefetch chart component
import('./components/HeavyChart');
};
return (
<div>
<h1>Product Analytics</h1>
<button
onClick={() => setShowChart(true)}
onMouseEnter={handleMouseEnter}
>
Show Chart
</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={data} />
</Suspense>
)}
</div>
);
}
// Conditional feature loading
function AdminPanel() {
const { user } = useAuth();
const [AdminTools, setAdminTools] = useState(null);
useEffect(() => {
if (user?.role === 'admin') {
// Only load admin tools for admin users
import('./components/AdminTools').then(module => {
setAdminTools(() => module.default);
});
}
}, [user]);
if (!user) return <Login />;
if (user.role !== 'admin') return <Forbidden />;
if (!AdminTools) return <Loading />;
return <AdminTools />;
}
// Named exports lazy loading
const { AdvancedFilters, DataExport } = lazy(() =>
import('./components/AdvancedFeatures').then(module => ({
default: {
AdvancedFilters: module.AdvancedFilters,
DataExport: module.DataExport
}
}))
);
Example: Next.js dynamic imports with loading states
// Next.js dynamic import
import dynamic from 'next/dynamic';
// Basic dynamic import
const DynamicComponent = dynamic(() => import('./components/Heavy'), {
loading: () => <p>Loading...</p>,
ssr: false // Disable SSR for this component
});
// Import with named export
const DynamicChart = dynamic(
() => import('./components/Charts').then(mod => mod.LineChart),
{ loading: () => <ChartSkeleton /> }
);
// Conditional dynamic import
function Dashboard() {
const DynamicMap = dynamic(() => import('./components/Map'), {
loading: () => <MapSkeleton />,
ssr: false
});
return (
<div>
<h1>Dashboard</h1>
<DynamicMap center={[51.505, -0.09]} zoom={13} />
</div>
);
}
// Multiple dynamic components
const [Modal, Tooltip, Drawer] = [
dynamic(() => import('./components/Modal')),
dynamic(() => import('./components/Tooltip')),
dynamic(() => import('./components/Drawer'))
];
// Webpack magic comments for better chunk names
const ProductDetail = lazy(() =>
import(
/* webpackChunkName: "product-detail" */
/* webpackPrefetch: true */
'./pages/ProductDetail'
)
);
Best Practices: Split routes by default. Lazy load modals, charts, rich editors. Prefetch on
hover/focus.
Use loading skeletons. Keep critical path synchronous. Monitor chunk sizes with bundle analyzer.
5. Tree Shaking Dead Code Elimination
Eliminate unused code from bundles through proper module exports, sideEffects configuration, and build optimizations.
| Technique | Purpose | Configuration | Impact |
|---|---|---|---|
| ES6 Modules | Enable static analysis | Use import/export | Required for tree shaking |
| sideEffects Flag | Mark pure modules | package.json | Aggressive elimination |
| Named Exports | Import only what's needed | Avoid default exports | Better tree shaking |
| Production Mode | Enable optimizations | NODE_ENV=production | Removes dev code |
Example: Proper module structure for tree shaking
// ❌ Bad: Default export with everything
// utils.js
export default {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
// ... 50 more functions
};
// Usage imports everything (no tree shaking)
import utils from './utils';
utils.add(1, 2);
// ✅ Good: Named exports
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
// Usage imports only what's needed (tree shaken)
import { add } from './utils';
add(1, 2);
// ❌ Bad: Barrel exports without re-exports
// index.js
export * from './utils';
export * from './validators';
export * from './formatters';
// ✅ Good: Direct imports or selective barrel exports
// Import directly from source
import { add } from './utils/math';
import { validateEmail } from './validators/email';
// Or use selective barrel exports
// index.js
export { add, subtract } from './utils/math';
export { validateEmail, validatePhone } from './validators';
// ❌ Bad: Side effects prevent tree shaking
// module.js
import './styles.css'; // Side effect
console.log('Module loaded'); // Side effect
export const doSomething = () => {};
// ✅ Good: Pure module
// module.js
export const doSomething = () => {};
// Import styles separately when needed
// App.tsx
import './styles.css';
import { doSomething } from './module';
Example: package.json sideEffects configuration
// package.json - Library configuration
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"sideEffects": false, // No side effects, safe to tree shake
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js"
},
"./utils": {
"import": "./dist/utils.esm.js",
"require": "./dist/utils.js"
}
}
}
// Or specify files with side effects
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
// Webpack configuration
module.exports = {
mode: 'production', // Enables tree shaking
optimization: {
usedExports: true, // Mark unused exports
minimize: true, // Remove unused code
sideEffects: true // Respect package.json sideEffects
}
};
// Rollup configuration
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [
resolve(),
commonjs(),
terser() // Minify and tree shake
],
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false
}
};
Example: Optimizing library imports for tree shaking
// ❌ Bad: Import entire library
import _ from 'lodash'; // Imports all of lodash (~70KB)
import * as MUI from '@mui/material'; // Imports everything
import moment from 'moment'; // Imports full moment.js
_.debounce(fn, 300);
const button = <MUI.Button />;
moment().format();
// ✅ Good: Import specific functions/components
import debounce from 'lodash/debounce'; // Only debounce (~2KB)
import Button from '@mui/material/Button'; // Only Button component
import dayjs from 'dayjs'; // Smaller alternative to moment
debounce(fn, 300);
const button = <Button />;
dayjs().format();
// ✅ Better: Use libraries with good tree shaking
// lodash-es (ES modules version)
import { debounce, throttle } from 'lodash-es';
// date-fns (tree-shakeable by default)
import { format, addDays } from 'date-fns';
// ✅ Best: Babel plugin for automatic optimization
// .babelrc
{
"plugins": [
["import", {
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false
}],
["import", {
"libraryName": "@mui/material",
"libraryDirectory": "",
"camel2DashComponentName": false
}]
]
}
// Write normal imports, babel transforms them
import { Button, TextField } from '@mui/material';
// Becomes:
// import Button from '@mui/material/Button';
// import TextField from '@mui/material/TextField';
// Next.js optimized imports (built-in)
// next.config.js
module.exports = {
modularizeImports: {
'@mui/material': {
transform: '@mui/material/{{member}}'
},
'lodash': {
transform: 'lodash/{{member}}'
}
}
};
Common Pitfalls: CommonJS modules can't be tree shaken. Barrel exports (index.js) can prevent
tree shaking.
Side effects (CSS imports, global mutations) prevent elimination. Always analyze bundle with
webpack-bundle-analyzer.
6. Bundle Optimization Webpack Rollup
Optimize production bundles with advanced techniques: minification, compression, code splitting, and caching strategies.
| Optimization | Technique | Tool | Benefit |
|---|---|---|---|
| Minification | Remove whitespace, shorten names | Terser, esbuild | 30-50% size reduction |
| Compression | Gzip, Brotli | compression-webpack-plugin | 60-80% size reduction |
| Code Splitting | Separate vendor/async chunks | SplitChunksPlugin | Better caching |
| Content Hashing | Filename based on content | [contenthash] | Long-term caching |
| Scope Hoisting | Flatten module scope | ModuleConcatenationPlugin | Smaller bundle, faster |
Example: Advanced Webpack optimization configuration
// webpack.config.js - Production optimization
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/assets/'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true,
pure_funcs: ['console.info', 'console.debug']
},
mangle: {
safari10: true
},
format: {
comments: false
}
},
extractComments: false,
parallel: true
})
],
// Split chunks for better caching
splitChunks: {
chunks: 'all',
cacheGroups: {
// Vendor libraries
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
// Common code shared across chunks
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
enforce: true
},
// Large libraries in separate chunks
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20
},
// UI libraries
mui: {
test: /[\\/]node_modules[\\/]@mui[\\/]/,
name: 'mui',
priority: 15
}
},
maxAsyncRequests: 30,
maxInitialRequests: 30,
minSize: 20000,
maxSize: 244000
},
// Runtime chunk for better caching
runtimeChunk: {
name: 'runtime'
},
// Module IDs based on path
moduleIds: 'deterministic'
},
plugins: [
// Gzip compression
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
}),
// Brotli compression (better than gzip)
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 10240,
minRatio: 0.8
}),
// Bundle analysis
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
performance: {
maxAssetSize: 244000,
maxEntrypointSize: 244000,
hints: 'warning'
}
};
Example: Vite optimization configuration
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import compression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
react(),
// Gzip compression
compression({
algorithm: 'gzip',
ext: '.gz'
}),
// Brotli compression
compression({
algorithm: 'brotliCompress',
ext: '.br'
}),
// Bundle analysis
visualizer({
open: false,
filename: 'dist/stats.html',
gzipSize: true,
brotliSize: true
})
],
build: {
target: 'es2020',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// Chunk splitting
rollupOptions: {
output: {
manualChunks: {
// Vendor chunks
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['@mui/material', '@emotion/react'],
'utils': ['lodash-es', 'date-fns']
},
// Chunk naming with content hash
chunkFileNames: 'assets/[name].[hash].js',
entryFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
},
// Chunk size warnings
chunkSizeWarningLimit: 500,
// Source maps for production debugging
sourcemap: 'hidden',
// CSS code splitting
cssCodeSplit: true,
// Asset inlining threshold
assetsInlineLimit: 4096
},
// Optimize dependencies
optimizeDeps: {
include: ['react', 'react-dom'],
exclude: ['@vite/client', '@vite/env']
}
});
Example: Advanced bundle analysis and optimization
// Analyze bundle with webpack-bundle-analyzer
npm install -D webpack-bundle-analyzer
// Run analysis
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json
// Identify optimization opportunities:
// 1. Large dependencies (replace with lighter alternatives)
// 2. Duplicate code (adjust splitChunks)
// 3. Unused imports (tree shaking issues)
// 4. Large assets (optimize images, fonts)
// Source map explorer (alternative)
npm install -D source-map-explorer
// package.json
{
"scripts": {
"analyze": "source-map-explorer 'dist/*.js'"
}
}
// Optimization strategies based on analysis:
// 1. Replace large libraries
// moment.js (67KB) → dayjs (2KB)
// lodash (71KB) → lodash-es + tree shaking
// MaterialUI → lighter alternative or selective imports
// 2. Dynamic imports for large features
const Chart = lazy(() => import(
/* webpackChunkName: "chart" */
/* webpackPrefetch: true */
'./components/Chart'
));
// 3. Optimize images
// Use next/image or webpack image-loader
{
test: /\.(png|jpg|jpeg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB inline limit
}
},
use: [
{
loader: 'image-webpack-loader',
options: {
mozjpeg: { progressive: true, quality: 65 },
optipng: { enabled: true },
pngquant: { quality: [0.65, 0.90], speed: 4 },
gifsicle: { interlaced: false },
webp: { quality: 75 }
}
}
]
}
// 4. Preload critical chunks
<link rel="preload" href="/assets/main.js" as="script">
<link rel="prefetch" href="/assets/chart.chunk.js" as="script">
// 5. Enable HTTP/2 server push
// In your server configuration
Link: </assets/main.js>; rel=preload; as=script
Link: </assets/styles.css>; rel=preload; as=style
Bundle Optimization Checklist
| Optimization | Target | Tool | Expected Reduction |
|---|---|---|---|
| Minification | JS, CSS, HTML | Terser, cssnano | 30-40% |
| Compression | All text files | Gzip, Brotli | 60-80% |
| Tree Shaking | Unused code | Webpack, Rollup | 20-50% |
| Code Splitting | Routes, vendors | Dynamic imports | Initial: 40-60% |
| Image Optimization | PNG, JPG, SVG | imagemin, WebP | 50-70% |
| Font Subsetting | Web fonts | glyphhanger | 70-90% |