As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Building efficient bundle analyzers requires a systematic approach that combines multiple analysis techniques to provide actionable insights for JavaScript application optimization. I have spent considerable time developing and refining these methods, and they have proven invaluable for identifying performance bottlenecks and optimization opportunities.
Bundle Composition Analysis
The foundation of effective bundle analysis lies in understanding how modules interconnect within your application. When I first started working with bundle analyzers, I realized that simply knowing the size of individual modules wasn't enough. The real value comes from mapping the relationships between modules and understanding how code splitting affects overall performance.
class ModuleCompositionAnalyzer {
constructor(webpackStats) {
this.stats = webpackStats;
this.moduleGraph = new Map();
this.chunkGraph = new Map();
this.entryPoints = new Map();
}
analyzeComposition() {
this.buildModuleGraph();
this.analyzeChunkRelationships();
this.identifyRedundantPaths();
return {
moduleCount: this.moduleGraph.size,
chunkCount: this.chunkGraph.size,
redundantModules: this.getRedundantModules(),
entryPointAnalysis: this.analyzeEntryPoints()
};
}
buildModuleGraph() {
this.stats.modules.forEach(module => {
const moduleInfo = {
id: module.id,
name: module.name,
size: module.size,
chunks: module.chunks,
dependencies: [],
dependents: [],
depth: 0
};
// Extract dependencies from reasons
if (module.reasons) {
module.reasons.forEach(reason => {
if (reason.moduleId && reason.moduleId !== module.id) {
moduleInfo.dependencies.push(reason.moduleId);
}
});
}
this.moduleGraph.set(module.id, moduleInfo);
});
// Build reverse dependencies
this.moduleGraph.forEach((module, moduleId) => {
module.dependencies.forEach(depId => {
const depModule = this.moduleGraph.get(depId);
if (depModule) {
depModule.dependents.push(moduleId);
}
});
});
this.calculateModuleDepths();
}
calculateModuleDepths() {
const visited = new Set();
const calculating = new Set();
const calculateDepth = (moduleId) => {
if (calculating.has(moduleId)) return 0; // Circular dependency
if (visited.has(moduleId)) {
return this.moduleGraph.get(moduleId).depth;
}
calculating.add(moduleId);
const module = this.moduleGraph.get(moduleId);
if (module.dependencies.length === 0) {
module.depth = 0;
} else {
const maxDepth = Math.max(
...module.dependencies.map(depId => calculateDepth(depId))
);
module.depth = maxDepth + 1;
}
calculating.delete(moduleId);
visited.add(moduleId);
return module.depth;
};
this.moduleGraph.forEach((module, moduleId) => {
if (!visited.has(moduleId)) {
calculateDepth(moduleId);
}
});
}
analyzeChunkRelationships() {
this.stats.chunks.forEach(chunk => {
const chunkInfo = {
id: chunk.id,
names: chunk.names,
size: chunk.size,
modules: chunk.modules || [],
parents: chunk.parents || [],
children: chunk.children || [],
isEntry: chunk.entry || false
};
this.chunkGraph.set(chunk.id, chunkInfo);
});
}
identifyRedundantPaths() {
const moduleChunkMap = new Map();
// Map modules to chunks
this.chunkGraph.forEach((chunk, chunkId) => {
chunk.modules.forEach(moduleId => {
if (!moduleChunkMap.has(moduleId)) {
moduleChunkMap.set(moduleId, []);
}
moduleChunkMap.get(moduleId).push(chunkId);
});
});
// Find modules that appear in multiple chunks
this.redundantModules = [];
moduleChunkMap.forEach((chunks, moduleId) => {
if (chunks.length > 1) {
const module = this.moduleGraph.get(moduleId);
this.redundantModules.push({
moduleId,
moduleName: module ? module.name : 'Unknown',
chunks,
duplicatedSize: (chunks.length - 1) * (module ? module.size : 0)
});
}
});
}
getRedundantModules() {
return this.redundantModules.sort((a, b) => b.duplicatedSize - a.duplicatedSize);
}
analyzeEntryPoints() {
const entryAnalysis = [];
this.chunkGraph.forEach((chunk, chunkId) => {
if (chunk.isEntry) {
const analysis = this.analyzeEntryPoint(chunkId);
entryAnalysis.push({
chunkId,
names: chunk.names,
...analysis
});
}
});
return entryAnalysis;
}
analyzeEntryPoint(entryChunkId) {
const visited = new Set();
const modulesByDepth = new Map();
let totalSize = 0;
const traverse = (chunkId) => {
if (visited.has(chunkId)) return;
visited.add(chunkId);
const chunk = this.chunkGraph.get(chunkId);
if (!chunk) return;
chunk.modules.forEach(moduleId => {
const module = this.moduleGraph.get(moduleId);
if (module) {
totalSize += module.size;
if (!modulesByDepth.has(module.depth)) {
modulesByDepth.set(module.depth, []);
}
modulesByDepth.get(module.depth).push(module);
}
});
chunk.children.forEach(childId => traverse(childId));
};
traverse(entryChunkId);
return {
totalSize,
moduleCount: visited.size,
maxDepth: Math.max(...modulesByDepth.keys()),
modulesByDepth: Object.fromEntries(modulesByDepth)
};
}
}
Understanding module composition helps identify opportunities for better code splitting and reduces redundant code across different entry points. I have found that visualizing these relationships often reveals unexpected dependencies that can be optimized.
Memory Usage Profiling
Memory profiling during bundle execution provides critical insights into runtime performance characteristics. Traditional bundle analyzers focus on static file sizes, but memory usage patterns during execution often tell a different story about actual performance impact.
class MemoryProfiler {
constructor(options = {}) {
this.options = {
sampleInterval: options.sampleInterval || 100,
trackAllocations: options.trackAllocations || true,
maxSamples: options.maxSamples || 1000,
...options
};
this.samples = [];
this.allocations = new Map();
this.leakCandidates = [];
this.isProfileing = false;
}
startProfiling() {
if (this.isProfileing) return;
this.isProfileing = true;
this.samples = [];
this.startTime = performance.now();
// Start memory sampling
this.samplingInterval = setInterval(() => {
this.takeSample();
}, this.options.sampleInterval);
// Track object allocations if supported
if (this.options.trackAllocations && window.performance.measureUserAgentSpecificMemory) {
this.trackObjectAllocations();
}
// Monitor garbage collection if available
if (window.performance.measureUserAgentSpecificMemory) {
this.monitorGarbageCollection();
}
}
stopProfiling() {
if (!this.isProfileing) return;
this.isProfileing = false;
clearInterval(this.samplingInterval);
const endTime = performance.now();
const duration = endTime - this.startTime;
return this.generateReport(duration);
}
takeSample() {
const sample = {
timestamp: performance.now() - this.startTime,
memory: this.getMemoryInfo(),
domNodes: document.querySelectorAll('*').length,
eventListeners: this.countEventListeners()
};
this.samples.push(sample);
// Keep only the most recent samples
if (this.samples.length > this.options.maxSamples) {
this.samples.shift();
}
// Detect potential memory leaks
this.detectMemoryLeaks(sample);
}
getMemoryInfo() {
if (performance.memory) {
return {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit
};
}
// Fallback estimation
return {
used: this.estimateMemoryUsage(),
total: 0,
limit: 0
};
}
estimateMemoryUsage() {
// Rough estimation based on DOM size and stored data
const domSize = document.documentElement.outerHTML.length;
const storageSize = this.calculateStorageSize();
return domSize + storageSize;
}
calculateStorageSize() {
let size = 0;
// Calculate localStorage size
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
size += localStorage[key].length + key.length;
}
}
// Calculate sessionStorage size
for (let key in sessionStorage) {
if (sessionStorage.hasOwnProperty(key)) {
size += sessionStorage[key].length + key.length;
}
}
return size;
}
countEventListeners() {
// This is a simplified count - actual implementation would need
// to track listeners added during profiling
return document.querySelectorAll('[onclick], [onload], [onchange]').length;
}
detectMemoryLeaks(currentSample) {
if (this.samples.length < 10) return;
const recentSamples = this.samples.slice(-10);
const memoryGrowth = recentSamples.map((sample, index) => {
if (index === 0) return 0;
return sample.memory.used - recentSamples[index - 1].memory.used;
});
const averageGrowth = memoryGrowth.reduce((sum, growth) => sum + growth, 0) / memoryGrowth.length;
// Detect consistent memory growth
if (averageGrowth > 1024 * 100) { // 100KB average growth
const growthRate = averageGrowth / this.options.sampleInterval;
this.leakCandidates.push({
timestamp: currentSample.timestamp,
growthRate,
memoryUsed: currentSample.memory.used,
suspectedCause: this.identifyLeakCause(currentSample)
});
}
}
identifyLeakCause(sample) {
const causes = [];
// Check DOM node growth
if (this.samples.length > 1) {
const previousSample = this.samples[this.samples.length - 2];
const domGrowth = sample.domNodes - previousSample.domNodes;
if (domGrowth > 10) {
causes.push(`DOM nodes increased by ${domGrowth}`);
}
}
// Check event listener growth
const listenerGrowth = sample.eventListeners - (this.samples[0]?.eventListeners || 0);
if (listenerGrowth > 5) {
causes.push(`Event listeners increased by ${listenerGrowth}`);
}
return causes.length > 0 ? causes.join(', ') : 'Unknown cause';
}
trackObjectAllocations() {
// Modern browsers might support allocation tracking
if (typeof FinalizationRegistry !== 'undefined') {
this.allocationRegistry = new FinalizationRegistry((heldValue) => {
this.recordDeallocation(heldValue);
});
}
}
recordAllocation(object, metadata) {
if (this.allocationRegistry) {
const allocationInfo = {
timestamp: performance.now() - this.startTime,
size: this.estimateObjectSize(object),
type: metadata.type || 'unknown',
stackTrace: this.captureStackTrace()
};
this.allocations.set(object, allocationInfo);
this.allocationRegistry.register(object, allocationInfo);
}
}
recordDeallocation(allocationInfo) {
// Track when objects are garbage collected
allocationInfo.deallocatedAt = performance.now() - this.startTime;
}
estimateObjectSize(obj) {
if (obj === null || obj === undefined) return 0;
const type = typeof obj;
switch (type) {
case 'boolean': return 4;
case 'number': return 8;
case 'string': return obj.length * 2;
case 'object':
if (Array.isArray(obj)) {
return obj.reduce((size, item) => size + this.estimateObjectSize(item), 0);
}
let size = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
size += key.length * 2 + this.estimateObjectSize(obj[key]);
}
}
return size;
default:
return 0;
}
}
captureStackTrace() {
const error = new Error();
return error.stack ? error.stack.split('\n').slice(2, 7) : [];
}
monitorGarbageCollection() {
// This would integrate with performance observers if available
if (window.PerformanceObserver) {
try {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.entryType === 'measure' && entry.name.includes('gc')) {
this.recordGCEvent(entry);
}
});
});
observer.observe({ entryTypes: ['measure'] });
} catch (error) {
console.warn('GC monitoring not available');
}
}
}
recordGCEvent(entry) {
const gcEvent = {
timestamp: entry.startTime,
duration: entry.duration,
type: entry.name
};
if (!this.gcEvents) {
this.gcEvents = [];
}
this.gcEvents.push(gcEvent);
}
generateReport(duration) {
const report = {
duration,
sampleCount: this.samples.length,
memoryAnalysis: this.analyzeMemoryUsage(),
leakDetection: this.analyzeLeaks(),
allocationAnalysis: this.analyzeAllocations(),
recommendations: this.generateRecommendations()
};
return report;
}
analyzeMemoryUsage() {
if (this.samples.length === 0) return null;
const memoryValues = this.samples.map(s => s.memory.used);
const minMemory = Math.min(...memoryValues);
const maxMemory = Math.max(...memoryValues);
const avgMemory = memoryValues.reduce((sum, val) => sum + val, 0) / memoryValues.length;
// Calculate memory growth trend
const firstQuarter = memoryValues.slice(0, Math.floor(memoryValues.length / 4));
const lastQuarter = memoryValues.slice(-Math.floor(memoryValues.length / 4));
const firstQuarterAvg = firstQuarter.reduce((sum, val) => sum + val, 0) / firstQuarter.length;
const lastQuarterAvg = lastQuarter.reduce((sum, val) => sum + val, 0) / lastQuarter.length;
const growthTrend = lastQuarterAvg - firstQuarterAvg;
return {
min: this.formatBytes(minMemory),
max: this.formatBytes(maxMemory),
average: this.formatBytes(avgMemory),
growth: this.formatBytes(growthTrend),
growthRate: (growthTrend / (this.samples[this.samples.length - 1].timestamp - this.samples[0].timestamp)) * 1000
};
}
analyzeLeaks() {
return {
potentialLeaks: this.leakCandidates.length,
totalGrowth: this.formatBytes(
this.leakCandidates.reduce((sum, leak) => sum + leak.growthRate * 1000, 0)
),
leakDetails: this.leakCandidates.map(leak => ({
timestamp: Math.round(leak.timestamp),
growthRate: this.formatBytes(leak.growthRate * 1000) + '/s',
memoryUsed: this.formatBytes(leak.memoryUsed),
cause: leak.suspectedCause
}))
};
}
analyzeAllocations() {
if (this.allocations.size === 0) return null;
const allocationsArray = Array.from(this.allocations.values());
const totalAllocated = allocationsArray.reduce((sum, alloc) => sum + alloc.size, 0);
const deallocated = allocationsArray.filter(alloc => alloc.deallocatedAt).length;
return {
totalAllocations: allocationsArray.length,
totalSize: this.formatBytes(totalAllocated),
deallocated,
stillAllocated: allocationsArray.length - deallocated,
allocationRate: allocationsArray.length / (this.samples[this.samples.length - 1].timestamp / 1000)
};
}
generateRecommendations() {
const recommendations = [];
if (this.leakCandidates.length > 0) {
recommendations.push('Potential memory leaks detected. Review event listener cleanup and object references.');
}
const memoryAnalysis = this.analyzeMemoryUsage();
if (memoryAnalysis && memoryAnalysis.growthRate > 1024) {
recommendations.push('High memory growth rate detected. Consider implementing object pooling or reducing object creation.');
}
if (this.samples.some(s => s.domNodes > 1000)) {
recommendations.push('High DOM node count detected. Consider virtual scrolling or lazy loading for large lists.');
}
return recommendations;
}
formatBytes(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
}
Memory profiling has been instrumental in identifying performance issues that static analysis cannot detect. The combination of heap monitoring and allocation tracking provides comprehensive insights into runtime behavior.
Tree Shaking Verification
Effective tree shaking verification goes beyond checking if unused exports are removed. Modern applications often have complex side effects and dynamic imports that can interfere with dead code elimination. I have developed techniques to systematically identify these issues.
javascript
class TreeShakingAnalyzer {
constructor(sourceCode, options = {}) {
this.sourceCode = sourceCode;
this.options = {
checkSideEffects: options.checkSideEffects !== false,
analyzeDeepImports: options.analyzeDeepImports !== false,
trackDynamicImports: options.trackDynamicImports !== false,
...options
};
this.exportMap = new Map();
this.importMap = new Map();
this.sideEffects = [];
this.unusedExports = [];
this.problematicPatterns = [];
}
analyze() {
this.parseExports();
this.parseImports();
this.identifyUsedExports();
this.detectSideEffects();
this.findProblematicPatterns();
return this.generateShakingReport();
}
parseExports() {
const exportPatterns = [
/export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w+)/g,
/export\s*\{\s*([^}]+)\s*\}/g,
/export\s+\*\s+from\s+['"]([^'"]+)['"]/g,
/export\s*\{\s*([^}]+)\s*\}\s*from\s+['"]([^'"]+)['"]/g
];
exportPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(this.sourceCode)) !== null) {
this.processExportMatch(match, pattern);
}
});
}
processExportMatch(match, pattern) {
const fullMatch = match[0];
const location = this.getLineNumber(match.index);
if (fullMatch.includes('export *')) {
// Re-export all
this.exportMap.set(`*:${match[1]}`, {
type: 'reexport-all',
source: match[1],
location,
used: false
});
} else if (fullMatch.includes('from')) {
// Named re-export
const exports = this.parseExportList(match[1]);
const source = match[2];
exports.forEach(exp => {
this.exportMap.set(exp.name, {
type: 'reexport',
source,
alias: exp.alias,
location,
used: false
});
});
} else if (match[1].includes(',')) {
// Multiple named exports
const exports = this.parseExportList(match[1]);
exports.forEach(exp => {
this.exportMap.set(exp.name, {
type: 'named',
alias: exp.alias,
location,
used: false,
definition: this.findDefinition(exp.name)
});
});
} else {
// Single export
this.exportMap.set(match[1], {
type: fullMatch.includes('default') ? 'default' : 'named',
location,
used: false,
definition: this.findDefinition(match[1])
});
}
}
parseExportList(exportString) {
return exportString.split(',').map(item => {
const trimmed = item.trim();
const asIndex = trimmed.indexOf(' as ');
if (asIndex !== -1) {
return {
name: trimmed.substring(0, asIndex).trim(),
alias: trimmed.substring(asIndex + 4).trim()
};
}
return { name: trimmed, alias: null };
});
}
parseImports() {
const importPatterns = [
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
/import\s*\{\s*([^}]+)\s*\}\s*from\s+['"]([^'"]+)['"]/g,
/import\s*\*\s*as\s*(\w+)\s*from\s+['"]([^'"]+)['"]/g,
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
];
importPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(this.sourceCode)) !== null) {
this.processImportMatch(match, pattern);
}
});
}
processImportMatch(match, pattern) {
const fullMatch = match[0];
const location = this.getLineNumber(match.index);
if (fullMatch.includes('import(')) {
// Dynamic import
this.importMap.set(`dynamic:${match[1]}`, {
type: 'dynamic',
source: match[1],
location,
used: true // Dynamic imports are always considered used
});
} else if (fullMatch.includes('* as')) {
// Namespace import
this.importMap.set(match[1], {
type: 'namespace',
source: match[2],
location,
used: this.isIdentifierUsed(match[1])
});
} else if (match[1] && match[1].includes(',')) {
// Named imports
const imports = this.parseImportList(match[1]);
const source = match[2];
imports.forEach(imp => {
this.importMap.set(imp.local, {
type: 'named',
source,
imported: imp.imported,
location,
used: this.isIdentifierUsed(imp.local)
});
});
} else {
// Default import
this.importMap.set(match[1], {
type: 'default',
source: match[2],
location,
used: this.isIdentifierUsed(match[1])
});
}
}
parseImportList(importString) {
return importString.split(',').map(item => {
const trimmed = item.trim();
const asIndex = trimmed.indexOf(' as ');
if (asIndex !== -1) {
return {
imported: trimmed.substring(0, asIndex).trim(),
local: trimmed.substring(asIndex + 4).trim()
};
}
return { imported: trimmed, local: trimmed };
});
}
identifyUsedExports() {
// Mark exports as used based on imports in other modules
this.importMap.forEach((importInfo, importName) => {
if (importInfo.used && importInfo.type !== 'dynamic') {
const exportKey = importInfo.imported || importName;
if (this.exportMap.has(exportKey)) {
this.exportMap.get(exportKey).used = true;
}
}
});
// Check for usage in the same module
this.exportMap.forEach((exportInfo, exportName) => {
if (!exportInfo.used) {
exportInfo.used = this.isIdentifierUsed(exportName);
}
});
}
isIdentifierUsed(identifier) {
// Simple usage detection - could be more sophisticated
const usagePattern = new RegExp(`\\b${identifier}\\b`, 'g');
const matches = this.sourceCode.match(usagePattern);
// If found more than once (definition + usage), consider it used
return matches && matches.length > 1;
}
detectSideEffects() {
if (!this.options.checkSideEffects) return;
const sideEffectPatterns = [
{
pattern: /console\.(log|warn|error|info)/g,
type: 'console',
severity: 'low'
},
{
pattern: /window\.\w+\s*=/g,
type: 'global-assignment',
severity: 'high'
},
{
pattern: /document\.(getElementById|querySelector|addEventListener)/g,
type: 'dom-interaction',
severity: 'medium'
},
{
pattern: /localStorage\.|sessionStorage\./g,
type: 'storage-access',
severity: 'medium'
},
{
pattern: /fetch\(|XMLHttpRequest/g,
type: 'network-request',
severity: 'high'
},
{
pattern: /setInterval\(|setTimeout\(/g,
type: 'timer',
severity: 'medium'
}
];
sideEffectPatterns.forEach(({ pattern, type, severity }) => {
let match;
while ((match = pattern.exec(this.sourceCode)) !== null) {
this.sideEffects.push({
type,
severity,
location: this.getLineNumber(match.index),
code: this.getCodeContext(match.index),
impact: this.assessSideEffectImpact(type, match[0])
});
}
});
}
assessSideEffectImpact(type, code) {
switch (type) {
case 'global-assignment':
return 'Prevents tree shaking of entire module';
case 'network-request':
return 'May cause unwanted network activity';
case 'dom-interaction':
return 'May cause DOM manipulation side effects';
case 'timer':
return 'May create persistent timers';
default:
return 'May have runtime side effects';
}
}
findProblematicPatterns() {
const patterns = [
{
name: 'default-export-object',
pattern: /export\s+default\s*\{[\s\S]*?\}/g,
issue: 'Default object exports prevent individual property tree shaking'
},
{
name: 'destructuring-reexport',
pattern: /export\s*\{\s*\.\.\.(\w+)\s*\}/g,
issue: 'Spread operator in exports prevents static analysis'
},
{
name: 'conditional-export',
pattern: /if\s*\([\s\S]*?\)\s*\{[\s\S]*?export/g,
issue: 'Conditional exports prevent tree shaking'
},
{
name: 'computed-property-export',
pattern: /export\s*\{\s*\[[\s\S]*?\]\s*:/g,
issue: 'Computed property names prevent static analysis'
}
];
patterns.forEach(({ name, pattern, issue }) => {
---
📘 **Checkout my [latest ebook](https://youtu.be/WpR6F4ky4uM) for free on my channel!**
Be sure to **like**, **share**, **comment**, and **subscribe** to the channel!
---
## 101 Books
**101 Books** is an AI-driven publishing company co-founded by author **Aarav Joshi**. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as **$4**—making quality knowledge accessible to everyone.
Check out our book **[Golang Clean Code](https://www.amazon.com/dp/B0DQQF9K3Z)** available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for **Aarav Joshi** to find more of our titles. Use the provided link to enjoy **special discounts**!
## Our Creations
Be sure to check out our creations:
**[Investor Central](https://www.investorcentral.co.uk/)** | **[Investor Central Spanish](https://spanish.investorcentral.co.uk/)** | **[Investor Central German](https://german.investorcentral.co.uk/)** | **[Smart Living](https://smartliving.investorcentral.co.uk/)** | **[Epochs & Echoes](https://epochsandechoes.com/)** | **[Puzzling Mysteries](https://www.puzzlingmysteries.com/)** | **[Hindutva](http://hindutva.epochsandechoes.com/)** | **[Elite Dev](https://elitedev.in/)** | **[JS Schools](https://jsschools.com/)**
---
### We are on Medium
**[Tech Koala Insights](https://techkoalainsights.com/)** | **[Epochs & Echoes World](https://world.epochsandechoes.com/)** | **[Investor Central Medium](https://medium.investorcentral.co.uk/)** | **[Puzzling Mysteries Medium](https://medium.com/puzzling-mysteries)** | **[Science & Epochs Medium](https://science.epochsandechoes.com/)** | **[Modern Hindutva](https://modernhindutva.substack.com/)**
Top comments (0)