perf: 优化 diff 页面渲染,消除卡顿
This commit is contained in:
+146
-54
@@ -7,6 +7,10 @@ const DiffViewer = {
|
|||||||
this.currentMode = 'side-by-side';
|
this.currentMode = 'side-by-side';
|
||||||
this.files = []; // parsed file info for tree
|
this.files = []; // parsed file info for tree
|
||||||
this.observer = null; // scroll observer for highlight sync
|
this.observer = null; // scroll observer for highlight sync
|
||||||
|
this._highlightObserver = null; // lazy hljs observer
|
||||||
|
this._fileIndex = null; // filename → { wrapper, rows: Map<lineNum, row> }
|
||||||
|
this._lastActiveFile = null; // last highlighted tree item
|
||||||
|
this._rafPending = false; // rAF throttle flag for scroll spy
|
||||||
|
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
console.error('Diff container not found:', containerId);
|
console.error('Diff container not found:', containerId);
|
||||||
@@ -215,21 +219,20 @@ const DiffViewer = {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Add syntax highlighting
|
|
||||||
this.container.querySelectorAll('pre code').forEach(block => {
|
|
||||||
if (window.hljs) {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add data-filename attributes to file wrappers for scroll sync
|
// Add data-filename attributes to file wrappers for scroll sync
|
||||||
this._annotateFileSections();
|
this._annotateFileSections();
|
||||||
|
|
||||||
|
// Build index for fast suggestion insertion
|
||||||
|
this._buildFileIndex();
|
||||||
|
|
||||||
// Bind file tree click events
|
// Bind file tree click events
|
||||||
this._bindFileTreeEvents();
|
this._bindFileTreeEvents();
|
||||||
|
|
||||||
// Set up scroll spy
|
// Set up scroll spy
|
||||||
this._setupScrollSpy();
|
this._setupScrollSpy();
|
||||||
|
|
||||||
|
// Lazy syntax highlighting (only when code blocks scroll into view)
|
||||||
|
this._setupLazyHighlight();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Render per-file diffs
|
// Render per-file diffs
|
||||||
@@ -286,15 +289,11 @@ const DiffViewer = {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
this.container.querySelectorAll('pre code').forEach(block => {
|
|
||||||
if (window.hljs) {
|
|
||||||
hljs.highlightElement(block);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._annotateFileSections();
|
this._annotateFileSections();
|
||||||
|
this._buildFileIndex();
|
||||||
this._bindFileTreeEvents();
|
this._bindFileTreeEvents();
|
||||||
this._setupScrollSpy();
|
this._setupScrollSpy();
|
||||||
|
this._setupLazyHighlight();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Annotate diff2html file wrappers with data-filename
|
// Annotate diff2html file wrappers with data-filename
|
||||||
@@ -311,6 +310,67 @@ const DiffViewer = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Build index: filename → { wrapper, rows: Map<lineNum, row> } for O(1) suggestion lookup
|
||||||
|
_buildFileIndex() {
|
||||||
|
this._fileIndex = new Map();
|
||||||
|
const diffContent = this.container.querySelector('#diff-content');
|
||||||
|
if (!diffContent) return;
|
||||||
|
|
||||||
|
const fileWrappers = diffContent.querySelectorAll('.d2h-file-wrapper');
|
||||||
|
fileWrappers.forEach(wrapper => {
|
||||||
|
const filename = wrapper.getAttribute('data-filename');
|
||||||
|
if (!filename) return;
|
||||||
|
|
||||||
|
const rows = new Map();
|
||||||
|
const table = wrapper.querySelector('.d2h-diff-table');
|
||||||
|
if (table) {
|
||||||
|
table.querySelectorAll('tr').forEach(row => {
|
||||||
|
// Index by left (old) line number
|
||||||
|
const leftCell = row.querySelector('.d2h-code-linenumber .d2h-code-side-linenumber');
|
||||||
|
if (leftCell) {
|
||||||
|
const num = parseInt(leftCell.textContent.trim());
|
||||||
|
if (num) rows.set('left-' + num, row);
|
||||||
|
}
|
||||||
|
// Index by right (new) line number
|
||||||
|
const rightCell = row.querySelector('.d2h-code-linenumber:not(.d2h-code-side-linenumber)');
|
||||||
|
if (rightCell) {
|
||||||
|
const num = parseInt(rightCell.textContent.trim());
|
||||||
|
if (num) rows.set('right-' + num, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fileIndex.set(filename, { wrapper, rows });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lazy syntax highlighting: only highlight code blocks when they scroll into view
|
||||||
|
_setupLazyHighlight() {
|
||||||
|
if (this._highlightObserver) {
|
||||||
|
this._highlightObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeBlocks = this.container.querySelectorAll('pre code');
|
||||||
|
if (codeBlocks.length === 0 || !window.hljs) return;
|
||||||
|
|
||||||
|
this._highlightObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const block = entry.target;
|
||||||
|
if (!block.dataset.highlighted) {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
}
|
||||||
|
this._highlightObserver.unobserve(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
rootMargin: '200px', // start highlighting 200px before visible
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
codeBlocks.forEach(block => this._highlightObserver.observe(block));
|
||||||
|
},
|
||||||
|
|
||||||
// Bind click events on file tree items
|
// Bind click events on file tree items
|
||||||
_bindFileTreeEvents() {
|
_bindFileTreeEvents() {
|
||||||
const treeItems = this.container.querySelectorAll('.file-tree-file');
|
const treeItems = this.container.querySelectorAll('.file-tree-file');
|
||||||
@@ -378,25 +438,45 @@ const DiffViewer = {
|
|||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._lastActiveFile = null;
|
||||||
|
this._rafPending = false;
|
||||||
|
|
||||||
const fileWrappers = diffContent.querySelectorAll('.d2h-file-wrapper');
|
const fileWrappers = diffContent.querySelectorAll('.d2h-file-wrapper');
|
||||||
if (fileWrappers.length === 0) return;
|
if (fileWrappers.length === 0) return;
|
||||||
|
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
// Throttle: batch all entries per frame, only process the most visible one
|
||||||
|
if (this._rafPending) return;
|
||||||
|
this._rafPending = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this._rafPending = false;
|
||||||
|
|
||||||
|
// Find the most recently intersecting entry
|
||||||
|
let bestEntry = null;
|
||||||
|
for (const entry of entries) {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const filename = entry.target.getAttribute('data-filename');
|
bestEntry = entry;
|
||||||
if (filename) {
|
break; // first intersecting is enough
|
||||||
// Highlight in tree
|
|
||||||
this.container.querySelectorAll('.file-tree-file').forEach(item => {
|
|
||||||
const treeName = item.getAttribute('data-filename');
|
|
||||||
if (treeName && (treeName === filename || treeName.endsWith('/' + filename) || filename.endsWith('/' + treeName))) {
|
|
||||||
item.classList.add('active');
|
|
||||||
// Scroll tree item into view if needed
|
|
||||||
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
} else {
|
|
||||||
item.classList.remove('active');
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
if (!bestEntry) return;
|
||||||
|
|
||||||
|
const filename = bestEntry.target.getAttribute('data-filename');
|
||||||
|
if (!filename || filename === this._lastActiveFile) return;
|
||||||
|
this._lastActiveFile = filename;
|
||||||
|
|
||||||
|
// Only toggle the changed items, not full scan
|
||||||
|
const treeFiles = this.container.querySelectorAll('.file-tree-file');
|
||||||
|
for (const item of treeFiles) {
|
||||||
|
const treeName = item.getAttribute('data-filename');
|
||||||
|
if (!treeName) continue;
|
||||||
|
const isActive = treeName === filename || treeName.endsWith('/' + filename) || filename.endsWith('/' + treeName);
|
||||||
|
if (isActive && !item.classList.contains('active')) {
|
||||||
|
item.classList.add('active');
|
||||||
|
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
} else if (!isActive && item.classList.contains('active')) {
|
||||||
|
item.classList.remove('active');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -452,11 +532,36 @@ const DiffViewer = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Switch view mode (side-by-side or line-by-line)
|
// Switch view mode (side-by-side or line-by-line) — only rebuild diff content, not the whole layout
|
||||||
setViewMode(mode) {
|
setViewMode(mode) {
|
||||||
if (!this.container || !this.rawDiff) return;
|
if (!this.container || !this.rawDiff) return;
|
||||||
this.currentMode = mode;
|
this.currentMode = mode;
|
||||||
this.renderDiff(this.rawDiff, { outputFormat: mode === 'unified' ? 'line-by-line' : 'side-by-side' });
|
|
||||||
|
const outputFormat = mode === 'unified' ? 'line-by-line' : 'side-by-side';
|
||||||
|
const config = {
|
||||||
|
drawFileList: false,
|
||||||
|
fileListToggle: false,
|
||||||
|
fileContentToggle: true,
|
||||||
|
matching: 'lines',
|
||||||
|
outputFormat: outputFormat,
|
||||||
|
synchronisedScroll: true,
|
||||||
|
highlight: true,
|
||||||
|
renderNothingWhenEmpty: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffContent = this.container.querySelector('#diff-content');
|
||||||
|
if (!diffContent) {
|
||||||
|
// Fallback: full re-render
|
||||||
|
this.renderDiff(this.rawDiff, { outputFormat });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only rebuild the diff content area
|
||||||
|
diffContent.innerHTML = Diff2Html.html(this.rawDiff, config);
|
||||||
|
this._annotateFileSections();
|
||||||
|
this._buildFileIndex();
|
||||||
|
this._setupScrollSpy();
|
||||||
|
this._setupLazyHighlight();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle file list visibility
|
// Toggle file list visibility
|
||||||
@@ -483,34 +588,24 @@ const DiffViewer = {
|
|||||||
const diffContent = this.container.querySelector('#diff-content');
|
const diffContent = this.container.querySelector('#diff-content');
|
||||||
if (!diffContent) return;
|
if (!diffContent) return;
|
||||||
|
|
||||||
// Find the target file wrapper
|
// Use pre-built index for O(1) file lookup
|
||||||
const fileWrappers = diffContent.querySelectorAll('.d2h-file-wrapper');
|
if (!this._fileIndex) return;
|
||||||
let targetWrapper = null;
|
|
||||||
for (const wrapper of fileWrappers) {
|
let fileEntry = null;
|
||||||
const wrapperName = wrapper.getAttribute('data-filename');
|
// Try exact match first, then suffix match
|
||||||
if (wrapperName && (wrapperName === filename || wrapperName.endsWith('/' + filename) || filename.endsWith('/' + wrapperName))) {
|
for (const [key, val] of this._fileIndex) {
|
||||||
targetWrapper = wrapper;
|
if (key === filename || key.endsWith('/' + filename) || filename.endsWith('/' + key)) {
|
||||||
|
fileEntry = val;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!targetWrapper) return;
|
if (!fileEntry) return;
|
||||||
|
|
||||||
// Find the target line
|
// O(1) row lookup via index
|
||||||
const table = targetWrapper.querySelector('.d2h-diff-table');
|
const rowKey = side + '-' + line;
|
||||||
if (!table) return;
|
const row = fileEntry.rows.get(rowKey);
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
const rows = table.querySelectorAll('tr');
|
|
||||||
for (const row of rows) {
|
|
||||||
// diff2html uses data-line-number on td elements
|
|
||||||
const lineNumCell = side === 'left'
|
|
||||||
? row.querySelector('.d2h-code-linenumber .d2h-code-side-linenumber')
|
|
||||||
: row.querySelector('.d2h-code-linenumber:not(.d2h-code-side-linenumber)');
|
|
||||||
|
|
||||||
if (!lineNumCell) continue;
|
|
||||||
|
|
||||||
const lineText = lineNumCell.textContent.trim();
|
|
||||||
const lineNum = parseInt(lineText);
|
|
||||||
if (lineNum === line) {
|
|
||||||
// Create suggestion card
|
// Create suggestion card
|
||||||
const severityStyles = {
|
const severityStyles = {
|
||||||
critical: 'border-l-4 border-red-500 bg-red-50',
|
critical: 'border-l-4 border-red-500 bg-red-50',
|
||||||
@@ -539,9 +634,6 @@ const DiffViewer = {
|
|||||||
|
|
||||||
// Insert after the current row
|
// Insert after the current row
|
||||||
row.parentNode.insertBefore(suggestionRow, row.nextSibling);
|
row.parentNode.insertBefore(suggestionRow, row.nextSibling);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user