马一丁

Optimize Front-End Memory Usage

@@ -1226,6 +1226,111 @@ @@ -1226,6 +1226,111 @@
1226 const consoleLayers = {}; 1226 const consoleLayers = {};
1227 const consoleLayerScrollPositions = {}; 1227 const consoleLayerScrollPositions = {};
1228 let activeConsoleLayer = currentApp; 1228 let activeConsoleLayer = currentApp;
  1229 + const logRenderers = {};
  1230 +
  1231 + // 轻量日志虚拟渲染器:不限制总行数,使用可视窗口渲染 + 节流
  1232 + class LogVirtualList {
  1233 + constructor(container) {
  1234 + this.container = container;
  1235 + this.lines = [];
  1236 + this.pending = [];
  1237 + this.pool = [];
  1238 + this.lineHeight = 18;
  1239 + this.maxVisible = 120;
  1240 + this.rafId = null;
  1241 + this.attachScroll();
  1242 + }
  1243 +
  1244 + attachScroll() {
  1245 + if (!this.container) return;
  1246 + this.container.addEventListener('scroll', () => this.scheduleRender());
  1247 + }
  1248 +
  1249 + setLineHeight(px) {
  1250 + if (px > 0) this.lineHeight = px;
  1251 + }
  1252 +
  1253 + append(text, className = 'console-line') {
  1254 + this.pending.push({ text, className });
  1255 + if (this.pending.length > 200) {
  1256 + this.flush();
  1257 + }
  1258 + this.scheduleRender();
  1259 + }
  1260 +
  1261 + clear(message = null) {
  1262 + this.lines = [];
  1263 + this.pending = [];
  1264 + this.pool = [];
  1265 + if (message) {
  1266 + this.lines.push({ text: message, className: 'console-line' });
  1267 + }
  1268 + this.scheduleRender(true);
  1269 + }
  1270 +
  1271 + flush() {
  1272 + if (!this.pending.length) return;
  1273 + this.lines.push(...this.pending);
  1274 + this.pending = [];
  1275 + }
  1276 +
  1277 + scheduleRender(force = false) {
  1278 + if (!this.container) return;
  1279 + if (!force && this.rafId) return;
  1280 + this.rafId = requestAnimationFrame(() => {
  1281 + this.rafId = null;
  1282 + this.render();
  1283 + });
  1284 + }
  1285 +
  1286 + render() {
  1287 + this.flush();
  1288 + const total = this.lines.length;
  1289 + if (!total) {
  1290 + this.container.innerHTML = '';
  1291 + return;
  1292 + }
  1293 +
  1294 + const lh = this.lineHeight;
  1295 + const viewport = this.container.clientHeight || 1;
  1296 + const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
  1297 +
  1298 + const start = Math.max(0, Math.floor(this.container.scrollTop / lh) - Math.floor(visible / 2));
  1299 + const end = Math.min(total, start + visible);
  1300 + const beforeHeight = start * lh;
  1301 + const afterHeight = (total - end) * lh;
  1302 +
  1303 + const needed = end - start;
  1304 + while (this.pool.length < needed) {
  1305 + const node = document.createElement('div');
  1306 + node.className = 'console-line';
  1307 + this.pool.push(node);
  1308 + }
  1309 +
  1310 + const fragment = document.createDocumentFragment();
  1311 + for (let idx = start; idx < end; idx++) {
  1312 + const line = this.lines[idx];
  1313 + const node = this.pool[idx - start];
  1314 + node.className = line.className || 'console-line';
  1315 + node.textContent = line.text;
  1316 + fragment.appendChild(node);
  1317 + }
  1318 +
  1319 + this.container.innerHTML = '';
  1320 + const beforeSpacer = document.createElement('div');
  1321 + beforeSpacer.style.height = `${beforeHeight}px`;
  1322 + const afterSpacer = document.createElement('div');
  1323 + afterSpacer.style.height = `${afterHeight}px`;
  1324 + this.container.appendChild(beforeSpacer);
  1325 + this.container.appendChild(fragment);
  1326 + this.container.appendChild(afterSpacer);
  1327 +
  1328 + const shouldStick = (this.container.scrollTop + this.container.clientHeight) >= (this.container.scrollHeight - lh * 2);
  1329 + if (shouldStick) {
  1330 + this.container.scrollTop = this.container.scrollHeight;
  1331 + }
  1332 + }
  1333 + }
