diff --git a/.jules/bolt.md b/.jules/bolt.md index b08b203a..4aa73190 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -4,3 +4,6 @@ ## 2026-07-12 - Optimize renderTaskRow DOM allocations **Learning:** Caching unattached template nodes and instantiating them via `.cloneNode(false)` reduces DOM instantiation overhead in O(N) render loops significantly. **Action:** Apply this optimization to other hot-path rendering elements such as rows, cells, and stack containers. +## 2026-07-18 - Optimize inner cell DOM allocations +**Learning:** Template caching is effective not just for parent rows/cells, but also for small inner elements (badges, labels) generated repeatedly inside O(N) loops. +**Action:** Extend cloneNode optimizations to all repeatedly created elements inside hot paths, even simple spans and labels. diff --git a/app.js b/app.js index b8c62279..4f86d7e0 100644 --- a/app.js +++ b/app.js @@ -952,6 +952,7 @@ function createWarningBadge(warning) { const persistentOwnerColorMap = new Map(); +let ownerBadgeTemplate = null; function createOwnerCellContent(owner) { if (!owner) { return createEmptyCell(); @@ -961,18 +962,26 @@ function createOwnerCellContent(owner) { persistentOwnerColorMap.set(owner, OWNER_COLORS[persistentOwnerColorMap.size % OWNER_COLORS.length]); } - const badge = document.createElement('span'); - badge.className = 'owner-badge'; + if (!ownerBadgeTemplate) { + ownerBadgeTemplate = document.createElement('span'); + ownerBadgeTemplate.className = 'owner-badge'; + } + + const badge = ownerBadgeTemplate.cloneNode(false); badge.style.background = persistentOwnerColorMap.get(owner); badge.textContent = owner; return badge; } +let statusBadgeTemplate = null; function createStatusCellContent(progressState) { if (!progressState.label) { return createEmptyCell(); } - const badge = document.createElement('span'); + if (!statusBadgeTemplate) { + statusBadgeTemplate = document.createElement('span'); + } + const badge = statusBadgeTemplate.cloneNode(false); badge.className = `status-badge ${progressState.className}`; badge.textContent = progressState.label; if (progressState.description) { @@ -993,12 +1002,22 @@ function createMetricText(value, testId = '') { return metric; } +let actualProgressLabelTemplate = null; +let actualProgressSrOnlyTemplate = null; + function createActualProgressCellContent(task, taskMetrics) { - const label = document.createElement('label'); + if (!actualProgressLabelTemplate) { + actualProgressLabelTemplate = document.createElement('label'); + } + if (!actualProgressSrOnlyTemplate) { + actualProgressSrOnlyTemplate = document.createElement('span'); + actualProgressSrOnlyTemplate.className = 'sr-only'; + } + + const label = actualProgressLabelTemplate.cloneNode(false); const fieldId = `actual-progress-${task.id}`; label.htmlFor = fieldId; - const srOnly = document.createElement('span'); - srOnly.className = 'sr-only'; + const srOnly = actualProgressSrOnlyTemplate.cloneNode(false); const rowEntityName = task.task || task.activity || task.phase || '작업'; srOnly.textContent = `실적진척상태 - ${rowEntityName}`; if (!actualProgressSelectTemplate) {