optimizations

This commit is contained in:
proddy
2025-10-18 17:14:53 +02:00
parent 5f79f0848f
commit 32193e3c62
3 changed files with 493 additions and 183 deletions

View File

@@ -4,6 +4,7 @@ import {
existsSync, existsSync,
readFileSync, readFileSync,
readdirSync, readdirSync,
statSync,
unlinkSync unlinkSync
} from 'fs'; } from 'fs';
import mime from 'mime-types'; import mime from 'mime-types';
@@ -15,67 +16,79 @@ const INDENT = ' ';
const outputPath = '../src/ESP32React/WWWData.h'; const outputPath = '../src/ESP32React/WWWData.h';
const sourcePath = './dist'; const sourcePath = './dist';
const bytesPerLine = 20; const bytesPerLine = 20;
var totalSize = 0; let totalSize = 0;
let bundleStats = {
js: { count: 0, uncompressed: 0, compressed: 0 },
css: { count: 0, uncompressed: 0, compressed: 0 },
html: { count: 0, uncompressed: 0, compressed: 0 },
svg: { count: 0, uncompressed: 0, compressed: 0 },
other: { count: 0, uncompressed: 0, compressed: 0 }
};
const generateWWWClass = () => const generateWWWClass =
`typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler; () => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
// Total size is ${totalSize} bytes // Bundle Statistics:
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
// - Generated on: ${new Date().toISOString()}
class WWWData { class WWWData {
${indent}public: ${INDENT}public:
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { ${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')} ${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "${f.hash}");`).join('\n')}
${indent.repeat(2)}} ${INDENT.repeat(2)}}
}; };
`; `;
function getFilesSync(dir, files = []) { const getFilesSync = (dir, files = []) => {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => { readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name); const entryPath = resolve(dir, entry.name);
if (entry.isDirectory()) { entry.isDirectory() ? getFilesSync(entryPath, files) : files.push(entryPath);
getFilesSync(entryPath, files);
} else {
files.push(entryPath);
}
}); });
return files; return files;
} };
function cleanAndOpen(path) { const cleanAndOpen = (path) => {
if (existsSync(path)) { existsSync(path) && unlinkSync(path);
unlinkSync(path);
}
return createWriteStream(path, { flags: 'w+' }); return createWriteStream(path, { flags: 'w+' });
} };
const getFileType = (filePath) => {
const ext = filePath.split('.').pop().toLowerCase();
if (ext === 'js') return 'js';
if (ext === 'css') return 'css';
if (ext === 'html') return 'html';
if (ext === 'svg') return 'svg';
return 'other';
};
const writeFile = (relativeFilePath, buffer) => { const writeFile = (relativeFilePath, buffer) => {
const variable = 'ESP_REACT_DATA_' + fileInfo.length; const variable = `ESP_REACT_DATA_${fileInfo.length}`;
const mimeType = mime.lookup(relativeFilePath); const mimeType = mime.lookup(relativeFilePath);
var size = 0; const fileType = getFileType(relativeFilePath);
writeStream.write('const uint8_t ' + variable + '[] = {'); let size = 0;
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 }); writeStream.write(`const uint8_t ${variable}[] = {`);
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
// create sha const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
const hashSum = crypto.createHash('sha256'); const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
hashSum.update(zipBuffer);
const hash = hashSum.digest('hex');
zipBuffer.forEach((b) => { zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) { if (!(size % bytesPerLine)) {
writeStream.write('\n'); writeStream.write('\n' + INDENT);
writeStream.write(indent);
} }
writeStream.write('0x' + ('00' + b.toString(16).toUpperCase()).slice(-2) + ','); writeStream.write('0x' + b.toString(16).toUpperCase().padStart(2, '0') + ',');
size++; size++;
}); });
if (size % bytesPerLine) { size % bytesPerLine && writeStream.write('\n');
writeStream.write('\n');
}
writeStream.write('};\n\n'); writeStream.write('};\n\n');
// Update bundle statistics
bundleStats[fileType].count++;
bundleStats[fileType].uncompressed += buffer.length;
bundleStats[fileType].compressed += zipBuffer.length;
fileInfo.push({ fileInfo.push({
uri: '/' + relativeFilePath.replace(sep, '/'), uri: '/' + relativeFilePath.replace(sep, '/'),
mimeType, mimeType,
@@ -84,32 +97,52 @@ const writeFile = (relativeFilePath, buffer) => {
hash hash
}); });
// console.log(relativeFilePath + ' (size ' + size + ' bytes)');
totalSize += size; totalSize += size;
}; };
// start console.log(`Generating ${outputPath} from ${sourcePath}`);
console.log('Generating ' + outputPath + ' from ' + sourcePath);
const includes = ARDUINO_INCLUDES;
const indent = INDENT;
const fileInfo = []; const fileInfo = [];
const writeStream = cleanAndOpen(resolve(outputPath)); const writeStream = cleanAndOpen(resolve(outputPath));
// includes writeStream.write(ARDUINO_INCLUDES);
writeStream.write(includes);
// process static files
const buildPath = resolve(sourcePath); const buildPath = resolve(sourcePath);
for (const filePath of getFilesSync(buildPath)) { for (const filePath of getFilesSync(buildPath)) {
const readStream = readFileSync(filePath); writeFile(relative(buildPath, filePath), readFileSync(filePath));
const relativeFilePath = relative(buildPath, filePath);
writeFile(relativeFilePath, readStream);
} }
// add class
writeStream.write(generateWWWClass()); writeStream.write(generateWWWClass());
// end
writeStream.end(); writeStream.end();
console.log('Total size: ' + totalSize / 1000 + ' KB'); // Calculate and display bundle statistics
const totalUncompressed = Object.values(bundleStats).reduce(
(sum, stat) => sum + stat.uncompressed,
0
);
const totalCompressed = Object.values(bundleStats).reduce(
(sum, stat) => sum + stat.compressed,
0
);
const compressionRatio = (
((totalUncompressed - totalCompressed) / totalUncompressed) *
100
).toFixed(1);
console.log('\n📊 Bundle Size Analysis:');
console.log('='.repeat(50));
console.log(`Total compressed size: ${(totalSize / 1000).toFixed(1)} KB`);
console.log(`Total uncompressed size: ${(totalUncompressed / 1000).toFixed(1)} KB`);
console.log(`Compression ratio: ${compressionRatio}%`);
console.log('\n📁 File Type Breakdown:');
Object.entries(bundleStats).forEach(([type, stats]) => {
if (stats.count > 0) {
const ratio = (
((stats.uncompressed - stats.compressed) / stats.uncompressed) *
100
).toFixed(1);
console.log(
`${type.toUpperCase().padEnd(4)}: ${stats.count} files, ${(stats.uncompressed / 1000).toFixed(1)} KB → ${(stats.compressed / 1000).toFixed(1)} KB (${ratio}% compression)`
);
}
});
console.log('='.repeat(50));

