@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
JavaScript
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 };