Skip to content

Commit 8043d2f

Browse files
author
Developer
committed
feat: implement bundle size optimization and code-splitting
- Add lazy loading wrapper for MediaProcessor with dynamic imports - Create separate export paths for granular imports: - /core: Core functionality without media (lighter bundle) - /media: Media processing only (can be loaded on-demand) - Enable tree-shaking with sideEffects: false in package.json - Add bundle analysis tool for monitoring sizes - Fix TypeScript errors: add memoryInfo to BrowserCapabilities - Remove unreachable code from WASM module - Update test files to match new type definitions Bundle size: 69.72 KB gzipped (media module is 29% of total) All 284 tests passing.
1 parent 342cffa commit 8043d2f

File tree

14 files changed

+504
-34
lines changed

14 files changed

+504
-34
lines changed

docs/IMPLEMENTATION.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,10 @@
296296
- [x] Add real image test fixtures ✅
297297
- [x] Validate against various image formats (JPEG, PNG, GIF, BMP, WebP) ✅
298298
- [ ] Browser compatibility testing (requires browser environment)
299-
- [ ] Bundle size optimization
300-
- [ ] Ensure WASM module is code-split properly
301-
- [ ] Optimize for tree-shaking
302-
- [ ] Measure and optimize bundle impact
299+
- [x] Bundle size optimization
300+
- [x] Ensure WASM module is code-split properly (lazy loading implemented) ✅
301+
- [x] Optimize for tree-shaking (sideEffects: false added) ✅
302+
- [x] Measure and optimize bundle impact (69.72 KB gzipped total) ✅
303303

