- Add 26 documentation files including test reports, deployment guides, and troubleshooting documentation - Include 3 CSV data files for trainer imports and user registration tracking - Add 43 JavaScript test files covering mobile optimization, Safari compatibility, and E2E testing - Include 18 PHP utility files for debugging, geocoding, and data analysis - Add 12 shell scripts for deployment verification, user management, and database operations - Update .gitignore with whitelist patterns for development files, documentation, and CSV data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
525 lines
No EOL
20 KiB
JavaScript
525 lines
No EOL
20 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
class CSSPerformanceAnalyzer {
|
|
constructor() {
|
|
this.performanceIssues = [];
|
|
this.optimizations = [];
|
|
this.metrics = new Map();
|
|
}
|
|
|
|
analyzePerformance() {
|
|
console.log('⚡ CSS PERFORMANCE ANALYSIS');
|
|
console.log('='.repeat(50));
|
|
|
|
const cssFiles = [
|
|
'./assets/css/hvac-common.css',
|
|
'./assets/css/hvac-dashboard.css',
|
|
'./assets/css/hvac-registration.css',
|
|
'./assets/css/hvac-certificates.css',
|
|
'./assets/css/hvac-email-attendees.css',
|
|
'./assets/css/hvac-event-summary.css',
|
|
'./assets/css/hvac-attendee-profile.css',
|
|
'./assets/css/hvac-mobile-nav.css',
|
|
'./assets/css/hvac-animations.css',
|
|
'./assets/css/hvac-print.css'
|
|
];
|
|
|
|
let totalSize = 0;
|
|
let totalRules = 0;
|
|
let totalSelectors = 0;
|
|
|
|
cssFiles.forEach(file => {
|
|
if (fs.existsSync(file)) {
|
|
const analysis = this.analyzeFile(file);
|
|
totalSize += analysis.size;
|
|
totalRules += analysis.rules;
|
|
totalSelectors += analysis.selectors;
|
|
}
|
|
});
|
|
|
|
this.generatePerformanceReport(totalSize, totalRules, totalSelectors);
|
|
}
|
|
|
|
analyzeFile(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const fileName = path.basename(filePath);
|
|
const stats = fs.statSync(filePath);
|
|
|
|
console.log(`\n🔍 ${fileName}:`);
|
|
|
|
// File size analysis
|
|
const sizeKB = Math.round(stats.size / 1024 * 100) / 100;
|
|
console.log(` 📊 File size: ${sizeKB}KB`);
|
|
|
|
if (sizeKB > 50) {
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'Large file size',
|
|
impact: 'Slower initial load time',
|
|
severity: 'medium',
|
|
recommendation: 'Consider splitting into smaller files or minification'
|
|
});
|
|
}
|
|
|
|
// Selector complexity analysis
|
|
const analysis = this.analyzeSelectorComplexity(content, fileName);
|
|
|
|
// Media query analysis
|
|
this.analyzeMediaQueries(content, fileName);
|
|
|
|
// Animation performance
|
|
this.analyzeAnimationPerformance(content, fileName);
|
|
|
|
// CSS specificity analysis
|
|
this.analyzeSpecificity(content, fileName);
|
|
|
|
// Unused/duplicate code analysis
|
|
this.analyzeCodeEfficiency(content, fileName);
|
|
|
|
// Critical rendering path impact
|
|
this.analyzeCriticalPath(content, fileName);
|
|
|
|
this.metrics.set(fileName, {
|
|
size: sizeKB,
|
|
rules: analysis.rules,
|
|
selectors: analysis.selectors,
|
|
complexity: analysis.avgComplexity
|
|
});
|
|
|
|
return {
|
|
size: sizeKB,
|
|
rules: analysis.rules,
|
|
selectors: analysis.selectors
|
|
};
|
|
}
|
|
|
|
analyzeSelectorComplexity(content, fileName) {
|
|
const selectors = content.match(/[^{}]+(?=\s*{)/g) || [];
|
|
const rules = content.match(/{[^}]*}/g) || [];
|
|
|
|
console.log(` 🎯 Selectors: ${selectors.length}`);
|
|
console.log(` 📜 Rules: ${rules.length}`);
|
|
|
|
// Analyze selector complexity
|
|
const complexSelectors = [];
|
|
const universalSelectors = [];
|
|
const deepSelectors = [];
|
|
const idSelectors = [];
|
|
|
|
let totalComplexity = 0;
|
|
|
|
selectors.forEach(selector => {
|
|
const cleanSelector = selector.trim();
|
|
|
|
// Count complexity (rough heuristic)
|
|
const complexity = this.calculateSelectorComplexity(cleanSelector);
|
|
totalComplexity += complexity;
|
|
|
|
if (complexity > 5) {
|
|
complexSelectors.push({ selector: cleanSelector, complexity });
|
|
}
|
|
|
|
if (cleanSelector.includes('*')) {
|
|
universalSelectors.push(cleanSelector);
|
|
}
|
|
|
|
if (cleanSelector.split(' ').length > 4) {
|
|
deepSelectors.push(cleanSelector);
|
|
}
|
|
|
|
if (cleanSelector.includes('#')) {
|
|
idSelectors.push(cleanSelector);
|
|
}
|
|
});
|
|
|
|
const avgComplexity = selectors.length > 0 ? totalComplexity / selectors.length : 0;
|
|
console.log(` 🔢 Average selector complexity: ${Math.round(avgComplexity * 100) / 100}`);
|
|
|
|
if (complexSelectors.length > 0) {
|
|
console.log(` ⚠️ Complex selectors: ${complexSelectors.length}`);
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'Complex selectors detected',
|
|
impact: 'Slower CSS parsing and matching',
|
|
severity: 'medium',
|
|
count: complexSelectors.length,
|
|
examples: complexSelectors.slice(0, 3).map(s => s.selector)
|
|
});
|
|
}
|
|
|
|
if (universalSelectors.length > 0) {
|
|
console.log(` ⚠️ Universal selectors: ${universalSelectors.length}`);
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'Universal selectors',
|
|
impact: 'Performance impact on large DOMs',
|
|
severity: 'low',
|
|
count: universalSelectors.length
|
|
});
|
|
}
|
|
|
|
if (deepSelectors.length > 0) {
|
|
console.log(` ⚠️ Deep selectors (4+ levels): ${deepSelectors.length}`);
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'Deep selector nesting',
|
|
impact: 'Slower selector matching',
|
|
severity: 'low',
|
|
count: deepSelectors.length
|
|
});
|
|
}
|
|
|
|
if (idSelectors.length > 10) {
|
|
console.log(` 💡 High ID selector usage: ${idSelectors.length}`);
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Consider using classes instead of IDs for styling',
|
|
benefit: 'Better reusability and lower specificity'
|
|
});
|
|
}
|
|
|
|
return {
|
|
selectors: selectors.length,
|
|
rules: rules.length,
|
|
avgComplexity,
|
|
complexSelectors,
|
|
universalSelectors,
|
|
deepSelectors
|
|
};
|
|
}
|
|
|
|
calculateSelectorComplexity(selector) {
|
|
let complexity = 0;
|
|
|
|
// Base complexity
|
|
complexity += 1;
|
|
|
|
// Add for each combinator
|
|
complexity += (selector.match(/[>+~]/g) || []).length;
|
|
|
|
// Add for each pseudo-class/element
|
|
complexity += (selector.match(/:[^:]/g) || []).length;
|
|
|
|
// Add for each attribute selector
|
|
complexity += (selector.match(/\[[^\]]*\]/g) || []).length;
|
|
|
|
// Add for each class
|
|
complexity += (selector.match(/\.[^.\s#[]+/g) || []).length;
|
|
|
|
// Add for each ID (heavily weighted)
|
|
complexity += (selector.match(/#[^.\s#[]+/g) || []).length * 2;
|
|
|
|
// Add for universal selector
|
|
if (selector.includes('*')) complexity += 1;
|
|
|
|
return complexity;
|
|
}
|
|
|
|
analyzeMediaQueries(content, fileName) {
|
|
const mediaQueries = content.match(/@media[^{]*{[^{}]*({[^}]*}[^{}]*)*}/g) || [];
|
|
|
|
if (mediaQueries.length > 0) {
|
|
console.log(` 📱 Media queries: ${mediaQueries.length}`);
|
|
|
|
// Check for redundant media queries
|
|
const breakpoints = new Set();
|
|
mediaQueries.forEach(mq => {
|
|
const condition = mq.match(/@media[^{]*/)[0];
|
|
breakpoints.add(condition);
|
|
});
|
|
|
|
if (breakpoints.size < mediaQueries.length) {
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Consolidate duplicate media queries',
|
|
benefit: 'Reduced file size and better organization',
|
|
details: `${mediaQueries.length} queries, ${breakpoints.size} unique breakpoints`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
analyzeAnimationPerformance(content, fileName) {
|
|
const animations = content.match(/@keyframes[^{]*{[^{}]*({[^}]*}[^{}]*)*}/g) || [];
|
|
const transforms = content.match(/transform\s*:[^;]*/g) || [];
|
|
const transitions = content.match(/transition\s*:[^;]*/g) || [];
|
|
|
|
if (animations.length > 0) {
|
|
console.log(` 🎬 Animations: ${animations.length}`);
|
|
|
|
// Check for performance-friendly properties
|
|
const expensiveProperties = [];
|
|
animations.forEach(animation => {
|
|
const expensiveProps = [
|
|
'width', 'height', 'top', 'left', 'right', 'bottom',
|
|
'margin', 'padding', 'border', 'box-shadow'
|
|
];
|
|
|
|
expensiveProps.forEach(prop => {
|
|
if (animation.includes(prop + ':')) {
|
|
expensiveProperties.push(prop);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (expensiveProperties.length > 0) {
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'Animations using expensive properties',
|
|
impact: 'Can cause layout thrashing and poor performance',
|
|
severity: 'medium',
|
|
recommendation: 'Use transform and opacity for animations',
|
|
properties: [...new Set(expensiveProperties)]
|
|
});
|
|
}
|
|
}
|
|
|
|
if (transforms.length > 10) {
|
|
console.log(` ✅ Good use of transforms: ${transforms.length}`);
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Good use of GPU-accelerated transforms',
|
|
benefit: 'Smooth animations with hardware acceleration'
|
|
});
|
|
}
|
|
}
|
|
|
|
analyzeSpecificity(content, fileName) {
|
|
const selectors = content.match(/[^{}]+(?=\s*{)/g) || [];
|
|
|
|
let highSpecificity = 0;
|
|
let averageSpecificity = 0;
|
|
let specificityWarnings = [];
|
|
|
|
selectors.forEach(selector => {
|
|
const specificity = this.calculateSpecificity(selector.trim());
|
|
averageSpecificity += specificity.total;
|
|
|
|
if (specificity.total > 100) {
|
|
highSpecificity++;
|
|
if (specificityWarnings.length < 3) {
|
|
specificityWarnings.push({
|
|
selector: selector.trim(),
|
|
specificity: specificity.total
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (selectors.length > 0) {
|
|
averageSpecificity = Math.round(averageSpecificity / selectors.length);
|
|
console.log(` 🎯 Average specificity: ${averageSpecificity}`);
|
|
}
|
|
|
|
if (highSpecificity > 0) {
|
|
console.log(` ⚠️ High specificity selectors: ${highSpecificity}`);
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'High specificity selectors',
|
|
impact: 'Harder to override, maintenance issues',
|
|
severity: 'low',
|
|
count: highSpecificity,
|
|
examples: specificityWarnings
|
|
});
|
|
}
|
|
}
|
|
|
|
calculateSpecificity(selector) {
|
|
const ids = (selector.match(/#[^.\s#[]+/g) || []).length;
|
|
const classes = (selector.match(/\.[^.\s#[]+/g) || []).length;
|
|
const attributes = (selector.match(/\[[^\]]*\]/g) || []).length;
|
|
const pseudoClasses = (selector.match(/:[^:]/g) || []).length;
|
|
const elements = (selector.match(/^[a-zA-Z]+|[^.\s#[:]+/g) || []).length;
|
|
|
|
return {
|
|
ids,
|
|
classes: classes + attributes + pseudoClasses,
|
|
elements,
|
|
total: (ids * 100) + ((classes + attributes + pseudoClasses) * 10) + elements
|
|
};
|
|
}
|
|
|
|
analyzeCodeEfficiency(content, fileName) {
|
|
// Check for duplicate rules (simple heuristic)
|
|
const rules = content.match(/{[^}]*}/g) || [];
|
|
const ruleContents = rules.map(rule => rule.replace(/\s+/g, ' ').trim());
|
|
const uniqueRules = new Set(ruleContents);
|
|
|
|
if (rules.length !== uniqueRules.size) {
|
|
const duplicates = rules.length - uniqueRules.size;
|
|
console.log(` ⚠️ Potential duplicate rules: ${duplicates}`);
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Remove duplicate CSS rules',
|
|
benefit: 'Smaller file size and better maintainability',
|
|
details: `${duplicates} potential duplicates found`
|
|
});
|
|
}
|
|
|
|
// Check for shorthand opportunities
|
|
const longhandProperties = content.match(/margin-top|margin-right|margin-bottom|margin-left|padding-top|padding-right|padding-bottom|padding-left/g) || [];
|
|
if (longhandProperties.length > 10) {
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Use CSS shorthand properties where possible',
|
|
benefit: 'Reduced file size',
|
|
count: longhandProperties.length
|
|
});
|
|
}
|
|
}
|
|
|
|
analyzeCriticalPath(content, fileName) {
|
|
// Check for render-blocking patterns
|
|
const imports = content.match(/@import[^;]*;/g) || [];
|
|
|
|
if (imports.length > 0) {
|
|
console.log(` ⚠️ CSS imports: ${imports.length}`);
|
|
this.performanceIssues.push({
|
|
file: fileName,
|
|
issue: 'CSS @import statements',
|
|
impact: 'Blocks parallel loading of stylesheets',
|
|
severity: 'high',
|
|
recommendation: 'Use HTML link tags instead',
|
|
count: imports.length
|
|
});
|
|
}
|
|
|
|
// Check for web fonts
|
|
const fontFaces = content.match(/@font-face\s*{[^}]*}/g) || [];
|
|
if (fontFaces.length > 0) {
|
|
console.log(` 🔤 Font faces: ${fontFaces.length}`);
|
|
|
|
// Check for font-display property
|
|
const fontDisplays = content.match(/font-display\s*:[^;]*/g) || [];
|
|
if (fontDisplays.length < fontFaces.length) {
|
|
this.optimizations.push({
|
|
file: fileName,
|
|
optimization: 'Add font-display: swap to @font-face rules',
|
|
benefit: 'Prevent invisible text during font load',
|
|
missing: fontFaces.length - fontDisplays.length
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
generatePerformanceReport(totalSize, totalRules, totalSelectors) {
|
|
console.log('\n' + '='.repeat(80));
|
|
console.log('⚡ CSS PERFORMANCE REPORT');
|
|
console.log('='.repeat(80));
|
|
|
|
// Overall metrics
|
|
console.log('\n📊 OVERALL METRICS:');
|
|
console.log(` Total CSS size: ${Math.round(totalSize * 100) / 100}KB`);
|
|
console.log(` Total rules: ${totalRules}`);
|
|
console.log(` Total selectors: ${totalSelectors}`);
|
|
console.log(` Average rules per file: ${Math.round(totalRules / this.metrics.size)}`);
|
|
|
|
// File size breakdown
|
|
console.log('\n📁 FILE SIZE BREAKDOWN:');
|
|
const sortedFiles = Array.from(this.metrics.entries())
|
|
.sort(([,a], [,b]) => b.size - a.size);
|
|
|
|
sortedFiles.forEach(([file, metrics]) => {
|
|
const percentage = Math.round((metrics.size / totalSize) * 100);
|
|
console.log(` ${file}: ${metrics.size}KB (${percentage}%) - ${metrics.rules} rules, ${metrics.selectors} selectors`);
|
|
});
|
|
|
|
// Performance issues
|
|
if (this.performanceIssues.length > 0) {
|
|
console.log('\n🚨 PERFORMANCE ISSUES:');
|
|
|
|
const highSeverity = this.performanceIssues.filter(i => i.severity === 'high');
|
|
const mediumSeverity = this.performanceIssues.filter(i => i.severity === 'medium');
|
|
const lowSeverity = this.performanceIssues.filter(i => i.severity === 'low');
|
|
|
|
if (highSeverity.length > 0) {
|
|
console.log('\n 🔴 HIGH SEVERITY:');
|
|
highSeverity.forEach((issue, i) => {
|
|
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
|
console.log(` Impact: ${issue.impact}`);
|
|
if (issue.recommendation) console.log(` Fix: ${issue.recommendation}`);
|
|
if (issue.count) console.log(` Count: ${issue.count}`);
|
|
});
|
|
}
|
|
|
|
if (mediumSeverity.length > 0) {
|
|
console.log('\n 🟡 MEDIUM SEVERITY:');
|
|
mediumSeverity.forEach((issue, i) => {
|
|
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
|
console.log(` Impact: ${issue.impact}`);
|
|
if (issue.recommendation) console.log(` Fix: ${issue.recommendation}`);
|
|
if (issue.count) console.log(` Count: ${issue.count}`);
|
|
});
|
|
}
|
|
|
|
if (lowSeverity.length > 0) {
|
|
console.log('\n 🟢 LOW SEVERITY:');
|
|
lowSeverity.forEach((issue, i) => {
|
|
console.log(` ${i + 1}. ${issue.file}: ${issue.issue}`);
|
|
console.log(` Impact: ${issue.impact}`);
|
|
if (issue.count) console.log(` Count: ${issue.count}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Optimization opportunities
|
|
if (this.optimizations.length > 0) {
|
|
console.log('\n💡 OPTIMIZATION OPPORTUNITIES:');
|
|
this.optimizations.forEach((opt, i) => {
|
|
console.log(`\n ${i + 1}. ${opt.file}: ${opt.optimization}`);
|
|
console.log(` Benefit: ${opt.benefit}`);
|
|
if (opt.details) console.log(` Details: ${opt.details}`);
|
|
if (opt.count) console.log(` Instances: ${opt.count}`);
|
|
});
|
|
}
|
|
|
|
// Performance recommendations
|
|
console.log('\n🎯 PERFORMANCE RECOMMENDATIONS:');
|
|
console.log(' 1. Minify CSS files for production');
|
|
console.log(' 2. Use CSS compression (gzip/brotli)');
|
|
console.log(' 3. Eliminate unused CSS with tools like PurgeCSS');
|
|
console.log(' 4. Split critical CSS for above-the-fold content');
|
|
console.log(' 5. Use CSS containment for better isolation');
|
|
console.log(' 6. Optimize selectors for better matching performance');
|
|
console.log(' 7. Use will-change property sparingly for animations');
|
|
console.log(' 8. Consider CSS-in-JS for component-scoped styles');
|
|
console.log(' 9. Use CSS custom properties efficiently');
|
|
console.log(' 10. Regular performance audits with dev tools');
|
|
|
|
// Performance score
|
|
const highIssues = this.performanceIssues.filter(i => i.severity === 'high').length;
|
|
const mediumIssues = this.performanceIssues.filter(i => i.severity === 'medium').length;
|
|
const lowIssues = this.performanceIssues.filter(i => i.severity === 'low').length;
|
|
|
|
const performanceScore = Math.max(0, 100 - (highIssues * 25) - (mediumIssues * 15) - (lowIssues * 5));
|
|
|
|
console.log('\n📈 PERFORMANCE SUMMARY:');
|
|
console.log(` High Priority Issues: ${highIssues}`);
|
|
console.log(` Medium Priority Issues: ${mediumIssues}`);
|
|
console.log(` Low Priority Issues: ${lowIssues}`);
|
|
console.log(` Optimization Opportunities: ${this.optimizations.length}`);
|
|
console.log(` Performance Score: ${performanceScore}/100`);
|
|
|
|
if (performanceScore >= 90) {
|
|
console.log(' 🟢 Excellent CSS performance');
|
|
} else if (performanceScore >= 70) {
|
|
console.log(' 🟡 Good CSS performance with room for improvement');
|
|
} else if (performanceScore >= 50) {
|
|
console.log(' 🟠 Moderate CSS performance - optimization needed');
|
|
} else {
|
|
console.log(' 🔴 Poor CSS performance - immediate attention required');
|
|
}
|
|
|
|
// File size recommendations
|
|
if (totalSize > 100) {
|
|
console.log('\n💾 FILE SIZE RECOMMENDATIONS:');
|
|
console.log(' • Consider code splitting for large CSS files');
|
|
console.log(' • Use critical CSS extraction');
|
|
console.log(' • Implement lazy loading for non-critical styles');
|
|
}
|
|
}
|
|
}
|
|
|
|
const analyzer = new CSSPerformanceAnalyzer();
|
|
analyzer.analyzePerformance(); |