Measuring Bundle Size Impact of Shared Dependencies #
Problem Statement #
Enterprise teams struggle to accurately quantify the exact kilobyte overhead introduced by shared dependencies in Module Federation. Without precise metrics, architects either over-share (triggering version conflicts and bloated initial chunks) or under-share (duplicating heavy libraries across remote applications). This ambiguity directly impacts Core Micro-Frontend Architecture & Tradeoffs by obscuring the true cost-benefit ratio of federated sharing strategies and complicating performance budgeting. Unmeasured payloads degrade Time to First Byte (TTFB), inflate cache invalidation costs, and introduce unpredictable cold-start latency across distributed teams.
Root Cause Analysis #
Webpack’s default shared configuration lacks transparent reporting, often aggregating shared modules into the host chunk during static analysis. Traditional build analyzers mask the true distribution across remotes, conflating compile-time bundling with runtime network delivery. Additionally, implicit sharing without explicit version pinning leads to silent duplication. Understanding how to isolate these metrics is foundational to Avoiding Bundle Duplication, as unmeasured shared dependencies frequently become the primary source of hidden payload bloat and cross-team coupling. The core failure mode stems from Webpack’s chunk splitting algorithm treating shared modules as implicit graph roots, which obscures transitive peer dependency boundaries and prevents accurate network-level attribution.
Step-by-Step Resolution #
- Isolate Dependency Graphs: Run
depcheck --ignore-dirs=dist,node_modulesandmadge --circular --json src/to map explicit imports versus transitive dependencies. Flag any library imported by multiple remotes but absent from thesharedmanifest. - Enforce Strict Sharing Policies: Configure
ModuleFederationPluginwith explicitsharedscopes,singleton: true, andeager: falseto guarantee lazy evaluation and prevent premature host chunk inflation. - Extract Granular Build Stats: Generate
stats.jsonviawebpack-stats-pluginand implement a custom Node.js parser to isolate chunk attribution for shared modules, filtering out host-only scaffolding. - Generate Differential Reports: Compare baseline monolithic builds against federated configurations using delta analysis on
stats.modulesandstats.chunks. Track byte deltas per shared library across major version bumps. - Automate CI/CD Budgets: Integrate size assertions into pipelines with hard thresholds for shared dependency regression. Fail builds when shared chunk growth exceeds ±3% or when duplicate instances are detected across remote entry points.
Configuration & Code Fix #
webpack.config.js #
Explicitly define shared dependencies with strict version constraints and lazy loading behavior to prevent eager chunk inflation.
const { ModuleFederationPlugin } = require('webpack').container;
const { StatsWriterPlugin } = require('webpack-stats-plugin');
module.exports = {
// ... standard entry/output/module config
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
remoteA: 'remoteA@http://localhost:3001/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
eager: false,
},
'@emotion/react': {
singleton: true,
requiredVersion: '^11.11.0',
eager: false,
},
},
}),
new StatsWriterPlugin({
filename: 'stats.json',
stats: {
all: false,
modules: true,
chunks: true,
chunkModules: true,
reasons: true,
assets: true,
sizes: true,
ids: true,
},
}),
],
};
scripts/analyze-shared-bundle.js #
Parses stats.json to isolate shared chunk sizes, map modules to chunks, and detect transitive/peer dependency leakage.
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const statsPath = path.resolve(__dirname, '../dist/stats.json');
if (!fs.existsSync(statsPath)) {
throw new Error('stats.json not found. Run webpack with StatsWriterPlugin first.');
}
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
// 1. Build lookup maps for accurate module-to-chunk attribution
const moduleToChunks = new Map();
const chunkToModules = new Map();
stats.modules.forEach(mod => {
if (mod.chunks) {
mod.chunks.forEach(chunkId => {
if (!moduleToChunks.has(mod.id)) moduleToChunks.set(mod.id, new Set());
moduleToChunks.get(mod.id).add(chunkId);
if (!chunkToModules.has(chunkId)) chunkToModules.set(chunkId, new Set());
chunkToModules.get(chunkId).add(mod.id);
});
}
});
// 2. Identify explicit shared modules by path matching
const SHARED_PATTERNS = [/node_modules\/react\b/, /node_modules\/lodash\b/, /node_modules\/@emotion/];
const sharedModuleIds = new Set();
stats.modules.forEach(mod => {
if (mod.nameForCondition && SHARED_PATTERNS.some(p => p.test(mod.nameForCondition))) {
sharedModuleIds.add(mod.id);
}
});
// 3. Detect transitive/peer dependencies pulled into shared chunks
const transitiveShared = stats.modules.filter(mod => {
if (sharedModuleIds.has(mod.id)) return false;
return mod.reasons?.some(reason => sharedModuleIds.has(reason.moduleId));
});
// 4. Calculate exact byte attribution
let sharedChunkSize = 0;
let remoteOnlySize = 0;
const sharedChunkIds = new Set();
stats.modules.forEach(mod => {
if (sharedModuleIds.has(mod.id) || transitiveShared.includes(mod)) {
mod.chunks?.forEach(c => sharedChunkIds.add(c));
}
});
stats.chunks.forEach(chunk => {
const isShared = sharedChunkIds.has(chunk.id);
if (isShared) {
sharedChunkSize += chunk.size;
} else {
remoteOnlySize += chunk.size;
}
});
console.log('=== Bundle Size Attribution ===');
console.log(`Shared Dependencies (Direct + Transitive): ${(sharedChunkSize / 1024).toFixed(2)} KB`);
console.log(`Remote-Specific Assets: ${(remoteOnlySize / 1024).toFixed(2)} KB`);
console.log(`Transitive Modules Detected: ${transitiveShared.length}`);
console.log(`Shared Chunks: ${sharedChunkIds.size}`);
Validation & Testing Steps #
- Verify Chunk Isolation: Execute
webpack --json > stats.jsonand run the analysis script. Confirm that shared modules resolve to dedicated chunk IDs (e.g.,shared-react.js) and are absent from remote-specific entry chunks. - Cross-Validate with Source Maps: Run
npx source-map-explorer dist/*.jsto visually confirm shared dependencies are not duplicated across remote entry points. Look for overlapping trees; any duplication indicates missingsingletonorrequiredVersionconstraints. - Network Waterfall Audit: Open Chrome DevTools → Network tab. Filter by
JSand reload the host app with all remotes mounted. Verify a single304or200request fetches the shared chunk. Multiple identical requests indicate cache-busting misconfiguration or runtime version fallbacks. - CI/CD Regression Enforcement: Integrate automated assertions into your pipeline:
# .github/workflows/bundle-budget.yml
node scripts/analyze-shared-bundle.js | grep "Shared Dependencies" | awk '{print $NF}' | xargs -I {} bash -c 'if (( $(echo "{} > 150.00" | bc -l) )); then echo "FAIL: Shared chunk exceeds 150KB budget"; exit 1; fi'
Rollback & Fallback Considerations #
If shared chunk extraction causes hydration mismatches or increases Time to Interactive (TTI), revert to eager: true for critical UI dependencies to force synchronous loading. Implement dynamic import fallbacks with runtime version mismatch detection to gracefully degrade to remote-specific bundles when requiredVersion constraints fail. Maintain a dual-build strategy during transition phases, shipping both federated and standalone artifacts to isolate regression vectors. Continuously monitor runtime error rates, LCP, and FCP metrics to validate rollback decisions. Document explicit fallback triggers and establish clear ownership for dependency version alignment across teams to prevent cascading module resolution failures in production.