304304
### Phase 6: Advanced Media Processing (Design Doc 2, Grant Month 5)
305305

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@
66
"main": "./dist/src/index.js",
77
"module": "./dist/src/index.js",
88
"types": "./dist/src/index.d.ts",
9+
"sideEffects": false,
910
"exports": {
1011
".": {
1112
"types": "./dist/src/index.d.ts",
1213
"import": "./dist/src/index.js",
1314
"default": "./dist/src/index.js"
1415
},
16+
"./core": {
17+
"types": "./dist/src/exports/core.d.ts",
18+
"import": "./dist/src/exports/core.js",
19+
"default": "./dist/src/exports/core.js"
20+
},
21+
"./media": {
22+
"types": "./dist/src/exports/media.d.ts",
23+
"import": "./dist/src/exports/media.js",
24+
"default": "./dist/src/exports/media.js"
25+
},
1526
"./dist/*": "./dist/*"
1627
},
1728
"scripts": {

scripts/analyze-bundle.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Bundle size analysis script
5+
* Measures and reports the size of different build outputs
6+
*/
7+
8+
import fs from 'fs';
9+
import path from 'path';
10+
import { fileURLToPath } from 'url';
11+
import { execSync } from 'child_process';
12+
import zlib from 'zlib';
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = path.dirname(__filename);
16+
const rootDir = path.join(__dirname, '..');
17+
const distDir = path.join(rootDir, 'dist');
18+
19+
/**
20+
* Get file size in bytes
21+
*/
22+
function getFileSize(filePath) {
23+
try {
24+
const stats = fs.statSync(filePath);
25+
return stats.size;
26+
} catch {
27+
return 0;
28+
}
29+
}
30+
31+
/**
32+
* Get gzipped size
33+
*/
34+
function getGzippedSize(filePath) {
35+
try {
36+
const content = fs.readFileSync(filePath);
37+
const gzipped = zlib.gzipSync(content);
38+
return gzipped.length;
39+
} catch {
40+
return 0;
41+
}
42+
}
43+
44+
/**
45+
* Format bytes to human readable
46+
*/
47+
function formatBytes(bytes) {
48+
if (bytes === 0) return '0 B';
49+
const k = 1024;
50+
const sizes = ['B', 'KB', 'MB', 'GB'];
51+
const i = Math.floor(Math.log(bytes) / Math.log(k));
52+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
53+
}
54+
55+
/**
56+
* Analyze a directory
57+
*/
58+
function analyzeDirectory(dirPath, name) {
59+
const files = [];
60+
let totalSize = 0;
61+
let totalGzipped = 0;
62+
63+
function walkDir(dir) {
64+
if (!fs.existsSync(dir)) return;
65+
66+
const items = fs.readdirSync(dir);
67+
for (const item of items) {
68+
const fullPath = path.join(dir, item);
69+
const stat = fs.statSync(fullPath);
70+
71+
if (stat.isDirectory()) {
72+
walkDir(fullPath);
73+
} else if (item.endsWith('.js')) {
74+
const size = getFileSize(fullPath);
75+
const gzipped = getGzippedSize(fullPath);
76+
const relative = path.relative(distDir, fullPath);
77+
78+
files.push({
79+
path: relative,
80+
size,
81+
gzipped
82+
});
83+
84+
totalSize += size;
85+
totalGzipped += gzipped;
86+
}
87+
}
88+
}
89+
90+
walkDir(dirPath);
91+
92+
return {
93+
name,
94+
files,
95+
totalSize,
96+
totalGzipped
97+
};
98+
}
99+
100+
/**
101+
* Main analysis
102+
*/
103+
function analyze() {
104+
console.log('📊 Bundle Size Analysis\n');
105+
console.log('=' .repeat(60));
106+
107+
// Build the project first
108+
console.log('Building project...');
109+
try {
110+
execSync('npm run build', { cwd: rootDir, stdio: 'pipe' });
111+
console.log('✅ Build complete\n');
112+
} catch (error) {
113+
console.error('❌ Build failed:', error.message);
114+
process.exit(1);
115+
}
116+
117+
// Analyze different parts
118+
const analyses = [
119+
analyzeDirectory(path.join(distDir, 'src'), 'Full Bundle'),
120+
analyzeDirectory(path.join(distDir, 'src', 'media'), 'Media Module'),
121+
analyzeDirectory(path.join(distDir, 'src', 'fs'), 'File System'),
122+
analyzeDirectory(path.join(distDir, 'src', 'api'), 'API Module'),
123+
analyzeDirectory(path.join(distDir, 'src', 'node'), 'Node Module'),
124+
analyzeDirectory(path.join(distDir, 'src', 'identity'), 'Identity Module')
125+
];
126+
127+
// Print results
128+
for (const analysis of analyses) {
129+
console.log(`\n📦 ${analysis.name}`);
130+
console.log('-'.repeat(40));
131+
132+
if (analysis.files.length === 0) {
133+
console.log('No files found');
134+
continue;
135+
}
136+
137+
// Sort files by size
138+
const topFiles = analysis.files
139+
.sort((a, b) => b.size - a.size)
140+
.slice(0, 5);
141+
142+
console.log('Top files:');
143+
for (const file of topFiles) {
144+
console.log(` ${file.path}`);
145+
console.log(` Raw: ${formatBytes(file.size)} | Gzipped: ${formatBytes(file.gzipped)}`);
146+
}
147+
148+
console.log(`\nTotal: ${formatBytes(analysis.totalSize)} (${formatBytes(analysis.totalGzipped)} gzipped)`);
149+
console.log(`Files: ${analysis.files.length}`);
150+
}
151+
152+
// Bundle size recommendations
153+
console.log('\n' + '='.repeat(60));
154+
console.log('📈 Size Optimization Recommendations:\n');
155+
156+
const fullBundle = analyses[0];
157+
const mediaModule = analyses[1];
158+
159+
const mediaPercentage = ((mediaModule.totalSize / fullBundle.totalSize) * 100).toFixed(1);
160+
161+
console.log(`• Media module is ${mediaPercentage}% of total bundle`);
162+
163+
if (mediaModule.totalSize > 50000) {
164+
console.log(` ⚠️ Consider lazy-loading media features (currently ${formatBytes(mediaModule.totalSize)})`);
165+
} else {
166+
console.log(` ✅ Media module size is reasonable`);
167+
}
168+
169+
if (fullBundle.totalGzipped > 200000) {
170+
console.log(`• ⚠️ Bundle size exceeds 200KB gzipped (${formatBytes(fullBundle.totalGzipped)})`);
171+
console.log(' Consider:');
172+
console.log(' - Code splitting with dynamic imports');
173+
console.log(' - Tree shaking unused exports');
174+
console.log(' - Minification in production');
175+
} else {
176+
console.log(`• ✅ Bundle size is within limits (${formatBytes(fullBundle.totalGzipped)} gzipped)`);
177+
}
178+
179+
// Export paths analysis
180+
console.log('\n📤 Export Paths:');
181+
const exportPaths = [
182+
{ path: 'Main (index.js)', file: path.join(distDir, 'src', 'index.js') },
183+
{ path: 'Core only', file: path.join(distDir, 'src', 'exports', 'core.js') },
184+
{ path: 'Media only', file: path.join(distDir, 'src', 'exports', 'media.js') }
185+
];
186+
187+
for (const exp of exportPaths) {
188+
const size = getFileSize(exp.file);
189+
const gzipped = getGzippedSize(exp.file);
190+
if (size > 0) {
191+
console.log(` ${exp.path}: ${formatBytes(size)} (${formatBytes(gzipped)} gzipped)`);
192+
}
193+
}
194+
195+
console.log('\n✨ Analysis complete!');
196+
}
197+
198+
// Run analysis
199+
analyze();

scripts/fix-test-types.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Fix missing memoryLimit and memoryInfo in test files
5+
*/
6+
7+
import fs from 'fs';
8+
import path from 'path';
9+
import { fileURLToPath } from 'url';
10+
11+
const __filename = fileURLToPath(import.meta.url);
12+
const __dirname = path.dirname(__filename);
13+
14+
const testFiles = [
15+
'../test/media/media-processor.test.ts',
16+
'../test/media/wasm-progress.test.ts',
17+
'../test/media/browser-compat.test.ts',
18+
'../test/media/browser-compat-integration.test.ts'
19+
];
20+
21+
testFiles.forEach(file => {
22+
const filePath = path.join(__dirname, file);
23+
if (!fs.existsSync(filePath)) {
24+
console.log(`File not found: ${filePath}`);
25+
return;
26+
}
27+
28+
let content = fs.readFileSync(filePath, 'utf-8');
29+
30+
// Fix missing memoryLimit - add default 1024
31+
content = content.replace(
32+
/memoryInfo: false,\n(\s+)performanceAPI: true/g,
33+
'memoryInfo: false,\n$1performanceAPI: true,\n$1memoryLimit: 1024'
34+
);
35+
36+
// Also fix cases where memoryLimit exists but memoryInfo is missing
37+
content = content.replace(
38+
/memoryLimit: (\d+),\n(\s+)performanceAPI: (true|false)/g,
39+
'memoryLimit: $1,\n$2performanceAPI: $3,\n$2memoryInfo: false'
40+
);
41+
42+
// Fix cases where both are missing entirely
43+
content = content.replace(
44+
/performanceAPI: (true|false)\n(\s+)\}/g,
45+
'performanceAPI: $1,\n$2memoryLimit: 1024,\n$2memoryInfo: false\n$2}'
46+
);
47+
48+
fs.writeFileSync(filePath, content, 'utf-8');
49+
console.log(`Fixed: ${file}`);
50+
});
51+
52+
console.log('Done fixing test types');

