Skip to main content
Advertisement

18.4 Bundle Optimization — Tree Shaking and Type Stripping

Tree Shaking and TypeScript

Tree shaking removes unused code from the bundle. There are important considerations to ensure TypeScript code tree shakes properly.

// ❌ Has side effects — cannot be tree shaken
export function util() { /* ... */ }
console.log('Module loaded') // Side effect!

// ✅ Pure functional exports — can be tree shaken
export function util() { /* ... */ }
export function anotherUtil() { /* ... */ }

package.json sideEffects Configuration

{
"sideEffects": false,
// Or specify only files with side effects
"sideEffects": [
"*.css",
"./src/polyfills.ts",
"./src/global-styles.ts"
]
}

TypeScript Type Stripping

What is Type Stripping?
A method of running TypeScript directly by removing only type annotations
Not transpilation — just erasing type information for faster execution

Supported tools:
- Node.js 22.6+ (--experimental-strip-types flag)
- Node.js 23.6+ (built-in support)
- tsx (esbuild-based)
- ts-node --transpile-only
# Node.js 22.6+ — type stripping
node --experimental-strip-types src/index.ts

# Node.js 23.6+ — built-in support
node src/index.ts

# tsx — fast execution (recommended)
npx tsx src/index.ts

tsconfig Bundle Optimization

// tsconfig.json — for library authors
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext", // ESM modules — enables tree shaking
"moduleResolution": "bundler",
"declaration": true, // Generate .d.ts
"declarationMap": true, // Link source maps with types
"sourceMap": true,
"strict": true,
"isolatedModules": true, // Per-file independent builds (Vite compatible)
"verbatimModuleSyntax": true, // Enforce import type — improves tree shaking
}
}
// verbatimModuleSyntax — explicit type-only imports
// ✅ Correct — bundler can remove type imports
import type { User } from './types'
import { createUser } from './utils'

// ❌ Incorrect — bundler may not know it's a type
import { User, createUser } from './utils'

Bundle Analysis

# webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

# next.js — @next/bundle-analyzer
npm install --save-dev @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'

const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})

export default bundleAnalyzer({
// next.js configuration
})
# Run bundle analysis
ANALYZE=true npm run build

Code Splitting

// Dynamic import — route-based code splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'))
const AdminPanel = lazy(() => import('./AdminPanel'))

// Conditional loading
async function loadFeature() {
if (needsFeature) {
const { feature } = await import('./feature')
return feature
}
}

// Next.js — automatic splitting at server/client boundaries
// app/page.tsx → Server Component (excluded from bundle)
// 'use client' → Client Component (included in bundle)

Library Bundle Size Optimization

// ❌ Import entire library
import _ from 'lodash'
const result = _.groupBy(users, 'role')

// ✅ Import only needed functions (tree shaking)
import groupBy from 'lodash/groupBy'
const result = groupBy(users, 'role')

// ✅ Better approach — use lightweight alternative
import { groupBy } from 'remeda' // Much smaller than lodash, TypeScript-first

// Date handling
// ❌ moment.js — 67KB gzipped
// ✅ date-fns — only needed functions (tree shaking optimized)
import { format, addDays } from 'date-fns'

// Schema validation
// Zod 13KB vs Valibot ~2KB (edge environments)

Pro Tips

Bundle Size CI Check

# .github/workflows/bundle-size.yml
- name: Check bundle size
run: |
npm run build
SIZE=$(du -sh dist/ | cut -f1)
echo "Bundle size: $SIZE"

# Fail if size limit exceeded
MAX_SIZE=5242880 # 5MB
ACTUAL=$(du -sb dist/ | cut -f1)
if [ $ACTUAL -gt $MAX_SIZE ]; then
echo "Bundle too large: $ACTUAL bytes (max: $MAX_SIZE)"
exit 1
fi

- name: Bundle size comment
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Advertisement