Removing Dead Code with JS Tree Shaking Guide
Learn to remove dead code with JavaScript tree shaking. Covers identifying dead code patterns, conditional compilation, environment-based elimination, dynamic import analysis, debugging tree shaking failures, and measuring dead code reduction.
Dead code inflates bundles, slows load times, and increases attack surface. This guide focuses on practical techniques for identifying and eliminating dead code beyond basic tree shaking.
For tree shaking fundamentals and ES module analysis, see JavaScript Tree Shaking: A Complete Tutorial.
Identifying Dead Code
// Dead code pattern 1: Unreachable code after return
function processData(data) {
if (!data) return null;
return transform(data);
console.log("This never executes"); // Dead code
cleanup(); // Dead code
}
// Dead code pattern 2: Unused variables and imports
import { formatDate, formatCurrency, slugify } from "./utils";
// Only formatDate is used below
const unused = "this string is never read"; // Dead code
console.log(formatDate(new Date()));
// Dead code pattern 3: Never-true conditions
const DEBUG = false;
if (DEBUG) {
// This entire block is dead code in production
console.log("Debug info:", internalState);
window.__DEBUG_PANEL__ = createDebugPanel();
}
// Dead code pattern 4: Unused class methods
class DataService {
fetch(url) { /* used */ }
cache(key, value) { /* used */ }
deprecated_legacyFetch(url) { /* never called anywhere */ }
internal_debugLog(msg) { /* never called anywhere */ }
}Environment-Based Dead Code Elimination
// Webpack DefinePlugin replaces expressions at build time
// webpack.config.js
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
"__DEV__": false,
"__FEATURE_ANALYTICS__": true,
"__FEATURE_EXPERIMENTS__": false,
}),
],
};
// Source code with feature flags
if (__DEV__) {
// Entire block removed in production build
enableHotReload();
installDevTools();
window.__APP_STATE__ = appState;
}
if (__FEATURE_EXPERIMENTS__) {
// Removed when feature flag is false
import("./experiments/ABTestRunner").then((mod) => mod.init());
}
// After DefinePlugin replacement:
if (false) {
enableHotReload(); // Dead code, removed by minifier
}
// Conditional exports based on environment
export function log(message) {
if (process.env.NODE_ENV !== "production") {
console.log(`[LOG] ${message}`);
}
// In production: function body is empty, may be eliminated entirely
}Dead Code Detection Script
// Script to find potentially unused exports
const fs = require("fs");
const path = require("path");
class DeadCodeDetector {
constructor(srcDir) {
this.srcDir = srcDir;
this.exports = new Map();
this.imports = new Map();
}
scan() {
const files = this.getJSFiles(this.srcDir);
// Phase 1: Collect all exports
for (const file of files) {
const content = fs.readFileSync(file, "utf8");
this.extractExports(file, content);
this.extractImports(file, content);
}
// Phase 2: Find unused exports
return this.findUnused();
}
extractExports(file, content) {
const patterns = [
/export\s+(?:function|class|const|let|var)\s+(\w+)/g,
/export\s+\{\s*([^}]+)\s*\}/g,
];
const fileExports = [];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(content)) !== null) {
const names = match[1].split(",").map((n) =>
n.trim().split(/\s+as\s+/).pop().trim()
);
fileExports.push(...names);
}
}
if (fileExports.length > 0) {
this.exports.set(file, fileExports);
}
}
extractImports(file, content) {
const pattern = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
let match;
while ((match = pattern.exec(content)) !== null) {
const names = match[1].split(",").map((n) =>
n.trim().split(/\s+as\s+/)[0].trim()
);
const source = match[2];
if (!this.imports.has(source)) {
this.imports.set(source, new Set());
}
names.forEach((n) => this.imports.get(source).add(n));
}
}
findUnused() {
const unused = [];
for (const [file, exports] of this.exports) {
const relativePath = "./" + path.relative(this.srcDir, file)
.replace(/\\/g, "/")
.replace(/\.(js|ts|jsx|tsx)$/, "");
for (const name of exports) {
let isUsed = false;
for (const [importPath, importedNames] of this.imports) {
if (
importPath === relativePath ||
importPath === relativePath.replace("./", "../")
) {
if (importedNames.has(name)) {
isUsed = true;
break;
}
}
}
if (!isUsed) {
unused.push({ file, export: name });
}
}
}
return unused;
}
getJSFiles(dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "node_modules") {
files.push(...this.getJSFiles(fullPath));
} else if (/\.(js|ts|jsx|tsx)$/.test(entry.name)) {
files.push(fullPath);
}
}
return files;
}
}
// Usage
const detector = new DeadCodeDetector("./src");
const unused = detector.scan();
console.table(unused);Debugging Tree Shaking Failures
// Technique 1: Check if a module is being included
// Add this to the suspect module:
console.log("MODULE INCLUDED:", import.meta.url);
// If you see this in production, tree shaking failed
// Technique 2: Webpack stats analysis
// webpack.config.js
module.exports = {
stats: {
usedExports: true,
providedExports: true,
optimizationBailout: true, // Shows WHY tree shaking failed
},
};
// Run with: webpack --json > stats.json
// Look for "optimizationBailout" messages
// Technique 3: Manual side effect check
function checkSideEffects(modulePath) {
const content = fs.readFileSync(modulePath, "utf8");
const sideEffectPatterns = [
{ pattern: /window\.\w+\s*=/, desc: "Global assignment" },
{ pattern: /document\.\w+/, desc: "DOM access" },
{ pattern: /console\.\w+\(/, desc: "Console output" },
{ pattern: /fetch\s*\(/, desc: "Network request" },
{ pattern: /addEventListener\s*\(/, desc: "Event listener" },
{ pattern: /\.prototype\.\w+\s*=/, desc: "Prototype modification" },
{ pattern: /setInterval|setTimeout/, desc: "Timer" },
];
const topLevelCode = extractTopLevelStatements(content);
const issues = [];
for (const { pattern, desc } of sideEffectPatterns) {
if (pattern.test(topLevelCode)) {
issues.push(desc);
}
}
return {
hasSideEffects: issues.length > 0,
issues,
safe: issues.length === 0,
};
}
function extractTopLevelStatements(code) {
// Simplified: remove function/class bodies
// Real implementation would use an AST parser
return code
.replace(/(?:function|class)\s+\w+[^{]*\{[^}]*\}/gs, "")
.replace(/export\s+(function|class)/g, "$1");
}Pure Annotations and IIFE Patterns
// #__PURE__ tells bundlers a function call has no side effects
// It is safe to remove if the result is unused
// Library code
export const logger = /* #__PURE__ */ createLogger("app");
export const store = /* #__PURE__ */ createStore(initialState);
export const theme = /* #__PURE__ */ computeTheme(defaultConfig);
// If logger, store, or theme are never imported,
// the createLogger/createStore/computeTheme calls are removed
// IIFE pattern (Immediately Invoked Function Expression)
// BAD: IIFE at top level is always a side effect
const config = (() => {
const env = detectEnvironment();
return { debug: env === "dev", apiUrl: env === "prod" ? PROD_URL : DEV_URL };
})();
// GOOD: Mark IIFE as pure
const config = /* #__PURE__ */ (() => {
const env = detectEnvironment();
return { debug: env === "dev" };
})();
// Terser/uglify also supports @__PURE__
const instance = /** @__PURE__ */ new HeavyClass();Measuring Dead Code Reduction
// Build size comparison script
const { execSync } = require("child_process");
const fs = require("fs");
function measureBuildSize(config) {
execSync(`webpack --config ${config}`, { stdio: "pipe" });
const distDir = "./dist";
const files = fs.readdirSync(distDir).filter((f) => f.endsWith(".js"));
let totalSize = 0;
let totalGzip = 0;
const details = files.map((file) => {
const filePath = `${distDir}/${file}`;
const stat = fs.statSync(filePath);
const gzipSize = execSync(`gzip -c "${filePath}" | wc -c`, {
encoding: "utf8",
}).trim();
totalSize += stat.size;
totalGzip += parseInt(gzipSize);
return {
file,
rawKB: (stat.size / 1024).toFixed(1),
gzipKB: (parseInt(gzipSize) / 1024).toFixed(1),
};
});
return { details, totalKB: (totalSize / 1024).toFixed(1), totalGzipKB: (totalGzip / 1024).toFixed(1) };
}
// Compare with and without tree shaking
const withShaking = measureBuildSize("webpack.prod.js");
const withoutShaking = measureBuildSize("webpack.no-shake.js");
console.log("With tree shaking:", withShaking.totalKB, "KB");
console.log("Without tree shaking:", withoutShaking.totalKB, "KB");
const saved = (
((withoutShaking.totalKB - withShaking.totalKB) / withoutShaking.totalKB) * 100
).toFixed(1);
console.log(`Reduction: ${saved}%`);| Dead Code Source | Detection Method | Removal Strategy |
|---|---|---|
| Unused exports | Bundle analyzer, dead code detector | Tree shaking with sideEffects: false |
| Debug-only code | Feature flags, DefinePlugin | Conditional compilation |
| Legacy compatibility | Usage analysis, browser targets | browserslist + @babel/preset-env |
| Unused dependencies | depcheck, npm-check | Remove from package.json |
| Unreachable branches | Static analysis, ESLint rules | Minifier dead code elimination |
| Polyfills for modern browsers | core-js usage analysis | useBuiltIns: "usage" in Babel |
Rune AI
Key Insights
- Side effects at the top level prevent tree shaking: Move all executable code inside exported functions; never run code on module import unless it is genuinely required
- DefinePlugin enables conditional dead code removal: Replace feature flags with literal booleans at build time so the minifier can eliminate entire code branches
- Dead code detection scripts find unused exports: Scan your codebase to identify exports that no module imports, then safely remove them
- #PURE annotations mark safe-to-remove function calls: Without this annotation, bundlers assume factory function calls have side effects and keep them
- Measure before and after with bundle analysis: Use webpack-bundle-analyzer to visualize what is actually in your bundle and verify that tree shaking is working as expected
Frequently Asked Questions
How much bundle size can tree shaking typically save?
Why does my dead code still appear in the bundle?
Should I use lodash or lodash-es for tree shaking?
How do feature flags interact with tree shaking?
Can tree shaking remove unused CSS?
Conclusion
Removing dead code requires a multi-pronged approach: tree shaking for unused exports, DefinePlugin for environment-specific code, and dead code detection scripts for legacy/unreachable code. Always configure sideEffects: false in package.json and keep Babel from converting ES modules. For tree shaking fundamentals, see JavaScript Tree Shaking: A Complete Tutorial. For bundler configuration, see JavaScript Bundlers: An Advanced Architecture.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.