src/exports/core.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Core S5.js exports without media processing
3+
* Lighter bundle for applications that don't need media features
4+
*/
5+
6+
// Main S5 classes
7+
export { S5 } from '../s5.js';
8+
export { FS5 } from '../fs/fs5.js';
9+
export { S5UserIdentity } from '../identity/identity.js';
10+
export { S5Node } from '../node/node.js';
11+
export { S5APIInterface } from '../api/s5.js';
12+
export { CryptoImplementation } from '../api/crypto.js';
13+
export { JSCryptoImplementation } from '../api/crypto/js.js';
14+
15+
// Export utility classes
16+
export { DirectoryWalker } from '../fs/utils/walker.js';
17+
export { BatchOperations } from '../fs/utils/batch.js';
18+
19+
// Export core types
20+
export type {
21+
DirV1,
22+
FileRef,
23+
DirRef,
24+
DirLink,
25+
PutOptions,
26+
GetOptions,
27+
ListOptions,
28+
ListResult,
29+
CursorData
30+
} from '../fs/dirv1/types.js';
31+
32+
// Export utility types
33+
export type {
34+
WalkOptions,
35+
WalkResult,
36+
WalkStats
37+
} from '../fs/utils/walker.js';
38+
39+
export type {
40+
BatchOptions,
41+
BatchProgress,
42+
BatchResult
43+
} from '../fs/utils/batch.js';

src/exports/media.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Media processing exports
3+
* Separate entry point for media-related functionality
4+
*/
5+
6+
// Export lazy-loaded versions for code-splitting
7+
export {
8+
MediaProcessorLazy as MediaProcessor,
9+
CanvasMetadataExtractorLazy as CanvasMetadataExtractor,
10+
WASMModuleLazy as WASMModule
11+
} from '../media/index.lazy.js';
12+
13+
// Export browser compatibility utilities
14+
export { BrowserCompat } from '../media/compat/browser.js';
15+
16+
// Export all media types
17+
export type {
18+
ImageMetadata,
19+
MediaOptions,
20+
InitializeOptions,
21+
ImageFormat,
22+
ColorSpace,
23+
ExifData,
24+
HistogramData,
25+
DominantColor,
26+
AspectRatio,
27+
Orientation,
28+
ProcessingSpeed,
29+
SamplingStrategy,
30+
BrowserCapabilities,
31+
ProcessingStrategy,
32+
WASMModule as WASMModuleType
33+
} from '../media/types.js';

0 commit comments

Comments
 (0)