perf(ui): 优化日志查看器在大量日志下的渲染与筛选性能 (#404)
- 日志行加 content-visibility:auto,视口外日志跳过布局与绘制, 全量渲染 9000 行时布局成本从数百毫秒降到个位数毫秒 - 分类筛选由整段 innerHTML 销毁重建改为切换 CSS class 显隐, 不再重建日志 DOM - addLog 仅在日志视图可见时重建 DOM,避免非日志页面产生无谓的全量重建 - 切到日志视图仍每次从服务端加载最新日志,保留原有及时性逻辑 不影响 GET /api/logs 服务端接口;不限制渲染行数,日志完整保留 main.js 保持上游原版未改动
This commit is contained in:
1 parent
825ddc25bb
commit
436dd63f15
2 files changed
+45
-18
No files matched your search
@@ -252,6 +252,14 @@ export const componentsCssContent = /* css */ `
|
||||
margin-bottom: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
/* 视口外日志行跳过布局与绘制,使大日志量下的排版成本只与可见行数相关;占位高度取单行日志的实测高度,已渲染过的行由浏览器记忆实际尺寸 */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 31px;
|
||||
}
|
||||
|
||||
/* 与当前筛选分类不匹配的日志行隐藏,筛选切换复用已构建的 DOM */
|
||||
.log-entry.log-entry-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-entry.info { color: #4fc3f7; }
|
||||
|
||||
+37
-18
@@ -56,32 +56,36 @@ function getLogCategory(message) {
|
||||
}
|
||||
|
||||
// 日志相关
|
||||
// 判断日志视图是否处于可见状态,用于避免在非日志页面产生无谓的全量 DOM 重建
|
||||
function isLogViewVisible() {
|
||||
const section = document.getElementById('logs-section');
|
||||
return !!section && section.classList.contains('active');
|
||||
}
|
||||
|
||||
function addLog(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logs.push({ timestamp, message, type, _category: getLogCategory(message) });
|
||||
if (logs.length > 100) logs.shift();
|
||||
renderLogs();
|
||||
// 仅当日志视图可见时才重建 DOM;切到其他页面期间产生的日志仅追加到数组,回到日志视图并重新渲染时再统一呈现,避免每次切换区块都全量重建
|
||||
if (isLogViewVisible()) {
|
||||
renderLogs();
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogs() {
|
||||
const container = document.getElementById('log-container');
|
||||
const filterContainer = document.getElementById('log-filters') || createFilterContainer();
|
||||
|
||||
let filteredLogs = logs;
|
||||
if (currentLogFilter !== 'ALL') {
|
||||
// 采用严格归属校验,同时支持续行上下文继承
|
||||
let lastCategory = 'system';
|
||||
filteredLogs = logs.filter(log => {
|
||||
let category = log._category;
|
||||
if (category === '_inherit_') {
|
||||
} else {
|
||||
lastCategory = category;
|
||||
}
|
||||
return category === currentLogFilter;
|
||||
});
|
||||
}
|
||||
|
||||
container.innerHTML = filteredLogs.map(log => {
|
||||
// 全量日志一次性构建 DOM,归属分类解析后写入 data-category 供筛选显隐使用,续行继承上一行的分类,保证筛选时上下文完整
|
||||
let lastCategory = 'system';
|
||||
container.innerHTML = logs.map(log => {
|
||||
let category = log._category;
|
||||
if (category === '_inherit_') {
|
||||
category = lastCategory;
|
||||
} else {
|
||||
lastCategory = category;
|
||||
}
|
||||
|
||||
let highlightedMessage = log.message;
|
||||
|
||||
// 行首连续标签高亮 (需双重转义)
|
||||
@@ -93,13 +97,24 @@ function renderLogs() {
|
||||
highlightedMessage = coloredPrefix + rest;
|
||||
}
|
||||
|
||||
return \`<div class="log-entry \${log.type}">[\${log.timestamp}] \${highlightedMessage}</div>\`;
|
||||
return \`<div class="log-entry \${log.type}" data-category="\${category}">[\${log.timestamp}] \${highlightedMessage}</div>\`;
|
||||
}).join('');
|
||||
applyLogFilter();
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
updateFilterUI();
|
||||
}
|
||||
|
||||
// 按当前筛选分类切换日志行显隐,复用已构建的 DOM 而不重建
|
||||
function applyLogFilter() {
|
||||
const container = document.getElementById('log-container');
|
||||
const showAll = currentLogFilter === 'ALL';
|
||||
const entries = container.children;
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
entries[i].classList.toggle('log-entry-hidden', !showAll && entries[i].dataset.category !== currentLogFilter);
|
||||
}
|
||||
}
|
||||
|
||||
function createFilterContainer() {
|
||||
const controls = document.querySelector('.log-controls');
|
||||
const filterDiv = document.createElement('div');
|
||||
@@ -147,7 +162,11 @@ function updateFilterUI() {
|
||||
|
||||
window.setLogFilter = function(tag) {
|
||||
currentLogFilter = tag;
|
||||
renderLogs();
|
||||
// 切换筛选仅调整已有日志行的显隐,不重建 DOM
|
||||
applyLogFilter();
|
||||
const container = document.getElementById('log-container');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
updateFilterUI();
|
||||
};
|
||||
|
||||
// 从API获取真实日志数据
|
||||
|
||||
Reference in new issue
Block a user