UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

883 lines (762 loc) 25.5 kB
const { parse } = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; const t = require('@babel/types'); /** * Performance Optimizer Module * Identifies and fixes React performance bottlenecks */ class PerformanceOptimizer { constructor() { this.performanceIssues = []; this.optimizations = []; this.metrics = { renderOptimizations: 0, memoizations: 0, hookOptimizations: 0, bundleOptimizations: 0 }; } /** * Analyze and optimize React component performance */ optimizePerformance(ast, filePath, context = {}) { const optimizations = []; const issues = []; // Reset analysis for this file this.performanceIssues = []; this.optimizations = []; // Analyze components for performance issues const componentAnalysis = this._analyzeComponents(ast); // Generate optimizations componentAnalysis.forEach(component => { // Check for missing memoization const memoOptimizations = this._optimizeMemoization(component, ast); optimizations.push(...memoOptimizations); // Check for expensive renders const renderOptimizations = this._optimizeRenders(component, ast); optimizations.push(...renderOptimizations); // Check for hook optimizations const hookOptimizations = this._optimizeHooks(component, ast); optimizations.push(...hookOptimizations); // Check for bundle size optimizations const bundleOptimizations = this._optimizeBundleSize(component, ast); optimizations.push(...bundleOptimizations); }); // Analyze global patterns const globalOptimizations = this._analyzeGlobalPatterns(ast, filePath); optimizations.push(...globalOptimizations); return { optimizations, issues: this.performanceIssues, metrics: this.metrics, recommendations: this._generateRecommendations(optimizations) }; } /** * Optimize component memoization */ _optimizeMemoization(component, ast) { const optimizations = []; if (!component.isMemoized && component.shouldMemoize) { optimizations.push({ type: 'add-memo', location: component.location, component: component.name, action: () => { this._wrapWithMemo(component.path, ast); }, description: `Wrap ${component.name} with React.memo`, impact: 'high', reason: component.memoReason }); this.metrics.memoizations++; } // Check for custom comparison function needs if (component.needsCustomComparison) { optimizations.push({ type: 'custom-memo-comparison', location: component.location, component: component.name, action: () => { this._addCustomMemoComparison(component.path, ast); }, description: `Add custom comparison function to ${component.name}`, impact: 'medium', reason: 'Component has complex props that need custom comparison' }); } return optimizations; } /** * Optimize expensive renders */ _optimizeRenders(component, ast) { const optimizations = []; // Check for expensive calculations in render component.expensiveOperations.forEach(operation => { optimizations.push({ type: 'extract-to-usememo', location: operation.location, component: component.name, operation: operation.type, action: () => { this._extractToUseMemo(operation.path, ast); }, description: `Extract ${operation.type} to useMemo in ${component.name}`, impact: 'high', reason: 'Expensive calculation should be memoized' }); this.metrics.renderOptimizations++; }); // Check for inline object/array creation component.inlineObjects.forEach(inline => { optimizations.push({ type: 'extract-inline-object', location: inline.location, component: component.name, action: () => { this._extractInlineObject(inline.path, ast); }, description: `Extract inline ${inline.type} to prevent re-creation`, impact: 'medium', reason: 'Inline objects cause unnecessary re-renders' }); }); // Check for unnecessary re-renders due to props if (component.hasUnnecessaryPropChanges) { optimizations.push({ type: 'optimize-prop-passing', location: component.location, component: component.name, action: () => { this._optimizePropPassing(component.path, ast); }, description: `Optimize prop passing for ${component.name}`, impact: 'medium', reason: 'Props are causing unnecessary re-renders' }); } return optimizations; } /** * Optimize React hooks usage */ _optimizeHooks(component, ast) { const optimizations = []; // Optimize useEffect dependencies component.effects.forEach(effect => { if (effect.hasMissingDependencies) { optimizations.push({ type: 'fix-effect-dependencies', location: effect.location, component: component.name, action: () => { this._fixEffectDependencies(effect.path, ast); }, description: `Fix useEffect dependencies in ${component.name}`, impact: 'high', reason: 'Missing dependencies can cause bugs and performance issues' }); this.metrics.hookOptimizations++; } if (effect.runsOnEveryRender) { optimizations.push({ type: 'optimize-effect-frequency', location: effect.location, component: component.name, action: () => { this._optimizeEffectFrequency(effect.path, ast); }, description: `Optimize useEffect frequency in ${component.name}`, impact: 'high', reason: 'Effect runs on every render' }); } }); // Optimize useCallback usage component.callbacks.forEach(callback => { if (callback.shouldUseCallback && !callback.isOptimized) { optimizations.push({ type: 'add-usecallback', location: callback.location, component: component.name, action: () => { this._wrapWithUseCallback(callback.path, ast); }, description: `Wrap callback with useCallback in ${component.name}`, impact: 'medium', reason: 'Callback is recreated on every render' }); } }); // Check for useState optimizations component.stateUpdates.forEach(stateUpdate => { if (stateUpdate.canBeBatched) { optimizations.push({ type: 'batch-state-updates', location: stateUpdate.location, component: component.name, action: () => { this._batchStateUpdates(stateUpdate.paths, ast); }, description: `Batch state updates in ${component.name}`, impact: 'medium', reason: 'Multiple state updates can be batched' }); } }); return optimizations; } /** * Optimize bundle size */ _optimizeBundleSize(component, ast) { const optimizations = []; // Check for unused imports component.unusedImports.forEach(unusedImport => { optimizations.push({ type: 'remove-unused-import', location: unusedImport.location, action: () => { unusedImport.path.remove(); }, description: `Remove unused import: ${unusedImport.name}`, impact: 'low', reason: 'Unused imports increase bundle size' }); this.metrics.bundleOptimizations++; }); // Check for tree-shaking opportunities component.imports.forEach(importDecl => { if (importDecl.canBeTreeShaken) { optimizations.push({ type: 'optimize-import', location: importDecl.location, action: () => { this._optimizeImportForTreeShaking(importDecl.path, ast); }, description: `Optimize import for tree-shaking: ${importDecl.source}`, impact: 'medium', reason: 'Import can be optimized for better tree-shaking' }); } }); return optimizations; } /** * Analyze global performance patterns */ _analyzeGlobalPatterns(ast, filePath) { const optimizations = []; // Check for dynamic imports opportunities const dynamicImportOpportunities = this._findDynamicImportOpportunities(ast); dynamicImportOpportunities.forEach(opportunity => { optimizations.push({ type: 'add-dynamic-import', location: opportunity.location, action: () => { this._convertToDynamicImport(opportunity.path, ast); }, description: `Convert to dynamic import: ${opportunity.component}`, impact: 'high', reason: 'Component can be lazy-loaded' }); }); // Check for code splitting opportunities const codeSplitOpportunities = this._findCodeSplitOpportunities(ast); codeSplitOpportunities.forEach(opportunity => { optimizations.push({ type: 'add-code-splitting', location: opportunity.location, action: () => { // This would be a suggestion rather than automatic transformation }, description: `Consider code splitting for route: ${opportunity.route}`, impact: 'high', reason: 'Route can benefit from code splitting', suggestion: true }); }); return optimizations; } /** * Analyze components for performance characteristics */ _analyzeComponents(ast) { const components = []; traverse(ast, { FunctionDeclaration: (path) => { const analysis = this._analyzeComponent(path); if (analysis.isComponent) { components.push(analysis); } }, VariableDeclarator: (path) => { if (t.isArrowFunctionExpression(path.node.init) || t.isFunctionExpression(path.node.init)) { const analysis = this._analyzeComponent(path); if (analysis.isComponent) { components.push(analysis); } } } }); return components; } /** * Analyze individual component for performance characteristics */ _analyzeComponent(path) { const name = this._getComponentName(path); const analysis = { name, path, location: path.node.loc, isComponent: this._isReactComponent(path), isMemoized: false, shouldMemoize: false, memoReason: '', needsCustomComparison: false, expensiveOperations: [], inlineObjects: [], hasUnnecessaryPropChanges: false, effects: [], callbacks: [], stateUpdates: [], unusedImports: [], imports: [], complexity: 0 }; if (!analysis.isComponent) return analysis; // Analyze component body this._analyzeComponentBody(path, analysis); // Determine if should memoize analysis.shouldMemoize = this._shouldComponentMemoize(analysis); analysis.isMemoized = this._isComponentMemoized(path); return analysis; } _analyzeComponentBody(path, analysis) { const functionNode = this._getFunctionNode(path); if (!functionNode) return; traverse(functionNode, { // Track expensive operations CallExpression: (callPath) => { if (this._isExpensiveOperation(callPath)) { analysis.expensiveOperations.push({ type: this._getOperationType(callPath), path: callPath, location: callPath.node.loc }); } // Analyze hooks const hookInfo = this._analyzeHook(callPath, analysis); if (hookInfo) { if (hookInfo.type === 'useEffect') { analysis.effects.push(hookInfo); } else if (hookInfo.type === 'callback') { analysis.callbacks.push(hookInfo); } else if (hookInfo.type === 'useState') { analysis.stateUpdates.push(hookInfo); } } }, // Track inline object/array creation ObjectExpression: (objPath) => { if (this._isInlineObject(objPath)) { analysis.inlineObjects.push({ type: 'object', path: objPath, location: objPath.node.loc }); } }, ArrayExpression: (arrPath) => { if (this._isInlineArray(arrPath)) { analysis.inlineObjects.push({ type: 'array', path: arrPath, location: arrPath.node.loc }); } }, // Calculate complexity IfStatement: () => analysis.complexity++, ConditionalExpression: () => analysis.complexity++, LogicalExpression: () => analysis.complexity++, SwitchStatement: () => analysis.complexity++, ForStatement: () => analysis.complexity++, WhileStatement: () => analysis.complexity++ }); } _analyzeHook(callPath, componentAnalysis) { if (!t.isCallExpression(callPath.node) || !t.isIdentifier(callPath.node.callee)) { return null; } const hookName = callPath.node.callee.name; if (hookName === 'useEffect') { return this._analyzeUseEffect(callPath); } else if (hookName === 'useState') { return this._analyzeUseState(callPath); } else if (hookName === 'useCallback') { return this._analyzeUseCallback(callPath); } else if (hookName === 'useMemo') { return this._analyzeUseMemo(callPath); } return null; } _analyzeUseEffect(path) { const args = path.node.arguments; const dependencies = args[1]; return { type: 'useEffect', path, location: path.node.loc, hasMissingDependencies: this._checkMissingDependencies(path), runsOnEveryRender: !dependencies, isEmpty: dependencies && t.isArrayExpression(dependencies) && dependencies.elements.length === 0 }; } _analyzeUseState(path) { return { type: 'useState', path, location: path.node.loc, canBeBatched: this._checkIfStateCanBeBatched(path) }; } _analyzeUseCallback(path) { return { type: 'callback', path, location: path.node.loc, isOptimized: true, shouldUseCallback: false // Already using useCallback }; } _analyzeUseMemo(path) { return { type: 'useMemo', path, location: path.node.loc, isOptimized: true }; } // Transformation methods _wrapWithMemo(componentPath, ast) { const functionNode = this._getFunctionNode(componentPath); if (!functionNode) return; // Create React.memo wrapper const memoCall = t.callExpression( t.memberExpression(t.identifier('React'), t.identifier('memo')), [functionNode] ); // Replace the original function if (t.isFunctionDeclaration(componentPath.node)) { componentPath.replaceWith( t.variableDeclaration('const', [ t.variableDeclarator(componentPath.node.id, memoCall) ]) ); } else if (t.isVariableDeclarator(componentPath.node)) { componentPath.node.init = memoCall; } } _addCustomMemoComparison(componentPath, ast) { // Add custom comparison function to React.memo const comparisonFunction = parse(` function areEqual(prevProps, nextProps) { // Custom comparison logic would go here return JSON.stringify(prevProps) === JSON.stringify(nextProps); } `).body[0]; // This would need more sophisticated implementation console.log('Adding custom memo comparison for', this._getComponentName(componentPath)); } _extractToUseMemo(operationPath, ast) { const operation = operationPath.node; // Create useMemo wrapper const useMemoCall = t.callExpression( t.identifier('useMemo'), [ t.arrowFunctionExpression([], operation), t.arrayExpression([]) // Dependencies would need to be analyzed ] ); operationPath.replaceWith(useMemoCall); } _extractInlineObject(inlinePath, ast) { // Move inline object to useMemo const objectExpression = inlinePath.node; const useMemoCall = t.callExpression( t.identifier('useMemo'), [ t.arrowFunctionExpression([], objectExpression), t.arrayExpression([]) // Dependencies would need to be analyzed ] ); inlinePath.replaceWith(useMemoCall); } _wrapWithUseCallback(callbackPath, ast) { const callback = callbackPath.node; const useCallbackCall = t.callExpression( t.identifier('useCallback'), [ callback, t.arrayExpression([]) // Dependencies would need to be analyzed ] ); callbackPath.replaceWith(useCallbackCall); } _fixEffectDependencies(effectPath, ast) { // Analyze the effect and add missing dependencies const dependencies = this._extractEffectDependencies(effectPath); const args = effectPath.node.arguments; if (args.length < 2) { args.push(t.arrayExpression(dependencies)); } else { args[1] = t.arrayExpression(dependencies); } } _optimizeEffectFrequency(effectPath, ast) { // Add empty dependency array to run only once const args = effectPath.node.arguments; if (args.length < 2) { args.push(t.arrayExpression([])); } } _batchStateUpdates(statePaths, ast) { // Wrap multiple state updates in unstable_batchedUpdates // This would need more sophisticated implementation console.log('Batching state updates for', statePaths.length, 'calls'); } _optimizeImportForTreeShaking(importPath, ast) { // Convert namespace imports to named imports where possible const source = importPath.node.source.value; // Example: import * as lodash from 'lodash' -> import { map, filter } from 'lodash' // This would need analysis of actual usage console.log('Optimizing import for tree-shaking:', source); } _convertToDynamicImport(componentPath, ast) { // Convert static import to dynamic import with React.lazy const componentName = this._getComponentName(componentPath); const lazyComponent = parse(` const ${componentName} = React.lazy(() => import('./${componentName}')); `).body[0]; // This would need more sophisticated implementation console.log('Converting to dynamic import:', componentName); } // Analysis helper methods _shouldComponentMemoize(analysis) { return analysis.complexity > 5 || analysis.expensiveOperations.length > 0 || analysis.inlineObjects.length > 2 || analysis.effects.length > 3; } _isComponentMemoized(path) { // Check if already wrapped with React.memo let parent = path.parent; while (parent) { if (t.isCallExpression(parent) && t.isMemberExpression(parent.callee) && t.isIdentifier(parent.callee.object, { name: 'React' }) && t.isIdentifier(parent.callee.property, { name: 'memo' })) { return true; } parent = parent.parent; } return false; } _isExpensiveOperation(path) { if (!t.isCallExpression(path.node)) return false; const callee = path.node.callee; // Array methods if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) { const methodName = callee.property.name; if (['map', 'filter', 'reduce', 'sort', 'find', 'some', 'every'].includes(methodName)) { return true; } } // Math operations if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'Math' })) { return true; } // JSON operations if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: 'JSON' })) { return true; } return false; } _getOperationType(path) { const callee = path.node.callee; if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) { return callee.property.name; } if (t.isIdentifier(callee)) { return callee.name; } return 'unknown'; } _isInlineObject(path) { // Check if object is created inline in JSX or function call return t.isJSXExpressionContainer(path.parent) || (t.isCallExpression(path.parent) && path.parent.arguments.includes(path.node)); } _isInlineArray(path) { // Similar to inline object check return t.isJSXExpressionContainer(path.parent) || (t.isCallExpression(path.parent) && path.parent.arguments.includes(path.node)); } _checkMissingDependencies(effectPath) { // Analyze effect body for external references // This would need sophisticated scope analysis return false; // Placeholder } _checkIfStateCanBeBatched(statePath) { // Check if there are multiple setState calls that can be batched // This would need more analysis return false; // Placeholder } _extractEffectDependencies(effectPath) { // Extract variables used in effect that should be dependencies // This would need sophisticated analysis return []; // Placeholder } _findDynamicImportOpportunities(ast) { const opportunities = []; // Find components that could be lazy-loaded traverse(ast, { ImportDeclaration(path) { const source = path.node.source.value; if (source.includes('component') || source.includes('Component')) { // This could be a component suitable for lazy loading path.node.specifiers.forEach(spec => { if (t.isImportDefaultSpecifier(spec) && /^[A-Z]/.test(spec.local.name)) { opportunities.push({ component: spec.local.name, source, path, location: path.node.loc }); } }); } } }); return opportunities; } _findCodeSplitOpportunities(ast) { const opportunities = []; // Look for route components or large features traverse(ast, { JSXElement(path) { const elementName = this._getJSXElementName(path); if (elementName === 'Route' || elementName === 'Switch') { opportunities.push({ route: elementName, path, location: path.node.loc }); } } }); return opportunities; } _generateRecommendations(optimizations) { const recommendations = []; // Group optimizations by impact const highImpact = optimizations.filter(opt => opt.impact === 'high'); const mediumImpact = optimizations.filter(opt => opt.impact === 'medium'); const lowImpact = optimizations.filter(opt => opt.impact === 'low'); if (highImpact.length > 0) { recommendations.push({ priority: 'high', title: 'Critical Performance Optimizations', description: `${highImpact.length} high-impact optimizations available`, optimizations: highImpact.slice(0, 5) }); } if (mediumImpact.length > 0) { recommendations.push({ priority: 'medium', title: 'Moderate Performance Improvements', description: `${mediumImpact.length} medium-impact optimizations available`, optimizations: mediumImpact.slice(0, 3) }); } if (lowImpact.length > 0) { recommendations.push({ priority: 'low', title: 'Minor Optimizations', description: `${lowImpact.length} low-impact optimizations available`, optimizations: lowImpact.slice(0, 2) }); } return recommendations; } // Utility methods _getComponentName(path) { if (t.isFunctionDeclaration(path.node) && path.node.id) { return path.node.id.name; } if (t.isVariableDeclarator(path.node) && t.isIdentifier(path.node.id)) { return path.node.id.name; } return 'Anonymous'; } _isReactComponent(path) { const functionNode = this._getFunctionNode(path); if (!functionNode) return false; // Check if returns JSX let returnsJSX = false; traverse(functionNode, { ReturnStatement(returnPath) { if (t.isJSXElement(returnPath.node.argument) || t.isJSXFragment(returnPath.node.argument)) { returnsJSX = true; } } }); return returnsJSX; } _getFunctionNode(path) { if (t.isFunctionDeclaration(path.node)) { return path.node; } if (t.isVariableDeclarator(path.node)) { return path.node.init; } return null; } _getJSXElementName(path) { const openingElement = path.node.openingElement; if (t.isJSXIdentifier(openingElement.name)) { return openingElement.name.name; } return null; } /** * Get performance analysis data for dashboard */ getAnalysisData() { return { performanceIssues: this.performanceIssues, optimizations: this.optimizations, metrics: this.metrics, recommendations: this._generateRecommendations(this.optimizations) }; } reset() { this.performanceIssues = []; this.optimizations = []; this.metrics = { renderOptimizations: 0, memoizations: 0, hookOptimizations: 0, bundleOptimizations: 0 }; } } module.exports = { PerformanceOptimizer };