feat(performance): add Redux performance monitoring and memoized selectors
- Introduced a Redux performance monitoring system to log action metrics, including duration and state size. - Implemented middleware for tracking performance of Redux actions and logging slow actions in development. - Added utility functions for analyzing performance metrics and generating recommendations for optimization. - Created memoized selectors to enhance performance and prevent unnecessary re-renders across various application states.
This commit is contained in:
122
worklenz-frontend/src/app/performance-monitor.ts
Normal file
122
worklenz-frontend/src/app/performance-monitor.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Middleware } from '@reduxjs/toolkit';
|
||||
|
||||
// Performance monitoring for Redux store
|
||||
export interface PerformanceMetrics {
|
||||
actionType: string;
|
||||
duration: number;
|
||||
timestamp: number;
|
||||
stateSize: number;
|
||||
}
|
||||
|
||||
class ReduxPerformanceMonitor {
|
||||
private metrics: PerformanceMetrics[] = [];
|
||||
private maxMetrics = 100; // Keep last 100 metrics
|
||||
private slowActionThreshold = 50; // Log actions taking more than 50ms
|
||||
|
||||
logMetric(metric: PerformanceMetrics) {
|
||||
this.metrics.push(metric);
|
||||
|
||||
// Keep only recent metrics
|
||||
if (this.metrics.length > this.maxMetrics) {
|
||||
this.metrics = this.metrics.slice(-this.maxMetrics);
|
||||
}
|
||||
|
||||
// Log slow actions in development
|
||||
if (process.env.NODE_ENV === 'development' && metric.duration > this.slowActionThreshold) {
|
||||
console.warn(`Slow Redux action detected: ${metric.actionType} took ${metric.duration}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return [...this.metrics];
|
||||
}
|
||||
|
||||
getSlowActions(threshold = this.slowActionThreshold) {
|
||||
return this.metrics.filter(m => m.duration > threshold);
|
||||
}
|
||||
|
||||
getAverageActionTime() {
|
||||
if (this.metrics.length === 0) return 0;
|
||||
const total = this.metrics.reduce((sum, m) => sum + m.duration, 0);
|
||||
return total / this.metrics.length;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.metrics = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const performanceMonitor = new ReduxPerformanceMonitor();
|
||||
|
||||
// Redux middleware for performance monitoring
|
||||
export const performanceMiddleware: Middleware = (store) => (next) => (action: any) => {
|
||||
const start = performance.now();
|
||||
|
||||
const result = next(action);
|
||||
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
|
||||
// Calculate approximate state size (in development only)
|
||||
let stateSize = 0;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
try {
|
||||
stateSize = JSON.stringify(store.getState()).length;
|
||||
} catch (e) {
|
||||
stateSize = -1; // Indicates serialization error
|
||||
}
|
||||
}
|
||||
|
||||
performanceMonitor.logMetric({
|
||||
actionType: action.type || 'unknown',
|
||||
duration,
|
||||
timestamp: Date.now(),
|
||||
stateSize,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Hook to access performance metrics in components
|
||||
export function useReduxPerformance() {
|
||||
return {
|
||||
metrics: performanceMonitor.getMetrics(),
|
||||
slowActions: performanceMonitor.getSlowActions(),
|
||||
averageTime: performanceMonitor.getAverageActionTime(),
|
||||
reset: () => performanceMonitor.reset(),
|
||||
};
|
||||
}
|
||||
|
||||
// Utility to detect potential performance issues
|
||||
export function analyzeReduxPerformance() {
|
||||
const metrics = performanceMonitor.getMetrics();
|
||||
const analysis = {
|
||||
totalActions: metrics.length,
|
||||
slowActions: performanceMonitor.getSlowActions().length,
|
||||
averageActionTime: performanceMonitor.getAverageActionTime(),
|
||||
largestStateSize: Math.max(...metrics.map(m => m.stateSize)),
|
||||
mostFrequentActions: {} as Record<string, number>,
|
||||
recommendations: [] as string[],
|
||||
};
|
||||
|
||||
// Count action frequencies
|
||||
metrics.forEach(m => {
|
||||
analysis.mostFrequentActions[m.actionType] =
|
||||
(analysis.mostFrequentActions[m.actionType] || 0) + 1;
|
||||
});
|
||||
|
||||
// Generate recommendations
|
||||
if (analysis.slowActions > analysis.totalActions * 0.1) {
|
||||
analysis.recommendations.push('Consider optimizing selectors with createSelector');
|
||||
}
|
||||
|
||||
if (analysis.largestStateSize > 1000000) { // 1MB
|
||||
analysis.recommendations.push('State size is large - consider normalizing data');
|
||||
}
|
||||
|
||||
if (analysis.averageActionTime > 20) {
|
||||
analysis.recommendations.push('Average action time is high - check for expensive reducers');
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
81
worklenz-frontend/src/app/selectors.ts
Normal file
81
worklenz-frontend/src/app/selectors.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from './store';
|
||||
|
||||
// Memoized selectors for better performance
|
||||
// These prevent unnecessary re-renders when state hasn't actually changed
|
||||
|
||||
// Auth selectors
|
||||
export const selectAuth = (state: RootState) => state.auth;
|
||||
export const selectUser = (state: RootState) => state.userReducer;
|
||||
export const selectIsAuthenticated = createSelector(
|
||||
[selectAuth],
|
||||
(auth) => !!auth.user
|
||||
);
|
||||
|
||||
// Project selectors
|
||||
export const selectProjects = (state: RootState) => state.projectsReducer;
|
||||
export const selectCurrentProject = (state: RootState) => state.projectReducer;
|
||||
export const selectProjectMembers = (state: RootState) => state.projectMemberReducer;
|
||||
|
||||
// Task selectors
|
||||
export const selectTasks = (state: RootState) => state.taskReducer;
|
||||
export const selectTaskManagement = (state: RootState) => state.taskManagement;
|
||||
export const selectTaskSelection = (state: RootState) => state.taskManagementSelection;
|
||||
|
||||
// UI State selectors
|
||||
export const selectTheme = (state: RootState) => state.themeReducer;
|
||||
export const selectLocale = (state: RootState) => state.localesReducer;
|
||||
export const selectAlerts = (state: RootState) => state.alertsReducer;
|
||||
|
||||
// Board and Project View selectors
|
||||
export const selectBoard = (state: RootState) => state.boardReducer;
|
||||
export const selectProjectView = (state: RootState) => state.projectViewReducer;
|
||||
export const selectProjectDrawer = (state: RootState) => state.projectDrawerReducer;
|
||||
|
||||
// Task attributes selectors
|
||||
export const selectTaskPriorities = (state: RootState) => state.priorityReducer;
|
||||
export const selectTaskLabels = (state: RootState) => state.taskLabelsReducer;
|
||||
export const selectTaskStatuses = (state: RootState) => state.taskStatusReducer;
|
||||
export const selectTaskDrawer = (state: RootState) => state.taskDrawerReducer;
|
||||
|
||||
// Settings selectors
|
||||
export const selectMembers = (state: RootState) => state.memberReducer;
|
||||
export const selectClients = (state: RootState) => state.clientReducer;
|
||||
export const selectJobs = (state: RootState) => state.jobReducer;
|
||||
export const selectTeams = (state: RootState) => state.teamReducer;
|
||||
export const selectCategories = (state: RootState) => state.categoriesReducer;
|
||||
export const selectLabels = (state: RootState) => state.labelReducer;
|
||||
|
||||
// Reporting selectors
|
||||
export const selectReporting = (state: RootState) => state.reportingReducer;
|
||||
export const selectProjectReports = (state: RootState) => state.projectReportsReducer;
|
||||
export const selectMemberReports = (state: RootState) => state.membersReportsReducer;
|
||||
export const selectTimeReports = (state: RootState) => state.timeReportsOverviewReducer;
|
||||
|
||||
// Admin and billing selectors
|
||||
export const selectAdminCenter = (state: RootState) => state.adminCenterReducer;
|
||||
export const selectBilling = (state: RootState) => state.billingReducer;
|
||||
|
||||
// Schedule and date selectors
|
||||
export const selectSchedule = (state: RootState) => state.scheduleReducer;
|
||||
export const selectDate = (state: RootState) => state.dateReducer;
|
||||
|
||||
// Feature-specific selectors
|
||||
export const selectHomePage = (state: RootState) => state.homePageReducer;
|
||||
export const selectAccountSetup = (state: RootState) => state.accountSetupReducer;
|
||||
export const selectRoadmap = (state: RootState) => state.roadmapReducer;
|
||||
export const selectGroupByFilter = (state: RootState) => state.groupByFilterDropdownReducer;
|
||||
|
||||
// Memoized computed selectors for common use cases
|
||||
export const selectHasActiveProject = createSelector(
|
||||
[selectCurrentProject],
|
||||
(project) => !!project && Object.keys(project).length > 0
|
||||
);
|
||||
|
||||
export const selectIsLoading = createSelector(
|
||||
[selectTasks, selectProjects],
|
||||
(tasks, projects) => {
|
||||
// Check if any major feature is loading
|
||||
return (tasks as any)?.loading || (projects as any)?.loading;
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user