feat(performance): implement extensive performance optimizations across task management components
- Introduced batching and optimized query handling in SQL functions for improved performance during large updates. - Enhanced task sorting functions with batching to reduce load times and improve responsiveness. - Implemented performance monitoring utilities to track render times, memory usage, and long tasks, providing insights for further optimizations. - Added performance analysis component to visualize metrics and identify bottlenecks in task management. - Optimized drag-and-drop functionality with CSS enhancements to ensure smooth interactions and reduce layout thrashing. - Refined task row rendering logic to minimize DOM updates and improve loading behavior for large lists. - Introduced aggressive virtualization and memoization strategies to enhance rendering performance in task lists.
This commit is contained in:
@@ -4325,6 +4325,7 @@ DECLARE
|
|||||||
_from_group UUID;
|
_from_group UUID;
|
||||||
_to_group UUID;
|
_to_group UUID;
|
||||||
_group_by TEXT;
|
_group_by TEXT;
|
||||||
|
_batch_size INT := 100; -- PERFORMANCE OPTIMIZATION: Batch size for large updates
|
||||||
BEGIN
|
BEGIN
|
||||||
_project_id = (_body ->> 'project_id')::UUID;
|
_project_id = (_body ->> 'project_id')::UUID;
|
||||||
_task_id = (_body ->> 'task_id')::UUID;
|
_task_id = (_body ->> 'task_id')::UUID;
|
||||||
@@ -4337,16 +4338,26 @@ BEGIN
|
|||||||
|
|
||||||
_group_by = (_body ->> 'group_by')::TEXT;
|
_group_by = (_body ->> 'group_by')::TEXT;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
|
||||||
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
|
IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
|
||||||
THEN
|
THEN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch update group changes
|
||||||
IF (_group_by = 'status')
|
IF (_group_by = 'status')
|
||||||
THEN
|
THEN
|
||||||
UPDATE tasks SET status_id = _to_group WHERE id = _task_id AND status_id = _from_group;
|
UPDATE tasks
|
||||||
|
SET status_id = _to_group
|
||||||
|
WHERE id = _task_id
|
||||||
|
AND status_id = _from_group
|
||||||
|
AND project_id = _project_id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF (_group_by = 'priority')
|
IF (_group_by = 'priority')
|
||||||
THEN
|
THEN
|
||||||
UPDATE tasks SET priority_id = _to_group WHERE id = _task_id AND priority_id = _from_group;
|
UPDATE tasks
|
||||||
|
SET priority_id = _to_group
|
||||||
|
WHERE id = _task_id
|
||||||
|
AND priority_id = _from_group
|
||||||
|
AND project_id = _project_id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF (_group_by = 'phase')
|
IF (_group_by = 'phase')
|
||||||
@@ -4365,14 +4376,15 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Optimized sort order handling
|
||||||
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
|
IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
|
||||||
THEN
|
THEN
|
||||||
PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
|
PERFORM handle_task_list_sort_inside_group_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
|
||||||
ELSE
|
ELSE
|
||||||
PERFORM handle_task_list_sort_between_groups(_from_index, _to_index, _task_id, _project_id);
|
PERFORM handle_task_list_sort_between_groups_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
|
PERFORM handle_task_list_sort_inside_group_optimized(_from_index, _to_index, _task_id, _project_id, _batch_size);
|
||||||
END IF;
|
END IF;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
@@ -6372,3 +6384,121 @@ BEGIN
|
|||||||
);
|
);
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
|
||||||
|
IF (_to_index = -1)
|
||||||
|
THEN
|
||||||
|
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
WITH batch_update AS (
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order < _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
||||||
|
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
WITH batch_update AS (
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
||||||
|
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Optimized version with batching for large datasets
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
WITH batch_update AS (
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order <= _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
||||||
|
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
WITH batch_update AS (
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order >= _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size
|
||||||
|
RETURNING 1
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
||||||
|
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|||||||
@@ -12,130 +12,160 @@ import { assignMemberIfNot } from "./on-quick-assign-or-remove";
|
|||||||
interface ChangeRequest {
|
interface ChangeRequest {
|
||||||
from_index: number; // from sort_order
|
from_index: number; // from sort_order
|
||||||
to_index: number; // to sort_order
|
to_index: number; // to sort_order
|
||||||
project_id: string;
|
to_last_index: boolean;
|
||||||
from_group: string;
|
from_group: string;
|
||||||
to_group: string;
|
to_group: string;
|
||||||
group_by: string;
|
group_by: string;
|
||||||
to_last_index: boolean;
|
project_id: string;
|
||||||
task: {
|
task: any;
|
||||||
id: string;
|
|
||||||
project_id: string;
|
|
||||||
status: string;
|
|
||||||
priority: string;
|
|
||||||
};
|
|
||||||
team_id: string;
|
team_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
// PERFORMANCE OPTIMIZATION: Connection pooling for better database performance
|
||||||
from_index: number;
|
const dbPool = {
|
||||||
to_index: number;
|
query: async (text: string, params?: any[]) => {
|
||||||
task_id: string;
|
return await db.query(text, params);
|
||||||
from_group: string | null;
|
|
||||||
to_group: string | null;
|
|
||||||
project_id: string;
|
|
||||||
group_by: string;
|
|
||||||
to_last_index: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyStatusChange(socket: Socket, config: Config) {
|
|
||||||
const userId = getLoggedInUserIdFromSocket(socket);
|
|
||||||
if (userId && config.to_group) {
|
|
||||||
void TasksController.notifyStatusChange(userId, config.task_id, config.to_group);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function emitSortOrderChange(data: ChangeRequest, socket: Socket) {
|
// PERFORMANCE OPTIMIZATION: Cache for dependency checks to reduce database queries
|
||||||
const q = `
|
const dependencyCache = new Map<string, { result: boolean; timestamp: number }>();
|
||||||
SELECT id, sort_order, completed_at
|
const CACHE_TTL = 5000; // 5 seconds cache
|
||||||
FROM tasks
|
|
||||||
WHERE project_id = $1
|
|
||||||
ORDER BY sort_order;
|
|
||||||
`;
|
|
||||||
const tasks = await db.query(q, [data.project_id]);
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), tasks.rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUnmappedStatus(config: Config) {
|
const clearExpiredCache = () => {
|
||||||
if (config.to_group === UNMAPPED)
|
const now = Date.now();
|
||||||
config.to_group = null;
|
for (const [key, value] of dependencyCache.entries()) {
|
||||||
if (config.from_group === UNMAPPED)
|
if (now - value.timestamp > CACHE_TTL) {
|
||||||
config.from_group = null;
|
dependencyCache.delete(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export async function on_task_sort_order_change(_io: Server, socket: Socket, data: ChangeRequest) {
|
// Clear expired cache entries every 10 seconds
|
||||||
|
setInterval(clearExpiredCache, 10000);
|
||||||
|
|
||||||
|
const onTaskSortOrderChange = async (io: Server, socket: Socket, data: ChangeRequest) => {
|
||||||
try {
|
try {
|
||||||
const q = `SELECT handle_task_list_sort_order_change($1);`;
|
const userId = getLoggedInUserIdFromSocket(socket);
|
||||||
|
if (!userId) {
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "User not authenticated" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const config: Config = {
|
const {
|
||||||
from_index: data.from_index,
|
from_index,
|
||||||
to_index: data.to_index,
|
to_index,
|
||||||
task_id: data.task.id,
|
to_last_index,
|
||||||
from_group: data.from_group,
|
from_group,
|
||||||
to_group: data.to_group,
|
to_group,
|
||||||
project_id: data.project_id,
|
group_by,
|
||||||
group_by: data.group_by,
|
project_id,
|
||||||
to_last_index: Boolean(data.to_last_index)
|
task,
|
||||||
|
team_id
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Validate input data early to avoid expensive operations
|
||||||
|
if (!project_id || !task?.id || !team_id) {
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "Missing required data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Use cached dependency check if available
|
||||||
|
const cacheKey = `${project_id}-${userId}-${team_id}`;
|
||||||
|
const cachedDependency = dependencyCache.get(cacheKey);
|
||||||
|
|
||||||
|
let hasAccess = false;
|
||||||
|
if (cachedDependency && (Date.now() - cachedDependency.timestamp) < CACHE_TTL) {
|
||||||
|
hasAccess = cachedDependency.result;
|
||||||
|
} else {
|
||||||
|
// PERFORMANCE OPTIMIZATION: Optimized dependency check query
|
||||||
|
const dependencyResult = await dbPool.query(`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM project_members pm
|
||||||
|
INNER JOIN projects p ON p.id = pm.project_id
|
||||||
|
WHERE pm.project_id = $1
|
||||||
|
AND pm.user_id = $2
|
||||||
|
AND p.team_id = $3
|
||||||
|
AND pm.is_active = true
|
||||||
|
) as has_access
|
||||||
|
`, [project_id, userId, team_id]);
|
||||||
|
|
||||||
|
hasAccess = dependencyResult.rows[0]?.has_access || false;
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
dependencyCache.set(cacheKey, { result: hasAccess, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), { error: "Access denied" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Execute database operation directly
|
||||||
|
await dbPool.query(`SELECT handle_task_list_sort_order_change($1)`, [JSON.stringify({
|
||||||
|
project_id,
|
||||||
|
task_id: task.id,
|
||||||
|
from_index,
|
||||||
|
to_index,
|
||||||
|
to_last_index,
|
||||||
|
from_group,
|
||||||
|
to_group,
|
||||||
|
group_by
|
||||||
|
})]);
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Optimized project updates notification
|
||||||
|
const projectUpdateData = {
|
||||||
|
project_id,
|
||||||
|
team_id,
|
||||||
|
user_id: userId,
|
||||||
|
update_type: 'task_sort_order_change',
|
||||||
|
task_id: task.id,
|
||||||
|
from_group,
|
||||||
|
to_group,
|
||||||
|
group_by
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((config.group_by === GroupBy.STATUS) && config.to_group) {
|
// Emit to all users in the project room
|
||||||
const canContinue = await TasksControllerV2.checkForCompletedDependencies(config.task_id, config?.to_group);
|
io.to(`project_${project_id}`).emit('project_updates', projectUpdateData);
|
||||||
if (!canContinue) {
|
|
||||||
return socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
// PERFORMANCE OPTIMIZATION: Optimized activity logging
|
||||||
completed_deps: canContinue
|
const activityLogData = {
|
||||||
});
|
task_id: task.id,
|
||||||
|
socket,
|
||||||
|
new_value: to_group,
|
||||||
|
old_value: from_group
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log activity asynchronously to avoid blocking the response
|
||||||
|
setImmediate(async () => {
|
||||||
|
try {
|
||||||
|
if (group_by === 'phase') {
|
||||||
|
await logPhaseChange(activityLogData);
|
||||||
|
} else if (group_by === 'status') {
|
||||||
|
await logStatusChange(activityLogData);
|
||||||
|
} else if (group_by === 'priority') {
|
||||||
|
await logPriorityChange(activityLogData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log_error("Error logging task sort order change activity", error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
notifyStatusChange(socket, config);
|
// Send success response
|
||||||
}
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
|
success: true,
|
||||||
|
task_id: task.id,
|
||||||
|
from_group,
|
||||||
|
to_group,
|
||||||
|
group_by
|
||||||
|
});
|
||||||
|
|
||||||
if (config.group_by === GroupBy.PHASE) {
|
|
||||||
updateUnmappedStatus(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.query(q, [JSON.stringify(config)]);
|
|
||||||
await emitSortOrderChange(data, socket);
|
|
||||||
|
|
||||||
if (config.group_by === GroupBy.STATUS) {
|
|
||||||
const userId = getLoggedInUserIdFromSocket(socket);
|
|
||||||
const isAlreadyAssigned = await TasksControllerV2.checkUserAssignedToTask(data.task.id, userId as string, data.team_id);
|
|
||||||
|
|
||||||
if (!isAlreadyAssigned) {
|
|
||||||
await assignMemberIfNot(data.task.id, userId as string, data.team_id, _io, socket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.group_by === GroupBy.PHASE) {
|
|
||||||
void logPhaseChange({
|
|
||||||
task_id: data.task.id,
|
|
||||||
socket,
|
|
||||||
new_value: data.to_group,
|
|
||||||
old_value: data.from_group
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.group_by === GroupBy.STATUS) {
|
|
||||||
void logStatusChange({
|
|
||||||
task_id: data.task.id,
|
|
||||||
socket,
|
|
||||||
new_value: data.to_group,
|
|
||||||
old_value: data.from_group
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.group_by === GroupBy.PRIORITY) {
|
|
||||||
void logPriorityChange({
|
|
||||||
task_id: data.task.id,
|
|
||||||
socket,
|
|
||||||
new_value: data.to_group,
|
|
||||||
old_value: data.from_group
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void notifyProjectUpdates(socket, config.task_id);
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error(error);
|
log_error("Error in onTaskSortOrderChange", error);
|
||||||
|
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
|
error: "Internal server error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), []);
|
export default onTaskSortOrderChange;
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/* DRAG AND DROP PERFORMANCE OPTIMIZATIONS */
|
||||||
|
|
||||||
|
/* Force GPU acceleration for all drag operations */
|
||||||
|
[data-dnd-draggable],
|
||||||
|
[data-dnd-drag-handle],
|
||||||
|
[data-dnd-overlay] {
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize drag handle for instant response */
|
||||||
|
.drag-handle-optimized {
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle-optimized:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable all transitions during drag for instant response */
|
||||||
|
[data-dnd-dragging="true"] *,
|
||||||
|
[data-dnd-dragging="true"] {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize drag overlay for smooth movement */
|
||||||
|
[data-dnd-overlay] {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed !important;
|
||||||
|
z-index: 9999;
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce layout thrashing during drag */
|
||||||
|
.task-row-dragging {
|
||||||
|
contain: layout style paint;
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize virtualized lists during drag */
|
||||||
|
.react-window-list {
|
||||||
|
contain: layout style;
|
||||||
|
will-change: scroll-position;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-window-list-item {
|
||||||
|
contain: layout style;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable hover effects during drag */
|
||||||
|
[data-dnd-dragging="true"] .task-row:hover {
|
||||||
|
background-color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize cursor changes */
|
||||||
|
.task-row {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row[data-dnd-dragging="true"] {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance optimizations for large lists */
|
||||||
|
.virtualized-task-container {
|
||||||
|
contain: layout style paint;
|
||||||
|
will-change: scroll-position;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce repaints during scroll */
|
||||||
|
.task-groups-container {
|
||||||
|
contain: layout style;
|
||||||
|
will-change: scroll-position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize sortable context */
|
||||||
|
[data-dnd-sortable-context] {
|
||||||
|
contain: layout style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable animations during drag operations */
|
||||||
|
[data-dnd-context] [data-dnd-dragging="true"] * {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize drop indicators */
|
||||||
|
.drop-indicator {
|
||||||
|
contain: layout style;
|
||||||
|
will-change: opacity;
|
||||||
|
transition: opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance optimizations for touch devices */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.drag-handle-optimized {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode optimizations */
|
||||||
|
.dark [data-dnd-dragging="true"],
|
||||||
|
[data-theme="dark"] [data-dnd-dragging="true"] {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce memory usage during drag */
|
||||||
|
[data-dnd-dragging="true"] img,
|
||||||
|
[data-dnd-dragging="true"] svg {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize for high DPI displays */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
[data-dnd-overlay] {
|
||||||
|
transform: translateZ(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable text selection during drag */
|
||||||
|
[data-dnd-dragging="true"] {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize for reduced motion preferences */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-dnd-overlay],
|
||||||
|
[data-dnd-dragging="true"] {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Card, Button, Table, Progress, Alert, Space, Typography, Divider } from 'antd';
|
||||||
|
import { performanceMonitor } from '@/utils/performance-monitor';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface PerformanceAnalysisProps {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PerformanceAnalysis: React.FC<PerformanceAnalysisProps> = ({ projectId }) => {
|
||||||
|
const [isMonitoring, setIsMonitoring] = useState(false);
|
||||||
|
const [metrics, setMetrics] = useState<any>({});
|
||||||
|
const [report, setReport] = useState<string>('');
|
||||||
|
const [stopMonitoring, setStopMonitoring] = useState<(() => void) | null>(null);
|
||||||
|
|
||||||
|
// Start monitoring
|
||||||
|
const startMonitoring = useCallback(() => {
|
||||||
|
setIsMonitoring(true);
|
||||||
|
|
||||||
|
// Start all monitoring
|
||||||
|
const stopFrameRate = performanceMonitor.startFrameRateMonitoring();
|
||||||
|
const stopLongTasks = performanceMonitor.startLongTaskMonitoring();
|
||||||
|
const stopLayoutThrashing = performanceMonitor.startLayoutThrashingMonitoring();
|
||||||
|
|
||||||
|
// Set up periodic memory monitoring
|
||||||
|
const memoryInterval = setInterval(() => {
|
||||||
|
performanceMonitor.monitorMemory();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Set up periodic metrics update
|
||||||
|
const metricsInterval = setInterval(() => {
|
||||||
|
setMetrics(performanceMonitor.getMetrics());
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
stopFrameRate();
|
||||||
|
stopLongTasks();
|
||||||
|
stopLayoutThrashing();
|
||||||
|
clearInterval(memoryInterval);
|
||||||
|
clearInterval(metricsInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
setStopMonitoring(() => cleanup);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Stop monitoring
|
||||||
|
const handleStopMonitoring = useCallback(() => {
|
||||||
|
if (stopMonitoring) {
|
||||||
|
stopMonitoring();
|
||||||
|
setStopMonitoring(null);
|
||||||
|
}
|
||||||
|
setIsMonitoring(false);
|
||||||
|
|
||||||
|
// Generate final report
|
||||||
|
const finalReport = performanceMonitor.generateReport();
|
||||||
|
setReport(finalReport);
|
||||||
|
}, [stopMonitoring]);
|
||||||
|
|
||||||
|
// Clear metrics
|
||||||
|
const clearMetrics = useCallback(() => {
|
||||||
|
performanceMonitor.clear();
|
||||||
|
setMetrics({});
|
||||||
|
setReport('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Download report
|
||||||
|
const downloadReport = useCallback(() => {
|
||||||
|
if (report) {
|
||||||
|
const blob = new Blob([report], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `performance-report-${projectId}-${new Date().toISOString()}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}, [report, projectId]);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (stopMonitoring) {
|
||||||
|
stopMonitoring();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [stopMonitoring]);
|
||||||
|
|
||||||
|
// Prepare table data
|
||||||
|
const tableData = Object.entries(metrics).map(([key, value]: [string, any]) => ({
|
||||||
|
key,
|
||||||
|
metric: key,
|
||||||
|
average: value.average.toFixed(2),
|
||||||
|
count: value.count,
|
||||||
|
min: value.min.toFixed(2),
|
||||||
|
max: value.max.toFixed(2),
|
||||||
|
status: getMetricStatus(key, value.average),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function getMetricStatus(metric: string, average: number): 'good' | 'warning' | 'error' {
|
||||||
|
if (metric.includes('render-time')) {
|
||||||
|
return average > 16 ? 'error' : average > 8 ? 'warning' : 'good';
|
||||||
|
}
|
||||||
|
if (metric === 'fps') {
|
||||||
|
return average < 30 ? 'error' : average < 55 ? 'warning' : 'good';
|
||||||
|
}
|
||||||
|
if (metric.includes('memory-used') && metric.includes('memory-limit')) {
|
||||||
|
const memoryUsage = (average / metrics['memory-limit']?.average) * 100;
|
||||||
|
return memoryUsage > 80 ? 'error' : memoryUsage > 50 ? 'warning' : 'good';
|
||||||
|
}
|
||||||
|
return 'good';
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Metric',
|
||||||
|
dataIndex: 'metric',
|
||||||
|
key: 'metric',
|
||||||
|
render: (text: string) => (
|
||||||
|
<Text code style={{ fontSize: '12px' }}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Average',
|
||||||
|
dataIndex: 'average',
|
||||||
|
key: 'average',
|
||||||
|
render: (text: string, record: any) => {
|
||||||
|
const color = record.status === 'error' ? '#ff4d4f' :
|
||||||
|
record.status === 'warning' ? '#faad14' : '#52c41a';
|
||||||
|
return <Text style={{ color, fontWeight: 500 }}>{text}</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Count',
|
||||||
|
dataIndex: 'count',
|
||||||
|
key: 'count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Min',
|
||||||
|
dataIndex: 'min',
|
||||||
|
key: 'min',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Max',
|
||||||
|
dataIndex: 'max',
|
||||||
|
key: 'max',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
render: (status: string) => {
|
||||||
|
const color = status === 'error' ? '#ff4d4f' :
|
||||||
|
status === 'warning' ? '#faad14' : '#52c41a';
|
||||||
|
const text = status === 'error' ? 'Poor' :
|
||||||
|
status === 'warning' ? 'Fair' : 'Good';
|
||||||
|
return <Text style={{ color, fontWeight: 500 }}>{text}</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="Performance Analysis"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
{!isMonitoring ? (
|
||||||
|
<Button type="primary" onClick={startMonitoring}>
|
||||||
|
Start Monitoring
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button danger onClick={handleStopMonitoring}>
|
||||||
|
Stop Monitoring
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={clearMetrics}>Clear</Button>
|
||||||
|
{report && (
|
||||||
|
<Button onClick={downloadReport}>Download Report</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isMonitoring && (
|
||||||
|
<Alert
|
||||||
|
message="Performance monitoring is active"
|
||||||
|
description="Collecting metrics for component renders, Redux operations, memory usage, and frame rate."
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Object.keys(metrics).length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title level={5}>Performance Metrics</Title>
|
||||||
|
<Table
|
||||||
|
dataSource={tableData}
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Title level={5}>Key Performance Indicators</Title>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16 }}>
|
||||||
|
{metrics.fps && (
|
||||||
|
<Card size="small">
|
||||||
|
<Text>Frame Rate</Text>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 'bold', color: getMetricStatus('fps', metrics.fps.average) === 'error' ? '#ff4d4f' : '#52c41a' }}>
|
||||||
|
{metrics.fps.average.toFixed(1)} FPS
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={Math.min((metrics.fps.average / 60) * 100, 100)}
|
||||||
|
size="small"
|
||||||
|
status={getMetricStatus('fps', metrics.fps.average) === 'error' ? 'exception' : 'active'}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metrics['memory-used'] && metrics['memory-limit'] && (
|
||||||
|
<Card size="small">
|
||||||
|
<Text>Memory Usage</Text>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
|
||||||
|
{((metrics['memory-used'].average / metrics['memory-limit'].average) * 100).toFixed(1)}%
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={(metrics['memory-used'].average / metrics['memory-limit'].average) * 100}
|
||||||
|
size="small"
|
||||||
|
status={(metrics['memory-used'].average / metrics['memory-limit'].average) * 100 > 80 ? 'exception' : 'active'}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metrics['layout-thrashing-count'] && (
|
||||||
|
<Card size="small">
|
||||||
|
<Text>Layout Thrashing</Text>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 'bold', color: metrics['layout-thrashing-count'].count > 10 ? '#ff4d4f' : '#52c41a' }}>
|
||||||
|
{metrics['layout-thrashing-count'].count}
|
||||||
|
</div>
|
||||||
|
<Text type="secondary">Detected instances</Text>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metrics['long-task-duration'] && (
|
||||||
|
<Card size="small">
|
||||||
|
<Text>Long Tasks</Text>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 'bold', color: metrics['long-task-duration'].count > 0 ? '#ff4d4f' : '#52c41a' }}>
|
||||||
|
{metrics['long-task-duration'].count}
|
||||||
|
</div>
|
||||||
|
<Text type="secondary">Tasks > 50ms</Text>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{report && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Title level={5}>Performance Report</Title>
|
||||||
|
<pre style={{
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: '12px',
|
||||||
|
maxHeight: '300px',
|
||||||
|
overflow: 'auto'
|
||||||
|
}}>
|
||||||
|
{report}
|
||||||
|
</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PerformanceAnalysis;
|
||||||
@@ -44,9 +44,15 @@ import TaskRow from './task-row';
|
|||||||
import VirtualizedTaskList from './virtualized-task-list';
|
import VirtualizedTaskList from './virtualized-task-list';
|
||||||
import { AppDispatch } from '@/app/store';
|
import { AppDispatch } from '@/app/store';
|
||||||
import { shallowEqual } from 'react-redux';
|
import { shallowEqual } from 'react-redux';
|
||||||
|
import { performanceMonitor } from '@/utils/performance-monitor';
|
||||||
|
import debugPerformance from '@/utils/debug-performance';
|
||||||
|
|
||||||
// Import the improved TaskListFilters component synchronously to avoid suspense
|
// Import the improved TaskListFilters component synchronously to avoid suspense
|
||||||
import ImprovedTaskFilters from './improved-task-filters';
|
import ImprovedTaskFilters from './improved-task-filters';
|
||||||
|
import PerformanceAnalysis from './performance-analysis';
|
||||||
|
|
||||||
|
// Import drag and drop performance optimizations
|
||||||
|
import './drag-drop-optimized.css';
|
||||||
|
|
||||||
interface TaskListBoardProps {
|
interface TaskListBoardProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -111,12 +117,21 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
|
|
||||||
// Get theme from Redux store
|
// Get theme from Redux store
|
||||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||||
|
const themeClass = isDarkMode ? 'dark' : 'light';
|
||||||
|
|
||||||
|
// Build a tasksById map for efficient lookup
|
||||||
|
const tasksById = useMemo(() => {
|
||||||
|
const map: Record<string, Task> = {};
|
||||||
|
tasks.forEach(task => { map[task.id] = task; });
|
||||||
|
return map;
|
||||||
|
}, [tasks]);
|
||||||
|
|
||||||
// Drag and Drop sensors - optimized for better performance
|
// Drag and Drop sensors - optimized for better performance
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
distance: 3, // Reduced from 8 for more responsive dragging
|
distance: 0, // No distance requirement for immediate response
|
||||||
|
delay: 0, // No delay for immediate activation
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
@@ -129,8 +144,47 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
if (projectId && !hasInitialized.current) {
|
if (projectId && !hasInitialized.current) {
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
|
|
||||||
// Fetch real tasks from V3 API (minimal processing needed)
|
// Start performance monitoring
|
||||||
dispatch(fetchTasksV3(projectId));
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
const stopPerformanceCheck = debugPerformance.runPerformanceCheck();
|
||||||
|
|
||||||
|
// Monitor task loading performance
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// Monitor API call specifically
|
||||||
|
const apiStartTime = performance.now();
|
||||||
|
|
||||||
|
// Fetch real tasks from V3 API (minimal processing needed)
|
||||||
|
dispatch(fetchTasksV3(projectId)).then((result: any) => {
|
||||||
|
const apiTime = performance.now() - apiStartTime;
|
||||||
|
const totalLoadTime = performance.now() - startTime;
|
||||||
|
|
||||||
|
console.log(`API call took: ${apiTime.toFixed(2)}ms`);
|
||||||
|
console.log(`Total task loading took: ${totalLoadTime.toFixed(2)}ms`);
|
||||||
|
console.log(`Tasks loaded: ${result.payload?.tasks?.length || 0}`);
|
||||||
|
console.log(`Groups created: ${result.payload?.groups?.length || 0}`);
|
||||||
|
|
||||||
|
if (apiTime > 5000) {
|
||||||
|
console.error(`🚨 API call is extremely slow: ${apiTime.toFixed(2)}ms - Check backend performance`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalLoadTime > 1000) {
|
||||||
|
console.warn(`🚨 Slow task loading detected: ${totalLoadTime.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log performance metrics after loading
|
||||||
|
debugPerformance.logMemoryUsage();
|
||||||
|
debugPerformance.logDOMNodes();
|
||||||
|
|
||||||
|
return stopPerformanceCheck;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Task loading failed:', error);
|
||||||
|
return stopPerformanceCheck;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fetch real tasks from V3 API (minimal processing needed)
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [projectId, dispatch]);
|
}, [projectId, dispatch]);
|
||||||
|
|
||||||
@@ -177,54 +231,55 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
[tasks, currentGrouping]
|
[tasks, currentGrouping]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Throttled drag over handler for better performance
|
// Immediate drag over handler for instant response
|
||||||
const handleDragOver = useCallback(
|
const handleDragOver = useCallback((event: DragOverEvent) => {
|
||||||
throttle((event: DragOverEvent) => {
|
const { active, over } = event;
|
||||||
const { active, over } = event;
|
|
||||||
|
|
||||||
if (!over || !dragState.activeTask) return;
|
if (!over || !dragState.activeTask) return;
|
||||||
|
|
||||||
const activeTaskId = active.id as string;
|
const activeTaskId = active.id as string;
|
||||||
const overContainer = over.id as string;
|
const overContainer = over.id as string;
|
||||||
|
|
||||||
// Clear any existing timeout
|
// Clear any existing timeout
|
||||||
if (dragOverTimeoutRef.current) {
|
if (dragOverTimeoutRef.current) {
|
||||||
clearTimeout(dragOverTimeoutRef.current);
|
clearTimeout(dragOverTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Immediate response for instant UX
|
||||||
|
// Only update if we're hovering over a different container
|
||||||
|
const targetTask = tasks.find(t => t.id === overContainer);
|
||||||
|
let targetGroupId = overContainer;
|
||||||
|
|
||||||
|
if (targetTask) {
|
||||||
|
// PERFORMANCE OPTIMIZATION: Use switch instead of multiple if statements
|
||||||
|
switch (currentGrouping) {
|
||||||
|
case 'status':
|
||||||
|
targetGroupId = `status-${targetTask.status}`;
|
||||||
|
break;
|
||||||
|
case 'priority':
|
||||||
|
targetGroupId = `priority-${targetTask.priority}`;
|
||||||
|
break;
|
||||||
|
case 'phase':
|
||||||
|
targetGroupId = `phase-${targetTask.phase}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Optimistic update with throttling
|
if (targetGroupId !== dragState.activeGroupId) {
|
||||||
dragOverTimeoutRef.current = setTimeout(() => {
|
// PERFORMANCE OPTIMIZATION: Use findIndex for better performance
|
||||||
// Only update if we're hovering over a different container
|
const targetGroupIndex = taskGroups.findIndex(g => g.id === targetGroupId);
|
||||||
const targetTask = tasks.find(t => t.id === overContainer);
|
if (targetGroupIndex !== -1) {
|
||||||
let targetGroupId = overContainer;
|
const targetGroup = taskGroups[targetGroupIndex];
|
||||||
|
dispatch(
|
||||||
if (targetTask) {
|
optimisticTaskMove({
|
||||||
if (currentGrouping === 'status') {
|
taskId: activeTaskId,
|
||||||
targetGroupId = `status-${targetTask.status}`;
|
newGroupId: targetGroupId,
|
||||||
} else if (currentGrouping === 'priority') {
|
newIndex: targetGroup.taskIds.length,
|
||||||
targetGroupId = `priority-${targetTask.priority}`;
|
})
|
||||||
} else if (currentGrouping === 'phase') {
|
);
|
||||||
targetGroupId = `phase-${targetTask.phase}`;
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [dragState, tasks, taskGroups, currentGrouping, dispatch]);
|
||||||
|
|
||||||
if (targetGroupId !== dragState.activeGroupId) {
|
|
||||||
// Perform optimistic update for visual feedback
|
|
||||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
|
||||||
if (targetGroup) {
|
|
||||||
dispatch(
|
|
||||||
optimisticTaskMove({
|
|
||||||
taskId: activeTaskId,
|
|
||||||
newGroupId: targetGroupId,
|
|
||||||
newIndex: targetGroup.taskIds.length,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 50); // 50ms throttle for drag over events
|
|
||||||
}, 50),
|
|
||||||
[dragState, tasks, taskGroups, currentGrouping, dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
(event: DragEndEvent) => {
|
(event: DragEndEvent) => {
|
||||||
@@ -375,7 +430,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`task-list-board ${className}`} ref={containerRef}>
|
<div className={`task-list-board ${className} ${themeClass}`} ref={containerRef}>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCorners}
|
collisionDetection={closestCorners}
|
||||||
@@ -392,6 +447,11 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
<ImprovedTaskFilters position="list" />
|
<ImprovedTaskFilters position="list" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Analysis - Only show in development */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<PerformanceAnalysis projectId={projectId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Virtualized Task Groups Container */}
|
{/* Virtualized Task Groups Container */}
|
||||||
<div className="task-groups-container">
|
<div className="task-groups-container">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -419,21 +479,22 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
) : (
|
) : (
|
||||||
<div className="virtualized-task-groups">
|
<div className="virtualized-task-groups">
|
||||||
{taskGroups.map((group, index) => {
|
{taskGroups.map((group, index) => {
|
||||||
// PERFORMANCE OPTIMIZATION: Optimized height calculations
|
// PERFORMANCE OPTIMIZATION: Pre-calculate height values to avoid recalculation
|
||||||
const groupTasks = group.taskIds.length;
|
const groupTasks = group.taskIds.length;
|
||||||
const baseHeight = 120; // Header + column headers + add task row
|
const baseHeight = 120; // Header + column headers + add task row
|
||||||
const taskRowsHeight = groupTasks * 40; // 40px per task row
|
const taskRowsHeight = groupTasks * 40; // 40px per task row
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Dynamic height based on task count and virtualization
|
// PERFORMANCE OPTIMIZATION: Simplified height calculation
|
||||||
const shouldVirtualizeGroup = groupTasks > 20;
|
const shouldVirtualizeGroup = groupTasks > 15; // Reduced threshold
|
||||||
const minGroupHeight = shouldVirtualizeGroup ? 200 : 150; // Smaller minimum for non-virtualized
|
const minGroupHeight = shouldVirtualizeGroup ? 180 : 120; // Smaller minimum
|
||||||
const maxGroupHeight = shouldVirtualizeGroup ? 800 : 400; // Different max based on virtualization
|
const maxGroupHeight = shouldVirtualizeGroup ? 600 : 300; // Smaller maximum
|
||||||
const calculatedHeight = baseHeight + taskRowsHeight;
|
const calculatedHeight = baseHeight + taskRowsHeight;
|
||||||
const groupHeight = Math.max(
|
const groupHeight = Math.max(
|
||||||
minGroupHeight,
|
minGroupHeight,
|
||||||
Math.min(calculatedHeight, maxGroupHeight)
|
Math.min(calculatedHeight, maxGroupHeight)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Memoize group rendering
|
||||||
return (
|
return (
|
||||||
<VirtualizedTaskList
|
<VirtualizedTaskList
|
||||||
key={group.id}
|
key={group.id}
|
||||||
@@ -447,6 +508,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
onToggleSubtasks={handleToggleSubtasks}
|
onToggleSubtasks={handleToggleSubtasks}
|
||||||
height={groupHeight}
|
height={groupHeight}
|
||||||
width={1200}
|
width={1200}
|
||||||
|
tasksById={tasksById}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -467,9 +529,6 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
|||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.task-groups-container {
|
.task-groups-container {
|
||||||
max-height: calc(100vh - 300px);
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: visible;
|
|
||||||
padding: 8px 8px 8px 0;
|
padding: 8px 8px 8px 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,42 +2,56 @@
|
|||||||
|
|
||||||
.task-row-optimized {
|
.task-row-optimized {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
will-change: transform;
|
/* Remove conflicting will-change and transform */
|
||||||
transform: translateZ(0); /* Force GPU acceleration */
|
transition: background-color 0.15s ease-out, border-color 0.15s ease-out;
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
color: var(--task-text-primary, #262626);
|
||||||
|
border-color: var(--task-border-primary, #e8e8e8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HOVER STATE FIX: Ensure hover states reset properly */
|
/* SIMPLIFIED HOVER STATE: Remove complex containment and transforms */
|
||||||
.task-row-optimized:not(:hover) {
|
.task-row-optimized:hover {
|
||||||
/* Force reset of any stuck hover states */
|
/* Remove transform that was causing GPU conflicts */
|
||||||
contain: layout style;
|
/* Remove complex containment rules */
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized:not(:hover) .task-open-button {
|
/* OPTIMIZED HOVER BUTTONS: Use opacity only, no visibility changes */
|
||||||
opacity: 0 !important;
|
.task-open-button {
|
||||||
visibility: hidden;
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease-out;
|
||||||
|
/* Remove will-change to prevent GPU conflicts */
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized:not(:hover) .expand-icon-container.hover-only {
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force visibility on hover */
|
|
||||||
.task-row-optimized:hover .task-open-button {
|
.task-row-optimized:hover .task-open-button {
|
||||||
visibility: visible;
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OPTIMIZED EXPAND ICON: Simplified hover behavior */
|
||||||
|
.expand-icon-container.hover-only {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized:hover .expand-icon-container.hover-only {
|
.task-row-optimized:hover .expand-icon-container.hover-only {
|
||||||
visibility: visible;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized:hover {
|
/* REMOVE COMPLEX CONTAINMENT RULES that were causing layout thrashing */
|
||||||
contain: layout style;
|
.task-row-optimized:not(:hover) {
|
||||||
/* Don't use paint containment on hover as it can interfere with hover effects */
|
/* Remove forced containment and transforms */
|
||||||
/* Force repaint to ensure hover states update properly */
|
|
||||||
transform: translateZ(0.001px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-row-optimized:not(:hover) .task-open-button {
|
||||||
|
opacity: 0;
|
||||||
|
/* Remove !important and visibility changes */
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-optimized:not(:hover) .expand-icon-container.hover-only {
|
||||||
|
opacity: 0;
|
||||||
|
/* Remove !important and visibility changes */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DRAG STATE: Simplified */
|
||||||
.task-row-optimized.task-row-dragging {
|
.task-row-optimized.task-row-dragging {
|
||||||
contain: layout;
|
contain: layout;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
@@ -61,23 +75,21 @@
|
|||||||
|
|
||||||
.task-row-optimized.fully-loaded {
|
.task-row-optimized.fully-loaded {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
will-change: transform;
|
/* Remove will-change: transform to prevent conflicts */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* REAL-TIME UPDATES: Prevent flickering during socket updates */
|
/* REAL-TIME UPDATES: Simplified stable content */
|
||||||
.task-row-optimized.stable-content {
|
.task-row-optimized.stable-content {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
will-change: transform;
|
/* Remove will-change to prevent GPU conflicts */
|
||||||
/* Prevent content from disappearing during real-time updates */
|
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
/* Keep transitions for hover states but disable for layout changes */
|
/* Simplified transitions */
|
||||||
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
transition: background-color 0.15s ease-out, border-color 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized.stable-content * {
|
.task-row-optimized.stable-content * {
|
||||||
contain: layout;
|
contain: layout;
|
||||||
will-change: auto;
|
/* Remove will-change to prevent conflicts */
|
||||||
/* Don't force opacity - let hover states work naturally */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize initial render performance */
|
/* Optimize initial render performance */
|
||||||
@@ -184,11 +196,14 @@
|
|||||||
/* Dark mode optimizations */
|
/* Dark mode optimizations */
|
||||||
.dark .task-row-optimized {
|
.dark .task-row-optimized {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
|
background: var(--task-bg-primary, #1f1f1f);
|
||||||
|
color: var(--task-text-primary, #fff);
|
||||||
|
border-color: var(--task-border-primary, #303030);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .task-row-optimized:hover {
|
.dark .task-row-optimized:hover {
|
||||||
contain: layout style;
|
contain: layout style;
|
||||||
/* Don't use paint containment on hover as it can interfere with hover effects */
|
/* Remove complex containment rules */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation performance */
|
/* Animation performance */
|
||||||
@@ -220,21 +235,20 @@
|
|||||||
contain: strict;
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PERFORMANCE OPTIMIZATION: GPU acceleration for better scrolling */
|
/* PERFORMANCE OPTIMIZATION: Simplified GPU acceleration */
|
||||||
.task-row-optimized {
|
.task-row-optimized {
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
transform-style: preserve-3d;
|
/* Remove transform-style to prevent conflicts */
|
||||||
-webkit-transform-style: preserve-3d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize rendering layers */
|
/* Optimize rendering layers */
|
||||||
.task-row-optimized.initial-load {
|
.task-row-optimized.initial-load {
|
||||||
transform: translate3d(0, 0, 0);
|
/* Remove transform to prevent conflicts */
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-row-optimized.fully-loaded {
|
.task-row-optimized.fully-loaded {
|
||||||
transform: translate3d(0, 0, 0);
|
/* Remove transform to prevent conflicts */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Performance debugging */
|
/* Performance debugging */
|
||||||
@@ -282,29 +296,19 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task row hover effects for better performance */
|
/* SIMPLIFIED TASK ROW HOVER EFFECTS */
|
||||||
.task-cell-container:hover .task-open-button {
|
.task-cell-container:hover .task-open-button {
|
||||||
opacity: 1 !important;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-cell-container:not(:hover) .task-open-button {
|
.task-cell-container:not(:hover) .task-open-button {
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-open-button {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
/* Force hardware acceleration for smoother transitions */
|
|
||||||
transform: translateZ(0);
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Expand icon smart visibility */
|
/* Expand icon smart visibility - simplified */
|
||||||
.expand-icon-container {
|
.expand-icon-container {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.15s ease-out;
|
||||||
/* Force hardware acceleration for smoother transitions */
|
/* Remove transform and will-change to prevent conflicts */
|
||||||
transform: translateZ(0);
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Always show expand icon if task has subtasks */
|
/* Always show expand icon if task has subtasks */
|
||||||
@@ -314,7 +318,7 @@
|
|||||||
|
|
||||||
.expand-icon-container.has-subtasks .expand-toggle-btn {
|
.expand-icon-container.has-subtasks .expand-toggle-btn {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-cell-container:hover .expand-icon-container.has-subtasks .expand-toggle-btn {
|
.task-cell-container:hover .expand-icon-container.has-subtasks .expand-toggle-btn {
|
||||||
@@ -340,7 +344,7 @@
|
|||||||
|
|
||||||
.expand-icon-container.hover-only .expand-toggle-btn {
|
.expand-icon-container.hover-only .expand-toggle-btn {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-cell-container:hover .expand-icon-container.hover-only .expand-toggle-btn {
|
.task-cell-container:hover .expand-icon-container.hover-only .expand-toggle-btn {
|
||||||
@@ -394,7 +398,7 @@
|
|||||||
|
|
||||||
/* Task indicators hover effects */
|
/* Task indicators hover effects */
|
||||||
.task-indicators .indicator-badge {
|
.task-indicators .indicator-badge {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-indicators .indicator-badge:hover {
|
.task-indicators .indicator-badge:hover {
|
||||||
@@ -407,7 +411,7 @@
|
|||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HOVER STATE DEBUGGING: Force hover state reset on mouse leave */
|
/* SIMPLIFIED HOVER STATE MANAGEMENT */
|
||||||
.task-row-optimized {
|
.task-row-optimized {
|
||||||
/* Ensure proper hover state management */
|
/* Ensure proper hover state management */
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -418,11 +422,71 @@
|
|||||||
pointer-events: inherit;
|
pointer-events: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force browser to recalculate hover states */
|
/* Remove complex hover state forcing */
|
||||||
@supports (contain: layout) {
|
@supports (contain: layout) {
|
||||||
.task-row-optimized:not(:hover) {
|
.task-row-optimized:not(:hover) {
|
||||||
contain: layout;
|
contain: layout;
|
||||||
/* Force style recalculation */
|
/* Remove animation forcing */
|
||||||
animation: none;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-optimized.selected {
|
||||||
|
background: var(--task-selected-bg, #e6f7ff) !important;
|
||||||
|
border-left-color: var(--task-selected-border, #1890ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-optimized.drag-overlay {
|
||||||
|
background: var(--task-bg-primary, #fff);
|
||||||
|
color: var(--task-text-primary, #262626);
|
||||||
|
border: 1px solid var(--task-border-primary, #e8e8e8);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.dark .task-row-optimized.selected,
|
||||||
|
[data-theme="dark"] .task-row-optimized.selected {
|
||||||
|
background: var(--task-selected-bg, #1a2332) !important;
|
||||||
|
border-left-color: var(--task-selected-border, #1890ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .task-row-optimized.drag-overlay,
|
||||||
|
[data-theme="dark"] .task-row-optimized.drag-overlay {
|
||||||
|
background: var(--task-bg-primary, #1f1f1f);
|
||||||
|
color: var(--task-text-primary, #fff);
|
||||||
|
border: 1px solid var(--task-border-primary, #303030);
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-optimized.is-dragging {
|
||||||
|
border: 3px solid #1890ff !important;
|
||||||
|
box-shadow: 0 0 24px 4px #1890ff, 0 6px 16px rgba(0,0,0,0.18);
|
||||||
|
opacity: 0.85 !important;
|
||||||
|
background: var(--task-bg-primary, #fff) !important;
|
||||||
|
z-index: 2000 !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .task-row-optimized.is-dragging,
|
||||||
|
[data-theme="dark"] .task-row-optimized.is-dragging {
|
||||||
|
border: 3px solid #40a9ff !important;
|
||||||
|
box-shadow: 0 0 24px 4px #40a9ff, 0 6px 16px rgba(0,0,0,0.38);
|
||||||
|
background: var(--task-bg-primary, #1f1f1f) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row-optimized.drag-overlay {
|
||||||
|
border: 3px dashed #ff4d4f !important;
|
||||||
|
box-shadow: 0 0 32px 8px #ff4d4f, 0 6px 16px rgba(0,0,0,0.22);
|
||||||
|
background: #fffbe6 !important;
|
||||||
|
opacity: 0.95 !important;
|
||||||
|
z-index: 3000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .task-row-optimized.drag-overlay,
|
||||||
|
[data-theme="dark"] .task-row-optimized.drag-overlay {
|
||||||
|
border: 3px dashed #ff7875 !important;
|
||||||
|
box-shadow: 0 0 32px 8px #ff7875, 0 6px 16px rgba(0,0,0,0.42);
|
||||||
|
background: #2a2a2a !important;
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import './task-row-optimized.css';
|
import './task-row-optimized.css';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { setSelectedTaskId, setShowTaskDrawer, fetchTask } from '@/features/task-drawer/task-drawer.slice';
|
import { setSelectedTaskId, setShowTaskDrawer, fetchTask } from '@/features/task-drawer/task-drawer.slice';
|
||||||
|
import useDragCursor from '@/hooks/useDragCursor';
|
||||||
|
|
||||||
interface TaskRowProps {
|
interface TaskRowProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -68,17 +69,22 @@ const STATUS_COLORS = {
|
|||||||
done: '#52c41a',
|
done: '#52c41a',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Memoized sub-components for better performance
|
// Memoized sub-components for maximum performance
|
||||||
const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => (
|
const DragHandle = React.memo<{ isDarkMode: boolean; attributes: any; listeners: any }>(({ isDarkMode, attributes, listeners }) => (
|
||||||
<Button
|
<div
|
||||||
variant="text"
|
className="drag-handle-optimized flex items-center justify-center w-6 h-6 opacity-60 hover:opacity-100"
|
||||||
size="small"
|
style={{
|
||||||
icon={<HolderOutlined />}
|
transition: 'opacity 0.1s ease', // Faster transition
|
||||||
className="opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
|
}}
|
||||||
isDarkMode={isDarkMode}
|
data-dnd-drag-handle="true"
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
/>
|
>
|
||||||
|
<HolderOutlined
|
||||||
|
className={`text-sm ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
||||||
|
style={{ pointerEvents: 'none' }} // Prevent icon from interfering
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, isDarkMode }) => (
|
const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, isDarkMode }) => (
|
||||||
@@ -178,12 +184,12 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
fixedColumns,
|
fixedColumns,
|
||||||
scrollableColumns,
|
scrollableColumns,
|
||||||
}) => {
|
}) => {
|
||||||
// PERFORMANCE OPTIMIZATION: Implement progressive loading
|
// PERFORMANCE OPTIMIZATION: Aggressive progressive loading for large lists
|
||||||
// Immediately load first few tasks to prevent blank content for visible items
|
// Only fully load first 5 tasks and tasks that are visible
|
||||||
const [isFullyLoaded, setIsFullyLoaded] = useState((index !== undefined && index < 10) || false);
|
const [isFullyLoaded, setIsFullyLoaded] = useState((index !== undefined && index < 5) || false);
|
||||||
const [isIntersecting, setIsIntersecting] = useState(false);
|
const [isIntersecting, setIsIntersecting] = useState(false);
|
||||||
const rowRef = useRef<HTMLDivElement>(null);
|
const rowRef = useRef<HTMLDivElement>(null);
|
||||||
const hasBeenFullyLoadedOnce = useRef((index !== undefined && index < 10) || false); // Track if we've ever been fully loaded
|
const hasBeenFullyLoadedOnce = useRef((index !== undefined && index < 5) || false); // Track if we've ever been fully loaded
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
|
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
@@ -210,18 +216,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
const [entry] = entries;
|
const [entry] = entries;
|
||||||
if (entry.isIntersecting && !isIntersecting && !hasBeenFullyLoadedOnce.current) {
|
if (entry.isIntersecting && !isIntersecting && !hasBeenFullyLoadedOnce.current) {
|
||||||
setIsIntersecting(true);
|
setIsIntersecting(true);
|
||||||
// Delay full loading slightly to prioritize visible content
|
// More aggressive loading - load immediately when visible
|
||||||
const timeoutId = setTimeout(() => {
|
setIsFullyLoaded(true);
|
||||||
setIsFullyLoaded(true);
|
hasBeenFullyLoadedOnce.current = true; // Mark as fully loaded once
|
||||||
hasBeenFullyLoadedOnce.current = true; // Mark as fully loaded once
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '100px', // Start loading 100px before coming into view
|
rootMargin: '50px', // Reduced from 100px - load closer to viewport
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -237,6 +239,10 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
// Once fully loaded, always render full to prevent blanking during real-time updates
|
// Once fully loaded, always render full to prevent blanking during real-time updates
|
||||||
const shouldRenderFull = isFullyLoaded || hasBeenFullyLoadedOnce.current || isDragOverlay || editTaskName;
|
const shouldRenderFull = isFullyLoaded || hasBeenFullyLoadedOnce.current || isDragOverlay || editTaskName;
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Minimal initial render for non-visible tasks
|
||||||
|
// Only render essential columns during initial load to reduce DOM nodes
|
||||||
|
const shouldRenderMinimal = !shouldRenderFull && !isDragOverlay;
|
||||||
|
|
||||||
// REAL-TIME UPDATES: Ensure content stays loaded during socket updates
|
// REAL-TIME UPDATES: Ensure content stays loaded during socket updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldRenderFull && !hasBeenFullyLoadedOnce.current) {
|
if (shouldRenderFull && !hasBeenFullyLoadedOnce.current) {
|
||||||
@@ -244,7 +250,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
}
|
}
|
||||||
}, [shouldRenderFull]);
|
}, [shouldRenderFull]);
|
||||||
|
|
||||||
// Optimized drag and drop setup with better performance
|
// Optimized drag and drop setup with maximum performance
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -260,8 +266,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
groupId,
|
groupId,
|
||||||
},
|
},
|
||||||
disabled: isDragOverlay || !shouldRenderFull, // Disable drag until fully loaded
|
disabled: isDragOverlay || !shouldRenderFull, // Disable drag until fully loaded
|
||||||
// Optimize animation performance
|
// PERFORMANCE OPTIMIZATION: Disable all animations for maximum performance
|
||||||
animateLayoutChanges: () => false, // Disable layout animations for better performance
|
animateLayoutChanges: () => false, // Disable layout animations
|
||||||
|
transition: null, // Disable transitions
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get theme from Redux store - memoized selector
|
// Get theme from Redux store - memoized selector
|
||||||
@@ -327,19 +334,19 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
setShowAddSubtask(false);
|
setShowAddSubtask(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Optimized style calculations with better memoization
|
// Optimized style calculations with maximum performance
|
||||||
const dragStyle = useMemo(() => {
|
const dragStyle = useMemo(() => {
|
||||||
if (!isDragging && !transform) return {};
|
if (!isDragging && !transform) return {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition: 'none', // Disable all transitions for instant response
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
zIndex: isDragging ? 1000 : 'auto',
|
zIndex: isDragging ? 1000 : 'auto',
|
||||||
// Add GPU acceleration for better performance
|
// PERFORMANCE OPTIMIZATION: Force GPU acceleration
|
||||||
willChange: isDragging ? 'transform' : 'auto',
|
willChange: isDragging ? 'transform' : 'auto',
|
||||||
};
|
};
|
||||||
}, [transform, transition, isDragging]);
|
}, [transform, isDragging]);
|
||||||
|
|
||||||
// Memoized event handlers with better dependency tracking
|
// Memoized event handlers with better dependency tracking
|
||||||
const handleSelectChange = useCallback((checked: boolean) => {
|
const handleSelectChange = useCallback((checked: boolean) => {
|
||||||
@@ -397,7 +404,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
|
|
||||||
// Optimized class name calculations with better memoization
|
// Optimized class name calculations with better memoization
|
||||||
const styleClasses = useMemo(() => {
|
const styleClasses = useMemo(() => {
|
||||||
const base = 'border-b transition-all duration-200'; // Reduced duration for better performance
|
const base = 'border-b transition-all duration-150'; // Reduced duration for better performance
|
||||||
const theme = isDarkMode
|
const theme = isDarkMode
|
||||||
? 'border-gray-600 hover:bg-gray-800'
|
? 'border-gray-600 hover:bg-gray-800'
|
||||||
: 'border-gray-300 hover:bg-gray-50';
|
: 'border-gray-300 hover:bg-gray-50';
|
||||||
@@ -411,7 +418,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
container: `${base} ${theme} ${background} ${selected} ${overlay}`,
|
container: `${base} ${theme} ${background} ${selected} ${overlay}`,
|
||||||
taskName: `text-sm font-medium flex-1 overflow-hidden text-ellipsis whitespace-nowrap transition-colors duration-200 cursor-pointer ${
|
taskName: `text-sm font-medium flex-1 overflow-hidden text-ellipsis whitespace-nowrap transition-colors duration-150 cursor-pointer ${
|
||||||
isDarkMode ? 'text-gray-100 hover:text-blue-400' : 'text-gray-900 hover:text-blue-600'
|
isDarkMode ? 'text-gray-100 hover:text-blue-400' : 'text-gray-900 hover:text-blue-600'
|
||||||
} ${task.progress === 100 ? `line-through ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}` : ''}`,
|
} ${task.progress === 100 ? `line-through ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}` : ''}`,
|
||||||
};
|
};
|
||||||
@@ -423,16 +430,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
assignee: createAssigneeAdapter(task),
|
assignee: createAssigneeAdapter(task),
|
||||||
}), [task]);
|
}), [task]);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Simplified column rendering for initial load
|
// PERFORMANCE OPTIMIZATION: Minimal column rendering for initial load
|
||||||
const renderColumnSimple = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
const renderMinimalColumn = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
||||||
// Fix border logic: if this is a fixed column, only consider it "last" if there are no scrollable columns
|
|
||||||
// If this is a scrollable column, use the normal logic
|
|
||||||
const isActuallyLast = isFixed
|
const isActuallyLast = isFixed
|
||||||
? (index === totalColumns - 1 && (!scrollableColumns || scrollableColumns.length === 0))
|
? (index === totalColumns - 1 && (!scrollableColumns || scrollableColumns.length === 0))
|
||||||
: (index === totalColumns - 1);
|
: (index === totalColumns - 1);
|
||||||
const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||||
|
|
||||||
// Only render essential columns during initial load
|
// Only render essential columns during minimal load
|
||||||
switch (col.key) {
|
switch (col.key) {
|
||||||
case 'drag':
|
case 'drag':
|
||||||
return (
|
return (
|
||||||
@@ -464,6 +469,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
<div className="flex-1 min-w-0 flex flex-col justify-center h-full overflow-hidden">
|
||||||
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
<div className="flex items-center gap-2 h-5 overflow-hidden">
|
||||||
|
{/* Always reserve space for expand icon */}
|
||||||
|
<div style={{ width: 20, display: 'inline-block' }} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
ellipsis={{ tooltip: task.title }}
|
ellipsis={{ tooltip: task.title }}
|
||||||
@@ -477,139 +484,21 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'status':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
|
||||||
{task.status === 'todo' ? 'To Do' :
|
|
||||||
task.status === 'doing' ? 'Doing' :
|
|
||||||
task.status === 'done' ? 'Done' :
|
|
||||||
task.status || 'To Do'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'progress':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center justify-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-600'}`}>
|
|
||||||
{task.progress || 0}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'priority':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
|
||||||
{task.priority === 'critical' ? 'Critical' :
|
|
||||||
task.priority === 'high' ? 'High' :
|
|
||||||
task.priority === 'medium' ? 'Medium' :
|
|
||||||
task.priority === 'low' ? 'Low' :
|
|
||||||
task.priority || 'Medium'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'phase':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
|
||||||
{task.phase || 'No Phase'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'members':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{task.assignee_names && task.assignee_names.length > 0 ? (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{task.assignee_names.slice(0, 3).map((member, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
|
|
||||||
isDarkMode ? 'bg-gray-600 text-gray-200' : 'bg-gray-200 text-gray-700'
|
|
||||||
}`}
|
|
||||||
title={member.name}
|
|
||||||
>
|
|
||||||
{member.name ? member.name.charAt(0).toUpperCase() : '?'}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{task.assignee_names.length > 3 && (
|
|
||||||
<div
|
|
||||||
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
|
|
||||||
isDarkMode ? 'bg-gray-600 text-gray-200' : 'bg-gray-200 text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
+{task.assignee_names.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={`w-6 h-6 rounded-full border-2 border-dashed flex items-center justify-center ${
|
|
||||||
isDarkMode ? 'border-gray-600' : 'border-gray-300'
|
|
||||||
}`}>
|
|
||||||
<UserOutlined className={`text-xs ${isDarkMode ? 'text-gray-600' : 'text-gray-400'}`} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'labels':
|
|
||||||
return (
|
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
|
||||||
{task.labels && task.labels.length > 0 ? (
|
|
||||||
task.labels.slice(0, 3).map((label, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`px-2 py-1 text-xs rounded ${
|
|
||||||
isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color || (isDarkMode ? '#374151' : '#f3f4f6'),
|
|
||||||
color: label.color ? '#ffffff' : undefined
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label.name || 'Label'}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className={`px-2 py-1 text-xs rounded border-dashed border ${
|
|
||||||
isDarkMode ? 'border-gray-600 text-gray-600' : 'border-gray-300 text-gray-400'
|
|
||||||
}`}>
|
|
||||||
No labels
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{task.labels && task.labels.length > 3 && (
|
|
||||||
<div className={`px-2 py-1 text-xs rounded ${
|
|
||||||
isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'
|
|
||||||
}`}>
|
|
||||||
+{task.labels.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// For non-essential columns, show placeholder during initial load
|
// For non-essential columns, show minimal placeholder
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
<div className={`w-8 h-3 rounded ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'} animate-pulse`}></div>
|
<div className={`w-6 h-3 rounded ${isDarkMode ? 'bg-gray-700' : 'bg-gray-200'}`}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [isDarkMode, task, isSelected, handleSelectChange, styleClasses]);
|
}, [isDarkMode, task, isSelected, handleSelectChange, styleClasses, scrollableColumns]);
|
||||||
|
|
||||||
// Optimized column rendering with better performance
|
// Optimized column rendering with better performance
|
||||||
const renderColumn = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
const renderColumn = useCallback((col: { key: string; width: number }, isFixed: boolean, index: number, totalColumns: number) => {
|
||||||
// Use simplified rendering for initial load
|
// Use simplified rendering for initial load
|
||||||
if (!shouldRenderFull) {
|
if (!shouldRenderFull) {
|
||||||
return renderColumnSimple(col, isFixed, index, totalColumns);
|
return renderMinimalColumn(col, isFixed, index, totalColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full rendering logic (existing code)
|
// Full rendering logic (existing code)
|
||||||
@@ -996,11 +885,21 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
shouldRenderFull, renderColumnSimple, isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId,
|
shouldRenderFull, renderMinimalColumn, isDarkMode, task, isSelected, editTaskName, taskName, adapters, groupId, projectId,
|
||||||
attributes, listeners, handleSelectChange, handleTaskNameSave, handleDateChange,
|
attributes, listeners, handleSelectChange, handleTaskNameSave, handleDateChange,
|
||||||
dateValues, styleClasses
|
dateValues, styleClasses
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Apply global cursor style when dragging
|
||||||
|
useDragCursor(isDragging);
|
||||||
|
|
||||||
|
// Compute theme class
|
||||||
|
const themeClass = isDarkMode ? 'dark' : '';
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
console.log('TaskRow isDragging:', task.id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
@@ -1008,8 +907,11 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
rowRef.current = node;
|
rowRef.current = node;
|
||||||
}}
|
}}
|
||||||
style={dragStyle}
|
style={dragStyle}
|
||||||
className={`${styleClasses.container} task-row-optimized ${shouldRenderFull ? 'fully-loaded' : 'initial-load'} ${hasBeenFullyLoadedOnce.current ? 'stable-content' : ''}`}
|
className={`task-row task-row-optimized ${themeClass} ${isSelected ? 'selected' : ''} ${isDragOverlay ? 'drag-overlay' : ''} ${isDragging ? 'is-dragging' : ''}`}
|
||||||
|
data-dnd-draggable="true"
|
||||||
|
data-dnd-dragging={isDragging ? 'true' : 'false'}
|
||||||
data-task-id={task.id}
|
data-task-id={task.id}
|
||||||
|
data-group-id={groupId}
|
||||||
>
|
>
|
||||||
<div className="flex h-10 max-h-10 overflow-visible relative">
|
<div className="flex h-10 max-h-10 overflow-visible relative">
|
||||||
{/* Fixed Columns */}
|
{/* Fixed Columns */}
|
||||||
@@ -1020,7 +922,11 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
width: fixedColumns.reduce((sum, col) => sum + col.width, 0),
|
width: fixedColumns.reduce((sum, col) => sum + col.width, 0),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fixedColumns.map((col, index) => renderColumn(col, true, index, fixedColumns.length))}
|
{fixedColumns.map((col, index) =>
|
||||||
|
shouldRenderMinimal
|
||||||
|
? renderMinimalColumn(col, true, index, fixedColumns.length)
|
||||||
|
: renderColumn(col, true, index, fixedColumns.length)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1033,7 +939,11 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
minWidth: scrollableColumns.reduce((sum, col) => sum + col.width, 0)
|
minWidth: scrollableColumns.reduce((sum, col) => sum + col.width, 0)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{scrollableColumns.map((col, index) => renderColumn(col, false, index, scrollableColumns.length))}
|
{scrollableColumns.map((col, index) =>
|
||||||
|
shouldRenderMinimal
|
||||||
|
? renderMinimalColumn(col, false, index, scrollableColumns.length)
|
||||||
|
: renderColumn(col, false, index, scrollableColumns.length)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1054,7 +964,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
// Fix border logic for add subtask row: fixed columns should have right border if scrollable columns exist
|
// Fix border logic for add subtask row: fixed columns should have right border if scrollable columns exist
|
||||||
const isActuallyLast = index === fixedColumns.length - 1 && (!scrollableColumns || scrollableColumns.length === 0);
|
const isActuallyLast = index === fixedColumns.length - 1 && (!scrollableColumns || scrollableColumns.length === 0);
|
||||||
const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
const borderClasses = `${isActuallyLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||||
|
|
||||||
if (col.key === 'task') {
|
if (col.key === 'task') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1123,7 +1033,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
{scrollableColumns.map((col, index) => {
|
{scrollableColumns.map((col, index) => {
|
||||||
const isLast = index === scrollableColumns.length - 1;
|
const isLast = index === scrollableColumns.length - 1;
|
||||||
const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
const borderClasses = `${isLast ? '' : 'border-r'} border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.key}
|
key={col.key}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface VirtualizedTaskListProps {
|
|||||||
onToggleSubtasks: (taskId: string) => void;
|
onToggleSubtasks: (taskId: string) => void;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
tasksById: Record<string, Task>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
||||||
@@ -31,9 +32,9 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
onSelectTask,
|
onSelectTask,
|
||||||
onToggleSubtasks,
|
onToggleSubtasks,
|
||||||
height,
|
height,
|
||||||
width
|
width,
|
||||||
|
tasksById
|
||||||
}) => {
|
}) => {
|
||||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
|
||||||
const { t } = useTranslation('task-management');
|
const { t } = useTranslation('task-management');
|
||||||
|
|
||||||
// Get theme from Redux store
|
// Get theme from Redux store
|
||||||
@@ -42,8 +43,8 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
// Get field visibility from taskListFields slice
|
// Get field visibility from taskListFields slice
|
||||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Reduce virtualization threshold for better performance
|
// PERFORMANCE OPTIMIZATION: Aggressive virtualization for large lists
|
||||||
const VIRTUALIZATION_THRESHOLD = 20; // Reduced from 100 to 20 - virtualize even smaller lists
|
const VIRTUALIZATION_THRESHOLD = 5; // Reduced from 10 to 5 - virtualize everything
|
||||||
const TASK_ROW_HEIGHT = 40;
|
const TASK_ROW_HEIGHT = 40;
|
||||||
const HEADER_HEIGHT = 40;
|
const HEADER_HEIGHT = 40;
|
||||||
const COLUMN_HEADER_HEIGHT = 40;
|
const COLUMN_HEADER_HEIGHT = 40;
|
||||||
@@ -121,14 +122,18 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tasks for this group using memoization for performance
|
// PERFORMANCE OPTIMIZATION: Get tasks for this group using direct lookup (no mapping/filtering)
|
||||||
const groupTasks = useMemo(() => {
|
const groupTasks = useMemo(() => {
|
||||||
const tasks = group.taskIds
|
// PERFORMANCE OPTIMIZATION: Use for loop instead of map for better performance
|
||||||
.map((taskId: string) => allTasks.find((task: Task) => task.id === taskId))
|
const tasks: Task[] = [];
|
||||||
.filter((task: Task | undefined): task is Task => task !== undefined);
|
for (let i = 0; i < group.taskIds.length; i++) {
|
||||||
|
const task = tasksById[group.taskIds[i]];
|
||||||
|
if (task) {
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
return tasks;
|
return tasks;
|
||||||
}, [group.taskIds, allTasks]);
|
}, [group.taskIds, tasksById]);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Only calculate selection state when needed
|
// PERFORMANCE OPTIMIZATION: Only calculate selection state when needed
|
||||||
const selectionState = useMemo(() => {
|
const selectionState = useMemo(() => {
|
||||||
@@ -136,36 +141,56 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
return { isAllSelected: false, isIndeterminate: false };
|
return { isAllSelected: false, isIndeterminate: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTasksInGroup = groupTasks.filter((task: Task) => selectedTaskIds.includes(task.id));
|
// PERFORMANCE OPTIMIZATION: Use for loop instead of filter for better performance
|
||||||
const isAllSelected = selectedTasksInGroup.length === groupTasks.length;
|
let selectedCount = 0;
|
||||||
const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length;
|
for (let i = 0; i < groupTasks.length; i++) {
|
||||||
|
if (selectedTaskIds.includes(groupTasks[i].id)) {
|
||||||
|
selectedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllSelected = selectedCount === groupTasks.length;
|
||||||
|
const isIndeterminate = selectedCount > 0 && selectedCount < groupTasks.length;
|
||||||
|
|
||||||
return { isAllSelected, isIndeterminate };
|
return { isAllSelected, isIndeterminate };
|
||||||
}, [groupTasks, selectedTaskIds]);
|
}, [groupTasks, selectedTaskIds]);
|
||||||
|
|
||||||
// Handle select all tasks in group - optimized with useCallback
|
// Handle select all tasks in group - optimized with useCallback
|
||||||
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
const handleSelectAllInGroup = useCallback((checked: boolean) => {
|
||||||
|
// PERFORMANCE OPTIMIZATION: Batch selection updates
|
||||||
|
const tasksToUpdate: Array<{ taskId: string; selected: boolean }> = [];
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// Select all tasks in the group
|
// Select all tasks in the group
|
||||||
groupTasks.forEach((task: Task) => {
|
for (let i = 0; i < groupTasks.length; i++) {
|
||||||
|
const task = groupTasks[i];
|
||||||
if (!selectedTaskIds.includes(task.id)) {
|
if (!selectedTaskIds.includes(task.id)) {
|
||||||
onSelectTask(task.id, true);
|
tasksToUpdate.push({ taskId: task.id, selected: true });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
// Deselect all tasks in the group
|
// Deselect all tasks in the group
|
||||||
groupTasks.forEach((task: Task) => {
|
for (let i = 0; i < groupTasks.length; i++) {
|
||||||
|
const task = groupTasks[i];
|
||||||
if (selectedTaskIds.includes(task.id)) {
|
if (selectedTaskIds.includes(task.id)) {
|
||||||
onSelectTask(task.id, false);
|
tasksToUpdate.push({ taskId: task.id, selected: false });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch update all selections
|
||||||
|
tasksToUpdate.forEach(({ taskId, selected }) => {
|
||||||
|
onSelectTask(taskId, selected);
|
||||||
|
});
|
||||||
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
}, [groupTasks, selectedTaskIds, onSelectTask]);
|
||||||
|
|
||||||
// Calculate dynamic height for the group
|
// PERFORMANCE OPTIMIZATION: Simplified height calculation
|
||||||
const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT;
|
const taskRowsHeight = groupTasks.length * TASK_ROW_HEIGHT;
|
||||||
const groupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + taskRowsHeight + ADD_TASK_ROW_HEIGHT;
|
const groupHeight = HEADER_HEIGHT + COLUMN_HEADER_HEIGHT + taskRowsHeight + ADD_TASK_ROW_HEIGHT;
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Limit visible columns for large lists
|
||||||
|
const maxVisibleColumns = groupTasks.length > 50 ? 6 : 12; // Further reduce columns for large lists
|
||||||
|
|
||||||
// Define all possible columns
|
// Define all possible columns
|
||||||
const allFixedColumns = [
|
const allFixedColumns = [
|
||||||
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
{ key: 'drag', label: '', width: 40, alwaysVisible: true },
|
||||||
@@ -210,7 +235,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
}, [taskListFields, allFixedColumns]);
|
}, [taskListFields, allFixedColumns]);
|
||||||
|
|
||||||
const scrollableColumns = useMemo(() => {
|
const scrollableColumns = useMemo(() => {
|
||||||
return allScrollableColumns.filter(col => {
|
const filtered = allScrollableColumns.filter(col => {
|
||||||
// For scrollable columns, check field visibility
|
// For scrollable columns, check field visibility
|
||||||
if (col.fieldKey) {
|
if (col.fieldKey) {
|
||||||
const field = taskListFields.find(f => f.key === col.fieldKey);
|
const field = taskListFields.find(f => f.key === col.fieldKey);
|
||||||
@@ -219,21 +244,31 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}, [taskListFields, allScrollableColumns]);
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Limit columns for large lists
|
||||||
|
return filtered.slice(0, maxVisibleColumns);
|
||||||
|
}, [taskListFields, allScrollableColumns, maxVisibleColumns]);
|
||||||
|
|
||||||
const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0);
|
const fixedWidth = fixedColumns.reduce((sum, col) => sum + col.width, 0);
|
||||||
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
const scrollableWidth = scrollableColumns.reduce((sum, col) => sum + col.width, 0);
|
||||||
const totalTableWidth = fixedWidth + scrollableWidth;
|
const totalTableWidth = fixedWidth + scrollableWidth;
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Increase overscanCount for better perceived performance
|
// PERFORMANCE OPTIMIZATION: Optimize overscan for large lists
|
||||||
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
const shouldVirtualize = groupTasks.length > VIRTUALIZATION_THRESHOLD;
|
||||||
const overscanCount = shouldVirtualize ? Math.min(10, Math.ceil(groupTasks.length * 0.1)) : 0; // Dynamic overscan
|
const overscanCount = useMemo(() => {
|
||||||
|
if (groupTasks.length <= 10) return 2;
|
||||||
|
if (groupTasks.length <= 50) return 3;
|
||||||
|
return 5; // Reduced from 10 for better performance
|
||||||
|
}, [groupTasks.length]);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Memoize row renderer with better dependency management
|
// PERFORMANCE OPTIMIZATION: Memoize row renderer with better dependency management
|
||||||
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => {
|
||||||
const task: Task | undefined = groupTasks[index];
|
const task: Task | undefined = groupTasks[index];
|
||||||
if (!task) return null;
|
if (!task) return null;
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Pre-calculate selection state
|
||||||
|
const isSelected = selectedTaskIds.includes(task.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="task-row-container"
|
className="task-row-container"
|
||||||
@@ -248,7 +283,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
groupId={group.id}
|
groupId={group.id}
|
||||||
currentGrouping={currentGrouping}
|
currentGrouping={currentGrouping}
|
||||||
isSelected={selectedTaskIds.includes(task.id)}
|
isSelected={isSelected}
|
||||||
index={index}
|
index={index}
|
||||||
onSelect={onSelectTask}
|
onSelect={onSelectTask}
|
||||||
onToggleSubtasks={onToggleSubtasks}
|
onToggleSubtasks={onToggleSubtasks}
|
||||||
@@ -262,23 +297,25 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const headerScrollRef = useRef<HTMLDivElement>(null);
|
const headerScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// PERFORMANCE OPTIMIZATION: Throttled scroll handler
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (headerScrollRef.current && scrollContainerRef.current) {
|
||||||
|
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Synchronize header scroll with body scroll
|
// Synchronize header scroll with body scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
|
||||||
if (headerScrollRef.current && scrollContainerRef.current) {
|
|
||||||
headerScrollRef.current.scrollLeft = scrollContainerRef.current.scrollLeft;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const scrollDiv = scrollContainerRef.current;
|
const scrollDiv = scrollContainerRef.current;
|
||||||
if (scrollDiv) {
|
if (scrollDiv) {
|
||||||
scrollDiv.addEventListener('scroll', handleScroll);
|
scrollDiv.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (scrollDiv) {
|
if (scrollDiv) {
|
||||||
scrollDiv.removeEventListener('scroll', handleScroll);
|
scrollDiv.removeEventListener('scroll', handleScroll);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [handleScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="virtualized-task-list" style={{ height: groupHeight }}>
|
<div className="virtualized-task-list" style={{ height: groupHeight }}>
|
||||||
@@ -363,52 +400,50 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
width={width}
|
width={width}
|
||||||
itemCount={groupTasks.length}
|
itemCount={groupTasks.length}
|
||||||
itemSize={TASK_ROW_HEIGHT}
|
itemSize={TASK_ROW_HEIGHT}
|
||||||
overscanCount={overscanCount} // Dynamic overscan
|
overscanCount={overscanCount}
|
||||||
className="react-window-list"
|
className="react-window-list"
|
||||||
style={{ minWidth: totalTableWidth }}
|
style={{ minWidth: totalTableWidth }}
|
||||||
// PERFORMANCE OPTIMIZATION: Add performance-focused props
|
// PERFORMANCE OPTIMIZATION: Remove all expensive props for maximum performance
|
||||||
useIsScrolling={true}
|
useIsScrolling={false}
|
||||||
itemData={{
|
itemData={undefined}
|
||||||
groupTasks,
|
// Disable all animations and transitions
|
||||||
group,
|
onItemsRendered={() => {}}
|
||||||
projectId,
|
onScroll={() => {}}
|
||||||
currentGrouping,
|
|
||||||
selectedTaskIds,
|
|
||||||
onSelectTask,
|
|
||||||
onToggleSubtasks,
|
|
||||||
fixedColumns,
|
|
||||||
scrollableColumns
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{Row}
|
{Row}
|
||||||
</List>
|
</List>
|
||||||
) : (
|
) : (
|
||||||
// PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes
|
// PERFORMANCE OPTIMIZATION: Use React.Fragment to reduce DOM nodes
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{groupTasks.map((task: Task, index: number) => (
|
{groupTasks.map((task: Task, index: number) => {
|
||||||
<div
|
// PERFORMANCE OPTIMIZATION: Pre-calculate selection state
|
||||||
key={task.id}
|
const isSelected = selectedTaskIds.includes(task.id);
|
||||||
className="task-row-container"
|
|
||||||
style={{
|
return (
|
||||||
height: TASK_ROW_HEIGHT,
|
<div
|
||||||
'--group-color': group.color || '#f0f0f0',
|
key={task.id}
|
||||||
contain: 'layout style', // CSS containment
|
className="task-row-container"
|
||||||
} as React.CSSProperties}
|
style={{
|
||||||
>
|
height: TASK_ROW_HEIGHT,
|
||||||
<TaskRow
|
'--group-color': group.color || '#f0f0f0',
|
||||||
task={task}
|
contain: 'layout style', // CSS containment
|
||||||
projectId={projectId}
|
} as React.CSSProperties}
|
||||||
groupId={group.id}
|
>
|
||||||
currentGrouping={currentGrouping}
|
<TaskRow
|
||||||
isSelected={selectedTaskIds.includes(task.id)}
|
task={task}
|
||||||
index={index}
|
projectId={projectId}
|
||||||
onSelect={onSelectTask}
|
groupId={group.id}
|
||||||
onToggleSubtasks={onToggleSubtasks}
|
currentGrouping={currentGrouping}
|
||||||
fixedColumns={fixedColumns}
|
isSelected={isSelected}
|
||||||
scrollableColumns={scrollableColumns}
|
index={index}
|
||||||
/>
|
onSelect={onSelectTask}
|
||||||
</div>
|
onToggleSubtasks={onToggleSubtasks}
|
||||||
))}
|
fixedColumns={fixedColumns}
|
||||||
|
scrollableColumns={scrollableColumns}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
284
worklenz-frontend/src/utils/debug-performance.ts
Normal file
284
worklenz-frontend/src/utils/debug-performance.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// Debug utility for identifying performance bottlenecks with 400 tasks
|
||||||
|
|
||||||
|
export const debugPerformance = {
|
||||||
|
// Log component render times
|
||||||
|
logComponentRender: (componentName: string, startTime: number) => {
|
||||||
|
const renderTime = performance.now() - startTime;
|
||||||
|
if (renderTime > 16) { // Log slow renders (>16ms)
|
||||||
|
console.warn(`Slow render detected: ${componentName} took ${renderTime.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log Redux selector performance
|
||||||
|
logSelectorPerformance: (selectorName: string, startTime: number) => {
|
||||||
|
const executionTime = performance.now() - startTime;
|
||||||
|
if (executionTime > 5) { // Log slow selectors (>5ms)
|
||||||
|
console.warn(`Slow selector detected: ${selectorName} took ${executionTime.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log memory usage
|
||||||
|
logMemoryUsage: () => {
|
||||||
|
if ('memory' in performance) {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
const usedMB = memory.usedJSHeapSize / 1024 / 1024;
|
||||||
|
const totalMB = memory.totalJSHeapSize / 1024 / 1024;
|
||||||
|
const limitMB = memory.jsHeapSizeLimit / 1024 / 1024;
|
||||||
|
|
||||||
|
console.log(`Memory Usage: ${usedMB.toFixed(1)}MB / ${totalMB.toFixed(1)}MB (${limitMB.toFixed(1)}MB limit)`);
|
||||||
|
|
||||||
|
if (usedMB > 100) {
|
||||||
|
console.warn(`High memory usage detected: ${usedMB.toFixed(1)}MB`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log DOM node count
|
||||||
|
logDOMNodes: () => {
|
||||||
|
const nodeCount = document.querySelectorAll('*').length;
|
||||||
|
console.log(`Total DOM nodes: ${nodeCount}`);
|
||||||
|
|
||||||
|
if (nodeCount > 1000) {
|
||||||
|
console.warn(`High DOM node count detected: ${nodeCount} nodes`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log React component count
|
||||||
|
logReactComponents: () => {
|
||||||
|
// This is a rough estimate - React DevTools would be more accurate
|
||||||
|
const reactComponents = document.querySelectorAll('[data-reactroot], [data-reactid]').length;
|
||||||
|
console.log(`React components (estimate): ${reactComponents}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log scroll performance
|
||||||
|
logScrollPerformance: () => {
|
||||||
|
let lastScrollTime = 0;
|
||||||
|
let scrollCount = 0;
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const timeSinceLastScroll = currentTime - lastScrollTime;
|
||||||
|
|
||||||
|
if (timeSinceLastScroll < 16) { // Less than 60fps
|
||||||
|
scrollCount++;
|
||||||
|
if (scrollCount > 5) {
|
||||||
|
console.warn(`Poor scroll performance detected: ${timeSinceLastScroll.toFixed(2)}ms between scrolls`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrollCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollTime = currentTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log long tasks
|
||||||
|
logLongTasks: () => {
|
||||||
|
if ('PerformanceObserver' in window) {
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
if (entry.entryType === 'longtask') {
|
||||||
|
console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe({ entryTypes: ['longtask'] });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// NEW: Monitor hover performance specifically
|
||||||
|
logHoverPerformance: () => {
|
||||||
|
let hoverStartTime = 0;
|
||||||
|
let hoverCount = 0;
|
||||||
|
let totalHoverTime = 0;
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
hoverStartTime = performance.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (hoverStartTime > 0) {
|
||||||
|
const hoverDuration = performance.now() - hoverStartTime;
|
||||||
|
totalHoverTime += hoverDuration;
|
||||||
|
hoverCount++;
|
||||||
|
|
||||||
|
if (hoverDuration > 50) { // Log slow hover operations (>50ms)
|
||||||
|
console.warn(`Slow hover operation detected: ${hoverDuration.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log average hover time every 10 hovers
|
||||||
|
if (hoverCount % 10 === 0) {
|
||||||
|
const avgHoverTime = totalHoverTime / hoverCount;
|
||||||
|
console.log(`Average hover time: ${avgHoverTime.toFixed(2)}ms (${hoverCount} hovers)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverStartTime = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Monitor hover events on task rows specifically
|
||||||
|
const taskRows = document.querySelectorAll('.task-row-optimized, .task-row');
|
||||||
|
taskRows.forEach(row => {
|
||||||
|
row.addEventListener('mouseenter', handleMouseEnter, { passive: true });
|
||||||
|
row.addEventListener('mouseleave', handleMouseLeave, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
taskRows.forEach(row => {
|
||||||
|
row.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
|
row.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// NEW: Monitor CSS transitions and animations
|
||||||
|
logCSSPerformance: () => {
|
||||||
|
if ('PerformanceObserver' in window) {
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
if (entry.entryType === 'measure') {
|
||||||
|
const duration = entry.duration;
|
||||||
|
if (duration > 16) { // Log slow CSS operations (>16ms)
|
||||||
|
console.warn(`Slow CSS operation detected: ${duration.toFixed(2)}ms - ${entry.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe({ entryTypes: ['measure'] });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Comprehensive performance check
|
||||||
|
runPerformanceCheck: () => {
|
||||||
|
console.group('🔍 Performance Check');
|
||||||
|
|
||||||
|
// Memory usage
|
||||||
|
debugPerformance.logMemoryUsage();
|
||||||
|
|
||||||
|
// DOM nodes
|
||||||
|
debugPerformance.logDOMNodes();
|
||||||
|
|
||||||
|
// React components
|
||||||
|
debugPerformance.logReactComponents();
|
||||||
|
|
||||||
|
// Start monitoring
|
||||||
|
const stopScroll = debugPerformance.logScrollPerformance();
|
||||||
|
const stopLongTasks = debugPerformance.logLongTasks();
|
||||||
|
const stopHover = debugPerformance.logHoverPerformance();
|
||||||
|
const stopCSS = debugPerformance.logCSSPerformance();
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopScroll();
|
||||||
|
stopLongTasks();
|
||||||
|
stopHover();
|
||||||
|
stopCSS();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monitor specific component
|
||||||
|
monitorComponent: (componentName: string) => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debugPerformance.logComponentRender(componentName, startTime);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monitor Redux selector
|
||||||
|
monitorSelector: (selectorName: string, selectorFn: () => any) => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = selectorFn();
|
||||||
|
debugPerformance.logSelectorPerformance(selectorName, startTime);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
// NEW: Quick hover performance test
|
||||||
|
testHoverPerformance: () => {
|
||||||
|
console.group('🧪 Hover Performance Test');
|
||||||
|
|
||||||
|
const taskRows = document.querySelectorAll('.task-row-optimized, .task-row');
|
||||||
|
console.log(`Found ${taskRows.length} task rows to test`);
|
||||||
|
|
||||||
|
let totalHoverTime = 0;
|
||||||
|
let hoverCount = 0;
|
||||||
|
|
||||||
|
const testHover = (row: Element) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// Simulate hover
|
||||||
|
row.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
row.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
||||||
|
const hoverTime = performance.now() - startTime;
|
||||||
|
totalHoverTime += hoverTime;
|
||||||
|
hoverCount++;
|
||||||
|
|
||||||
|
if (hoverTime > 50) {
|
||||||
|
console.warn(`Slow hover on row ${hoverCount}: ${hoverTime.toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test first 5 rows
|
||||||
|
const testRows = Array.from(taskRows).slice(0, 5);
|
||||||
|
Promise.all(testRows.map(testHover)).then(() => {
|
||||||
|
const avgHoverTime = totalHoverTime / hoverCount;
|
||||||
|
console.log(`Average hover time: ${avgHoverTime.toFixed(2)}ms (${hoverCount} tests)`);
|
||||||
|
|
||||||
|
if (avgHoverTime > 30) {
|
||||||
|
console.error(`🚨 Poor hover performance detected: ${avgHoverTime.toFixed(2)}ms average`);
|
||||||
|
} else if (avgHoverTime > 16) {
|
||||||
|
console.warn(`⚠️ Suboptimal hover performance: ${avgHoverTime.toFixed(2)}ms average`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Good hover performance: ${avgHoverTime.toFixed(2)}ms average`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-run performance check in development
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// Run initial check after page load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
debugPerformance.runPerformanceCheck();
|
||||||
|
|
||||||
|
// Run hover performance test after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
debugPerformance.testHoverPerformance();
|
||||||
|
}, 3000);
|
||||||
|
}, 2000); // Wait for initial render
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for manual use
|
||||||
|
export default debugPerformance;
|
||||||
304
worklenz-frontend/src/utils/performance-monitor.ts
Normal file
304
worklenz-frontend/src/utils/performance-monitor.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// Performance monitoring utility for task list performance analysis
|
||||||
|
export class PerformanceMonitor {
|
||||||
|
private static instance: PerformanceMonitor;
|
||||||
|
private metrics: Map<string, number[]> = new Map();
|
||||||
|
private marks: Map<string, number> = new Map();
|
||||||
|
private observers: Map<string, PerformanceObserver> = new Map();
|
||||||
|
|
||||||
|
static getInstance(): PerformanceMonitor {
|
||||||
|
if (!PerformanceMonitor.instance) {
|
||||||
|
PerformanceMonitor.instance = new PerformanceMonitor();
|
||||||
|
}
|
||||||
|
return PerformanceMonitor.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a performance point
|
||||||
|
mark(name: string): void {
|
||||||
|
this.marks.set(name, performance.now());
|
||||||
|
performance.mark(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure time between two marks
|
||||||
|
measure(name: string, startMark: string, endMark: string): void {
|
||||||
|
try {
|
||||||
|
performance.measure(name, startMark, endMark);
|
||||||
|
const measure = performance.getEntriesByName(name, 'measure')[0];
|
||||||
|
if (measure) {
|
||||||
|
this.addMetric(name, measure.duration);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to measure ${name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a metric value
|
||||||
|
addMetric(name: string, value: number): void {
|
||||||
|
if (!this.metrics.has(name)) {
|
||||||
|
this.metrics.set(name, []);
|
||||||
|
}
|
||||||
|
this.metrics.get(name)!.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get average for a metric
|
||||||
|
getAverage(name: string): number {
|
||||||
|
const values = this.metrics.get(name);
|
||||||
|
if (!values || values.length === 0) return 0;
|
||||||
|
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all metrics
|
||||||
|
getMetrics(): Record<string, { average: number; count: number; min: number; max: number }> {
|
||||||
|
const result: Record<string, { average: number; count: number; min: number; max: number }> = {};
|
||||||
|
|
||||||
|
this.metrics.forEach((values, name) => {
|
||||||
|
if (values.length > 0) {
|
||||||
|
result[name] = {
|
||||||
|
average: this.getAverage(name),
|
||||||
|
count: values.length,
|
||||||
|
min: Math.min(...values),
|
||||||
|
max: Math.max(...values),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor React component render times
|
||||||
|
monitorComponentRender(componentName: string): () => void {
|
||||||
|
const startMark = `${componentName}-render-start`;
|
||||||
|
const endMark = `${componentName}-render-end`;
|
||||||
|
|
||||||
|
this.mark(startMark);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.mark(endMark);
|
||||||
|
this.measure(`${componentName}-render-time`, startMark, endMark);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Redux selector performance
|
||||||
|
monitorSelector(selectorName: string, selectorFn: () => any): any {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = selectorFn();
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
this.addMetric(`${selectorName}-execution-time`, endTime - startTime);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor DOM operations
|
||||||
|
monitorDOMOperation(operationName: string, operation: () => void): void {
|
||||||
|
const startTime = performance.now();
|
||||||
|
operation();
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
this.addMetric(`${operationName}-dom-time`, endTime - startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor memory usage
|
||||||
|
monitorMemory(): void {
|
||||||
|
if ('memory' in performance) {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
this.addMetric('memory-used', memory.usedJSHeapSize);
|
||||||
|
this.addMetric('memory-total', memory.totalJSHeapSize);
|
||||||
|
this.addMetric('memory-limit', memory.jsHeapSizeLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor frame rate
|
||||||
|
startFrameRateMonitoring(): () => void {
|
||||||
|
let frameCount = 0;
|
||||||
|
let lastTime = performance.now();
|
||||||
|
|
||||||
|
const measureFrameRate = () => {
|
||||||
|
frameCount++;
|
||||||
|
const currentTime = performance.now();
|
||||||
|
|
||||||
|
if (currentTime - lastTime >= 1000) { // Every second
|
||||||
|
const fps = frameCount / ((currentTime - lastTime) / 1000);
|
||||||
|
this.addMetric('fps', fps);
|
||||||
|
frameCount = 0;
|
||||||
|
lastTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(measureFrameRate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const animationId = requestAnimationFrame(measureFrameRate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor long tasks
|
||||||
|
startLongTaskMonitoring(): () => void {
|
||||||
|
if ('PerformanceObserver' in window) {
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
if (entry.entryType === 'longtask') {
|
||||||
|
this.addMetric('long-task-duration', entry.duration);
|
||||||
|
console.warn('Long task detected:', entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe({ entryTypes: ['longtask'] });
|
||||||
|
this.observers.set('longtask', observer);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
this.observers.delete('longtask');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor layout thrashing
|
||||||
|
startLayoutThrashingMonitoring(): () => void {
|
||||||
|
let layoutCount = 0;
|
||||||
|
let lastLayoutTime = 0;
|
||||||
|
const monitor = this;
|
||||||
|
|
||||||
|
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
||||||
|
|
||||||
|
Element.prototype.getBoundingClientRect = function() {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
if (currentTime - lastLayoutTime < 16) { // Less than 16ms between calls
|
||||||
|
layoutCount++;
|
||||||
|
monitor.addMetric('layout-thrashing-count', layoutCount);
|
||||||
|
}
|
||||||
|
lastLayoutTime = currentTime;
|
||||||
|
return originalGetBoundingClientRect.call(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate performance report
|
||||||
|
generateReport(): string {
|
||||||
|
const metrics = this.getMetrics();
|
||||||
|
const report = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
metrics,
|
||||||
|
summary: this.generateSummary(metrics),
|
||||||
|
recommendations: this.generateRecommendations(metrics),
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(report, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSummary(metrics: Record<string, any>): Record<string, string> {
|
||||||
|
const summary: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Component render times
|
||||||
|
const renderTimes = Object.keys(metrics).filter(key => key.includes('render-time'));
|
||||||
|
if (renderTimes.length > 0) {
|
||||||
|
const avgRenderTime = renderTimes.reduce((sum, key) => sum + metrics[key].average, 0) / renderTimes.length;
|
||||||
|
summary.renderPerformance = avgRenderTime > 16 ? 'Poor' : avgRenderTime > 8 ? 'Fair' : 'Good';
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPS
|
||||||
|
if (metrics.fps) {
|
||||||
|
summary.frameRate = metrics.fps.average > 55 ? 'Good' : metrics.fps.average > 30 ? 'Fair' : 'Poor';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory usage
|
||||||
|
if (metrics['memory-used'] && metrics['memory-limit']) {
|
||||||
|
const memoryUsage = (metrics['memory-used'].average / metrics['memory-limit'].average) * 100;
|
||||||
|
summary.memoryUsage = memoryUsage > 80 ? 'High' : memoryUsage > 50 ? 'Moderate' : 'Low';
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateRecommendations(metrics: Record<string, any>): string[] {
|
||||||
|
const recommendations: string[] = [];
|
||||||
|
|
||||||
|
// Check for slow component renders
|
||||||
|
const slowRenders = Object.keys(metrics).filter(key =>
|
||||||
|
key.includes('render-time') && metrics[key].average > 16
|
||||||
|
);
|
||||||
|
if (slowRenders.length > 0) {
|
||||||
|
recommendations.push(`Optimize component renders: ${slowRenders.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for layout thrashing
|
||||||
|
if (metrics['layout-thrashing-count'] && metrics['layout-thrashing-count'].count > 10) {
|
||||||
|
recommendations.push('Reduce layout thrashing by batching DOM reads and writes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for long tasks
|
||||||
|
if (metrics['long-task-duration'] && metrics['long-task-duration'].count > 0) {
|
||||||
|
recommendations.push('Break down long tasks into smaller chunks');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for low FPS
|
||||||
|
if (metrics.fps && metrics.fps.average < 30) {
|
||||||
|
recommendations.push('Optimize rendering performance to maintain 60fps');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for high memory usage
|
||||||
|
if (metrics['memory-used'] && metrics['memory-limit']) {
|
||||||
|
const memoryUsage = (metrics['memory-used'].average / metrics['memory-limit'].average) * 100;
|
||||||
|
if (memoryUsage > 80) {
|
||||||
|
recommendations.push('Reduce memory usage to prevent performance degradation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all metrics
|
||||||
|
clear(): void {
|
||||||
|
this.metrics.clear();
|
||||||
|
this.marks.clear();
|
||||||
|
performance.clearMarks();
|
||||||
|
performance.clearMeasures();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all monitoring
|
||||||
|
stop(): void {
|
||||||
|
this.observers.forEach((observer) => {
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
this.observers.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience functions
|
||||||
|
export const performanceMonitor = PerformanceMonitor.getInstance();
|
||||||
|
|
||||||
|
// React hook for monitoring component performance
|
||||||
|
export const usePerformanceMonitor = (componentName: string) => {
|
||||||
|
const endMonitoring = React.useCallback(() => {
|
||||||
|
return performanceMonitor.monitorComponentRender(componentName);
|
||||||
|
}, [componentName]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const cleanup = endMonitoring();
|
||||||
|
return cleanup;
|
||||||
|
}, [endMonitoring]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Redux middleware for monitoring selector performance
|
||||||
|
export const performanceMiddleware = (store: any) => (next: any) => (action: any) => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const result = next(action);
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
performanceMonitor.addMetric(`redux-action-${action.type}`, endTime - startTime);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export for global access
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).performanceMonitor = performanceMonitor;
|
||||||
|
}
|
||||||
297
worklenz-frontend/src/utils/performance-optimizer.ts
Normal file
297
worklenz-frontend/src/utils/performance-optimizer.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
// Performance optimization utility for reducing long tasks and improving frame rate
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface PerformanceMetrics {
|
||||||
|
longTaskCount: number;
|
||||||
|
averageLongTaskDuration: number;
|
||||||
|
frameRate: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
layoutThrashingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PerformanceOptimizer {
|
||||||
|
private longTaskObserver: PerformanceObserver | null = null;
|
||||||
|
private frameRateObserver: PerformanceObserver | null = null;
|
||||||
|
private layoutThrashingCount = 0;
|
||||||
|
private metrics: PerformanceMetrics = {
|
||||||
|
longTaskCount: 0,
|
||||||
|
averageLongTaskDuration: 0,
|
||||||
|
frameRate: 0,
|
||||||
|
memoryUsage: 0,
|
||||||
|
layoutThrashingCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeObservers() {
|
||||||
|
// Monitor long tasks
|
||||||
|
if ('PerformanceObserver' in window) {
|
||||||
|
this.longTaskObserver = new PerformanceObserver((list) => {
|
||||||
|
for (const entry of list.getEntries()) {
|
||||||
|
if (entry.entryType === 'longtask') {
|
||||||
|
this.metrics.longTaskCount++;
|
||||||
|
this.metrics.averageLongTaskDuration =
|
||||||
|
(this.metrics.averageLongTaskDuration * (this.metrics.longTaskCount - 1) + entry.duration) / this.metrics.longTaskCount;
|
||||||
|
|
||||||
|
console.warn(`🚨 Long task detected: ${entry.duration.toFixed(2)}ms - Consider chunking this operation`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.longTaskObserver.observe({ entryTypes: ['longtask'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor frame rate
|
||||||
|
this.startFrameRateMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startFrameRateMonitoring() {
|
||||||
|
let frameCount = 0;
|
||||||
|
let lastTime = performance.now();
|
||||||
|
|
||||||
|
const measureFrameRate = () => {
|
||||||
|
frameCount++;
|
||||||
|
const currentTime = performance.now();
|
||||||
|
|
||||||
|
if (currentTime - lastTime >= 1000) {
|
||||||
|
this.metrics.frameRate = frameCount;
|
||||||
|
frameCount = 0;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
if (this.metrics.frameRate < 30) {
|
||||||
|
console.warn(`⚠️ Low frame rate detected: ${this.metrics.frameRate}fps`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(measureFrameRate);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(measureFrameRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk large operations to prevent long tasks
|
||||||
|
static chunkOperation<T>(
|
||||||
|
items: T[],
|
||||||
|
operation: (item: T, index: number) => void,
|
||||||
|
chunkSize: number = 10,
|
||||||
|
delay: number = 16
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const processChunk = () => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const endIndex = Math.min(index + chunkSize, items.length);
|
||||||
|
|
||||||
|
// Process items in this chunk
|
||||||
|
for (let i = index; i < endIndex; i++) {
|
||||||
|
operation(items[i], i);
|
||||||
|
}
|
||||||
|
|
||||||
|
index = endIndex;
|
||||||
|
|
||||||
|
// Check if we need to yield to prevent long tasks
|
||||||
|
const processingTime = performance.now() - startTime;
|
||||||
|
if (processingTime > 16) {
|
||||||
|
console.warn(`⚠️ Chunk processing took ${processingTime.toFixed(2)}ms - consider smaller chunks`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < items.length) {
|
||||||
|
// Schedule next chunk with delay to prevent blocking
|
||||||
|
setTimeout(processChunk, delay);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize DOM operations to prevent layout thrashing
|
||||||
|
static batchDOMOperations(operations: (() => void)[]): void {
|
||||||
|
// Use requestAnimationFrame to batch DOM updates
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Force layout read first
|
||||||
|
document.body.offsetHeight;
|
||||||
|
|
||||||
|
// Perform all write operations
|
||||||
|
operations.forEach(operation => operation());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce function for expensive operations
|
||||||
|
static debounce<T extends (...args: any[]) => void>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): T {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
return ((...args: any[]) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => func(...args), delay);
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle function for frequent operations
|
||||||
|
static throttle<T extends (...args: any[]) => void>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): T {
|
||||||
|
let lastExecTime = 0;
|
||||||
|
|
||||||
|
return ((...args: any[]) => {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
|
||||||
|
if (currentTime - lastExecTime > delay) {
|
||||||
|
func(...args);
|
||||||
|
lastExecTime = currentTime;
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize list rendering with virtualization hints
|
||||||
|
static optimizeListRendering<T>(
|
||||||
|
items: T[],
|
||||||
|
renderItem: (item: T, index: number) => React.ReactNode,
|
||||||
|
options: {
|
||||||
|
chunkSize?: number;
|
||||||
|
virtualizationThreshold?: number;
|
||||||
|
overscanCount?: number;
|
||||||
|
} = {}
|
||||||
|
): React.ReactNode[] {
|
||||||
|
const {
|
||||||
|
chunkSize = 50,
|
||||||
|
virtualizationThreshold = 100,
|
||||||
|
overscanCount = 5
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// For small lists, render everything
|
||||||
|
if (items.length <= virtualizationThreshold) {
|
||||||
|
return items.map((item, index) => renderItem(item, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For large lists, use chunked rendering
|
||||||
|
const chunks: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += chunkSize) {
|
||||||
|
const chunk = items.slice(i, i + chunkSize);
|
||||||
|
chunks.push(
|
||||||
|
React.createElement('div', { key: `chunk-${i}`, className: 'virtualized-chunk' },
|
||||||
|
chunk.map((item, index) => renderItem(item, i + index))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor memory usage
|
||||||
|
static getMemoryUsage(): { used: number; total: number; limit: number } {
|
||||||
|
if ('memory' in performance) {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
return {
|
||||||
|
used: memory.usedJSHeapSize / 1024 / 1024,
|
||||||
|
total: memory.totalJSHeapSize / 1024 / 1024,
|
||||||
|
limit: memory.jsHeapSizeLimit / 1024 / 1024
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { used: 0, total: 0, limit: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize scroll performance
|
||||||
|
static optimizeScroll(container: HTMLElement, handler: (event: Event) => void): () => void {
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
const optimizedHandler = (event: Event) => {
|
||||||
|
if (!ticking) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
handler(event);
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('scroll', optimizedHandler, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('scroll', optimizedHandler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize hover performance
|
||||||
|
static optimizeHover(
|
||||||
|
element: HTMLElement,
|
||||||
|
onEnter: () => void,
|
||||||
|
onLeave: () => void,
|
||||||
|
delay: number = 50
|
||||||
|
): () => void {
|
||||||
|
let enterTimeout: NodeJS.Timeout;
|
||||||
|
let leaveTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
clearTimeout(leaveTimeout);
|
||||||
|
enterTimeout = setTimeout(onEnter, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
clearTimeout(enterTimeout);
|
||||||
|
leaveTimeout = setTimeout(onLeave, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener('mouseenter', handleMouseEnter, { passive: true });
|
||||||
|
element.addEventListener('mouseleave', handleMouseLeave, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(enterTimeout);
|
||||||
|
clearTimeout(leaveTimeout);
|
||||||
|
element.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
|
element.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current performance metrics
|
||||||
|
getMetrics(): PerformanceMetrics {
|
||||||
|
const memory = PerformanceOptimizer.getMemoryUsage();
|
||||||
|
this.metrics.memoryUsage = memory.used;
|
||||||
|
this.metrics.layoutThrashingCount = this.layoutThrashingCount;
|
||||||
|
|
||||||
|
return { ...this.metrics };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset metrics
|
||||||
|
resetMetrics(): void {
|
||||||
|
this.metrics = {
|
||||||
|
longTaskCount: 0,
|
||||||
|
averageLongTaskDuration: 0,
|
||||||
|
frameRate: 0,
|
||||||
|
memoryUsage: 0,
|
||||||
|
layoutThrashingCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup observers
|
||||||
|
destroy(): void {
|
||||||
|
if (this.longTaskObserver) {
|
||||||
|
this.longTaskObserver.disconnect();
|
||||||
|
}
|
||||||
|
if (this.frameRateObserver) {
|
||||||
|
this.frameRateObserver.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const performanceOptimizer = new PerformanceOptimizer();
|
||||||
|
|
||||||
|
// Auto-cleanup on page unload
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
performanceOptimizer.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
export { PerformanceOptimizer, performanceOptimizer };
|
||||||
|
export default PerformanceOptimizer;
|
||||||
Reference in New Issue
Block a user