View File

@@ -1,31 +1,108 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", // Target modern browsers for better performance
"target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["node"], // Optimized library selection
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node", "vite/client"],
// JavaScript handling
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "checkJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true, // Module system optimized for Vite
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"composite": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
// Emit configuration
"noEmit": true, "noEmit": true,
"useUnknownInCatchVariables": false, "declaration": false,
"declarationMap": false,
"sourceMap": false,
// React/JSX configuration
"jsx": "react-jsx", "jsx": "react-jsx",
"noImplicitAny": false, "jsxImportSource": "react",
"baseUrl": "src",
// Strict type checking for better code quality
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
// Additional checks
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"useUnknownInCatchVariables": true,
// Performance optimizations
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
// Path mapping for cleaner imports
"baseUrl": ".",
"paths": { "paths": {
"@": ["src"], "@/*": ["src/*"],
"@/*": ["src/*"] "@/components/*": ["src/components/*"],
"@/utils/*": ["src/utils/*"],
"@/types/*": ["src/types/*"],
"@/hooks/*": ["src/hooks/*"],
"@/services/*": ["src/services/*"],
"@/assets/*": ["src/assets/*"],
// Support for bare imports from src directory
"App": ["src/App"],
"AppRouting": ["src/AppRouting"],
"CustomTheme": ["src/CustomTheme"],
"SignIn": ["src/SignIn"],
"AuthenticatedRouting": ["src/AuthenticatedRouting"],
"env": ["src/env"],
"components": ["src/components"],
"contexts": ["src/contexts"],
"i18n": ["src/i18n"],
"utils": ["src/utils"],
"validators": ["src/validators"],
"types": ["src/types"],
"api": ["src/api"],
"app": ["src/app"],
// Wildcard patterns for subdirectories
"components/*": ["src/components/*"],
"contexts/*": ["src/contexts/*"],
"i18n/*": ["src/i18n/*"],
"utils/*": ["src/utils/*"],
"validators/*": ["src/validators/*"],
"types/*": ["src/types/*"],
"api/*": ["src/api/*"],
"app/*": ["src/app/*"]
} }
}, },
"include": ["src/**/*", "vite.config.ts"], "include": ["src/**/*", "vite.config.ts", "progmem-generator.js"],
"exclude": ["node_modules", "dist"] "exclude": [
"node_modules",
"dist",
"build",
".tsbuildinfo",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
],
"ts-node": {
"esm": true
}
} }

View File

