diff --git a/.jules/bolt.md b/.jules/bolt.md index b08b203a..3f6eb67b 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-15 - Optimize DOM allocations in inner cell renderers +**Learning:** In addition to row and general cell templates, caching the inner DOM structures of specific cell types (like badges, labels, and text wrappers) and instantiating them via `.cloneNode(false)` avoids repetitive O(N) `document.createElement()` overhead during table rendering. +**Action:** Extract static element definitions in inner cell rendering functions to global template variables and use `.cloneNode`. diff --git a/app.js b/app.js index b8c62279..f02ef0d3 100644 --- a/app.js +++ b/app.js @@ -898,6 +898,9 @@ function createTreeCellContent(value, depth) { return treeValue; } +let textCellWrapperTemplate = null; +let textCellValidationTemplate = null; + function createTextCellContent(value, warning = '') { if (!value) { return warning ? createWarningBadge(warning) : createEmptyCell(); @@ -905,10 +908,16 @@ function createTextCellContent(value, warning = '') { if (!warning) { return document.createTextNode(value); } - const wrapper = document.createElement('div'); + + if (!textCellWrapperTemplate) { + textCellWrapperTemplate = document.createElement('div'); + textCellValidationTemplate = document.createElement('div'); + textCellValidationTemplate.className = 'validation-message'; + } + + const wrapper = textCellWrapperTemplate.cloneNode(false); wrapper.appendChild(document.createTextNode(value)); - const validation = document.createElement('div'); - validation.className = 'validation-message'; + const validation = textCellValidationTemplate.cloneNode(false); validation.textContent = warning; wrapper.appendChild(validation); return wrapper; @@ -951,6 +960,7 @@ function createWarningBadge(warning) { } const persistentOwnerColorMap = new Map(); +let ownerBadgeTemplate = null; function createOwnerCellContent(owner) { if (!owner) { @@ -961,18 +971,27 @@ 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 +1012,23 @@ function createMetricText(value, testId = '') { return metric; } +let actualProgressLabelTemplate = null; +let actualProgressSrOnlyTemplate = null; +let actualProgressValidationTemplate = null; + function createActualProgressCellContent(task, taskMetrics) { - const label = document.createElement('label'); + if (!actualProgressLabelTemplate) { + actualProgressLabelTemplate = document.createElement('label'); + actualProgressSrOnlyTemplate = document.createElement('span'); + actualProgressSrOnlyTemplate.className = 'sr-only'; + actualProgressValidationTemplate = document.createElement('div'); + actualProgressValidationTemplate.className = 'validation-message'; + } + + 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) { @@ -1021,9 +1051,8 @@ function createActualProgressCellContent(task, taskMetrics) { const warning = taskMetrics.plannedDateWarning || taskMetrics.actualDateWarning; if (warning) { - const validation = document.createElement('div'); + const validation = actualProgressValidationTemplate.cloneNode(false); validation.id = `actual-progress-error-${task.id}`; - validation.className = 'validation-message'; validation.textContent = warning; label.appendChild(validation); select.setAttribute('aria-invalid', 'true');