1229 1334
1230 const CONFIG_ENDPOINT = '/api/config'; 1335 const CONFIG_ENDPOINT = '/api/config';
1231 const SYSTEM_STATUS_ENDPOINT = '/api/system/status'; 1336 const SYSTEM_STATUS_ENDPOINT = '/api/system/status';
@@ -2109,6 +2214,7 @@ @@ -2109,6 +2214,7 @@
2109 placeholder.className = 'console-line'; 2214 placeholder.className = 'console-line';
2110 placeholder.textContent = `[系统] ${appNames[app] || app} 日志就绪`; 2215 placeholder.textContent = `[系统] ${appNames[app] || app} 日志就绪`;
2111 layer.appendChild(placeholder); 2216 layer.appendChild(placeholder);
  2217 + logRenderers[app] = new LogVirtualList(layer);
2112 2218
2113 container.appendChild(layer); 2219 container.appendChild(layer);
2114 consoleLayers[app] = layer; 2220 consoleLayers[app] = layer;
@@ -2136,6 +2242,7 @@ @@ -2136,6 +2242,7 @@
2136 2242
2137 container.appendChild(layer); 2243 container.appendChild(layer);
2138 consoleLayers[app] = layer; 2244 consoleLayers[app] = layer;
  2245 + logRenderers[app] = new LogVirtualList(layer);
2139 return layer; 2246 return layer;
2140 } 2247 }
2141 2248
@@ -2169,40 +2276,28 @@ @@ -2169,40 +2276,28 @@
2169 return; 2276 return;
2170 } 2277 }
2171 2278
2172 - const container = getConsoleContainer();  
2173 - if (container) {  
2174 - container.scrollTop = container.scrollHeight;  
2175 - consoleLayerScrollPositions[app] = container.scrollTop; 2279 + const renderer = logRenderers[app];
  2280 + if (renderer && renderer.container) {
  2281 + renderer.container.scrollTop = renderer.container.scrollHeight;
  2282 + consoleLayerScrollPositions[app] = renderer.container.scrollTop;
2176 } 2283 }
2177 } 2284 }
2178 2285
2179 function appendConsoleTextLine(app, text, className = 'console-line') { 2286 function appendConsoleTextLine(app, text, className = 'console-line') {
2180 - const layer = getConsoleLayer(app);  
2181 - if (!layer) return;  
2182 -  
2183 - const line = document.createElement('div');  
2184 - line.className = className;  
2185 - line.textContent = text;  
2186 - layer.appendChild(line);  
2187 - syncConsoleScroll(app); 2287 + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
  2288 + renderer.append(text, className);
2188 } 2289 }
2189 2290
2190 function appendConsoleElement(app, element) { 2291 function appendConsoleElement(app, element) {
2191 - const layer = getConsoleLayer(app);  
2192 - if (!layer || !element) return;  
2193 -  
2194 - layer.appendChild(element);  
2195 - syncConsoleScroll(app); 2292 + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
  2293 + if (!element || !renderer.container) return;
  2294 + renderer.container.appendChild(element);
  2295 + renderer.scheduleRender(true);
2196 } 2296 }
2197 2297
2198 function clearConsoleLayer(app, message = null) { 2298 function clearConsoleLayer(app, message = null) {
2199 - const layer = getConsoleLayer(app);  
2200 - if (!layer) return;  
2201 -  
2202 - layer.innerHTML = '';  
2203 - if (message) {  
2204 - appendConsoleTextLine(app, message);  
2205 - } 2299 + const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
  2300 + renderer.clear(message);
2206 } 2301 }
2207 2302
2208 // 加载控制台输出 2303 // 加载控制台输出