@@ -1,130 +1,330 @@
import preact from '@preact/preset-vite'; import preact from '@preact/preset-vite';
import fs from 'fs';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
// import viteImagemin from 'vite-plugin-imagemin'; import { Plugin } from 'vite';
import viteImagemin from 'vite-plugin-imagemin';
import viteTsconfigPaths from 'vite-tsconfig-paths'; import viteTsconfigPaths from 'vite-tsconfig-paths';
import zlib from 'zlib';
// @ts-expect-error - mock server doesn't have type declarations
import mockServer from '../mock-api/mockServer.js'; import mockServer from '../mock-api/mockServer.js';
export default defineConfig(({ command, mode }) => { // Plugin to display bundle size information
if (command === 'serve') { const bundleSizeReporter = (): Plugin => {
console.log('Preparing for standalone build with server, mode=' + mode);
return {
plugins: [preact(), viteTsconfigPaths(), mockServer()],
server: {
open: true,
port: mode == 'production' ? 4173 : 3000,
proxy: {
'/api': {
target: 'http://localhost:3080',
changeOrigin: true,
secure: false
},
'/rest': 'http://localhost:3080',
'/gh': 'http://localhost:3080' // mock for GitHub API
}
}
};
}
if (mode === 'hosted') {
console.log('Preparing for hosted build');
return {
plugins: [preact(), viteTsconfigPaths()],
build: {
chunkSizeWarningLimit: 1024
}
};
}
console.log('Preparing for production, optimized build');
return { return {
plugins: [ name: 'bundle-size-reporter',
preact(), // eslint-disable-next-line @typescript-eslint/no-explicit-any
viteTsconfigPaths(), writeBundle(options: any, bundle: any) {
// { console.log('\n📦 Bundle Size Report:');
// ...viteImagemin({ console.log('='.repeat(50));
// verbose: false,
// gifsicle: {
// optimizationLevel: 7,
// interlaced: false
// },
// optipng: {
// optimizationLevel: 7
// },
// mozjpeg: {
// quality: 20
// },
// pngquant: {
// quality: [0.8, 0.9],
// speed: 4
// },
// svgo: {
// plugins: [
// {
// name: 'removeViewBox'
// },
// {
// name: 'removeEmptyAttrs',
// active: false
// }
// ]
// }
// }),
// enforce: 'pre'
// },
visualizer({
template: 'treemap', // or sunburst
open: false,
gzipSize: true,
brotliSize: true,
filename: '../analyse.html' // will be saved in project's root
})
],
build: { let totalSize = 0;
// target: 'es2022', const files: Array<{ name: string; size: number; gzipSize?: number }> = [];
chunkSizeWarningLimit: 1024,
minify: 'terser',
terserOptions: {
compress: {
passes: 4,
arrows: true,
drop_console: true,
drop_debugger: true,
sequences: true
},
mangle: {
// toplevel: true
// module: true
// properties: {
// regex: /^_/
// }
},
ecma: 5,
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
safari10: false,
toplevel: false
},
rollupOptions: { for (const [fileName, chunk] of Object.entries(
output: { bundle as Record<string, unknown>
manualChunks(id: string) { )) {
if (id.includes('node_modules')) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
// creating a chunk to react routes deps. Reducing the vendor chunk size if ((chunk as any).type === 'chunk' || (chunk as any).type === 'asset') {
if (id.includes('react-router')) { const filePath = path.join((options.dir as string) || 'dist', fileName);
return '@react-router'; let size = 0;
} let gzipSize = 0;
return 'vendor';
} try {
const stats = fs.statSync(filePath);
size = stats.size;
totalSize += size;
// Calculate gzip size
const fileContent = fs.readFileSync(filePath);
gzipSize = zlib.gzipSync(fileContent).length;
files.push({
name: fileName,
size,
gzipSize
});
} catch (error) {
console.warn(`Could not read file ${fileName}:`, error);
} }
} }
} }
// Sort files by size (largest first)
files.sort((a, b) => b.size - a.size);
// Display individual file sizes
files.forEach((file) => {
const sizeKB = (file.size / 1024).toFixed(2);
const gzipKB = file.gzipSize ? (file.gzipSize / 1024).toFixed(2) : 'N/A';
console.log(
`📄 ${file.name.padEnd(30)} ${sizeKB.padStart(8)} KB (${gzipKB} KB gzipped)`
);
});
console.log('='.repeat(50));
console.log(`📊 Total Bundle Size: ${(totalSize / 1024).toFixed(2)} KB`);
// Calculate and display gzip total
const totalGzipSize = files.reduce(
(sum, file) => sum + (file.gzipSize || 0),
0
);
console.log(`🗜️ Total Gzipped Size: ${(totalGzipSize / 1024).toFixed(2)} KB`);
// Show compression ratio
const compressionRatio = (
((totalSize - totalGzipSize) / totalSize) *
100
).toFixed(1);
console.log(`📈 Compression Ratio: ${compressionRatio}%`);
console.log('='.repeat(50));
} }
}; };
}); };
export default defineConfig(
({ command, mode }: { command: string; mode: string }) => {
if (command === 'serve') {
console.log('Preparing for standalone build with server, mode=' + mode);
return {
plugins: [
preact({
// Keep dev tools enabled for development
devToolsEnabled: true,
prefreshEnabled: true
}),
viteTsconfigPaths(),
bundleSizeReporter(), // Add bundle size reporting
mockServer()
],
server: {
open: true,
port: mode == 'production' ? 4173 : 3000,
proxy: {
'/api': {
target: 'http://localhost:3080',
changeOrigin: true,
secure: false
},
'/rest': 'http://localhost:3080',
'/gh': 'http://localhost:3080' // mock for GitHub API
}
},
// Optimize development builds
build: {
target: 'es2020',
minify: false, // Disable minification for faster dev builds
sourcemap: true // Enable source maps for debugging
}
};
}
if (mode === 'hosted') {
console.log('Preparing for hosted build');
return {
plugins: [
preact({
// Enable Preact optimizations for hosted build
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(),
bundleSizeReporter() // Add bundle size reporting
],
build: {
target: 'es2020',
chunkSizeWarningLimit: 512,
minify: 'terser',
cssMinify: true,
assetsInlineLimit: 4096,
terserOptions: {
compress: {
passes: 3,
drop_console: true,
drop_debugger: true,
dead_code: true,
unused: true
},
mangle: {
toplevel: true
},
ecma: 2020
},
rollupOptions: {
treeshake: {
moduleSideEffects: false
},
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
if (id.includes('preact')) {
return '@preact';
}
return 'vendor';
}
}
}
}
}
};
}
console.log('Preparing for production, optimized build');
return {
plugins: [
preact({
// Enable Preact optimizations
devToolsEnabled: false,
prefreshEnabled: false
}),
viteTsconfigPaths(),
// Enable image optimization for size reduction
{
...viteImagemin({
verbose: false,
gifsicle: {
optimizationLevel: 7,
interlaced: false
},
optipng: {
optimizationLevel: 7
},
mozjpeg: {
quality: 20
},
pngquant: {
quality: [0.8, 0.9],
speed: 4
},
svgo: {
plugins: [
{
name: 'removeViewBox'
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
}),
enforce: 'pre'
},
visualizer({
template: 'treemap', // or sunburst
open: false,
gzipSize: true,
brotliSize: true,
filename: '../analyse.html' // will be saved in project's root
}),
bundleSizeReporter() // Add bundle size reporting
],
build: {
// Target modern browsers for smaller bundles
target: 'es2020',
chunkSizeWarningLimit: 512, // Reduced warning threshold
minify: 'terser',
// Enable CSS minification
cssMinify: true,
// Optimize asset handling
assetsInlineLimit: 4096, // Inline small assets
terserOptions: {
compress: {
passes: 6, // Increased passes for better compression
arrows: true,
drop_console: true,
drop_debugger: true,
sequences: true,
// Additional aggressive compression options
dead_code: true,
hoist_funs: true,
hoist_vars: true,
if_return: true,
join_vars: true,
loops: true,
pure_getters: true,
reduce_vars: true,
side_effects: false,
switches: true,
unsafe: true,
unsafe_arrows: true,
unsafe_comps: true,
unsafe_Function: true,
unsafe_math: true,
unsafe_proto: true,
unsafe_regexp: true,
unsafe_undefined: true,
unused: true
},
mangle: {
toplevel: true, // Enable top-level mangling
module: true // Enable module mangling
// properties: {
// regex: /^_/ // Mangle properties starting with _
// }
},
ecma: 2020, // Updated to modern ECMAScript
enclose: false,
keep_classnames: false,
keep_fnames: false,
ie8: false,
module: false,
safari10: false,
toplevel: true // Enable top-level optimization
},
rollupOptions: {
// Enable tree shaking
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
tryCatchDeoptimization: false
},
output: {
// Optimize chunk naming for better caching
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks(id: string) {
if (id.includes('node_modules')) {
// More granular chunk splitting for better caching
if (id.includes('react-router')) {
return '@react-router';
}
if (id.includes('preact')) {
return '@preact';
}
if (id.includes('uuid')) {
return '@uuid';
}
if (id.includes('axios') || id.includes('fetch')) {
return '@http';
}
if (id.includes('lodash') || id.includes('ramda')) {
return '@utils';
}
return 'vendor';
}
// Split large application modules
if (id.includes('components/')) {
return 'components';
}
if (id.includes('pages/') || id.includes('routes/')) {
return 'pages';
}
return undefined;
},
// Enable source maps for debugging (optional)
sourcemap: false // Disable for production to save space
}
}
}
};
}
);