feat: 全新 Korom 资源管理服务组件,优化内存占用

新增完整的程序本体资源管理与内存优化功能:
1. 新增背景图预处理服务,主进程将大图降采样至屏幕适配尺寸,避免全分辨率大图占用渲染进程大量内存
2. 实现资源注册表系统,支持引用计数、LRU自动逐出与释放回调
3. 新增资源与内存调试面板,可查看渲染进程JS堆、Electron进程内存与资源缓存状态
4. 修复进程内存信息API的单位与字段映射错误
5. 优化背景层缓存键避免过长dataURL,为Silk动画添加窗口隐藏时停帧逻辑减少GPU占用
6. 修复资源注册表类型适配问题并更新项目文档
This commit is contained in:
2026-09-04 18:56:43 +08:00
parent 7d8b7bd433
commit cd4578de5a
15 changed files with 452 additions and 15 deletions
+109
View File
@@ -0,0 +1,109 @@
/**
* 背景图处理服务(主进程,程序本体资源管理)。
*
* 目标:把用户自选的大图背景在进入渲染进程前,降采样到
* 「窗口实际需要」的尺寸(长边按 maxEdge 限制),从而避免
* 数 MB~数十 MB 的原图以 base64 + 全分辨率解码的形式常驻内存,
* 且不改变可见显示效果(超出屏幕物理像素的部分在视觉上不可见)。
*
* 规则:
* - 长边 ≤ maxEdge → 原样返回(零损耗,效果 100% 一致);
* - 长边 > maxEdge → 等比 resize 后重编码(JPEG 有损 q0.9 / 带透明通道用 PNG 无损);
* - 动画 GIF / 解析失败 / 无法解码 → 返回 null 或原样,由调用方回退到原始文件(不改变现有行为)。
*/
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
const { nativeImage } = electron;
export interface PreparedBackground {
dataUrl: string | null;
bytes: number;
width: number;
height: number;
optimized: boolean;
}
const MIME_MAP: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
};
const TRANSPARENT_MIMES = new Set(['image/png', 'image/webp', 'image/gif']);
function bufferToDataUrl(buffer: Buffer, mime: string): string {
return `data:${mime};base64,${buffer.toString('base64')}`;
}
/**
* 解析并(按需)优化背景图片,返回可直接给 CSS 使用的 data URL。
* @param srcPath 原图文件路径
* @param maxEdge 目标长边上限(像素),默认 4096
*/
export function prepareBackgroundImage(srcPath: string, maxEdge = 4096): PreparedBackground | null {
const raw: PreparedBackground = { dataUrl: null, bytes: 0, width: 0, height: 0, optimized: false };
try {
const buffer = fs.readFileSync(srcPath);
const ext = path.extname(srcPath).toLowerCase();
const mime = MIME_MAP[ext] || 'image/png';
// 动画 GIFnativeImage 只能解码首帧,直接原样返回,避免破坏动画
if (mime === 'image/gif') {
raw.dataUrl = bufferToDataUrl(buffer, mime);
raw.bytes = buffer.length;
return raw;
}
const image = nativeImage.createFromBuffer(buffer);
if (image.isEmpty()) return null;
const size = image.getSize();
const longEdge = Math.max(size.width, size.height);
const needResize = longEdge > maxEdge && size.width > 0 && size.height > 0;
// 仅当确实需要降尺寸时才做重编码(视觉零影响的边界:超出屏幕物理像素的部分不可见);
// 未超限但体积大的图原样返回,避免任何有损重编码改变显示效果。
if (!needResize) {
raw.dataUrl = bufferToDataUrl(buffer, mime);
raw.bytes = buffer.length;
raw.width = size.width;
raw.height = size.height;
return raw;
}
let output = image;
const scale = maxEdge / longEdge;
const w = Math.max(1, Math.round(size.width * scale));
const h = Math.max(1, Math.round(size.height * scale));
output = image.resize({ width: w, height: h, quality: 'best' });
if (output.isEmpty()) return null;
const outSize = output.getSize();
const hasTransparency = TRANSPARENT_MIMES.has(mime);
let outBuffer: Buffer;
let outMime: string;
if (hasTransparency) {
outBuffer = output.toPNG();
outMime = 'image/png';
} else {
outBuffer = output.toJPEG(90);
outMime = 'image/jpeg';
}
if (outBuffer.length === 0) return null;
raw.dataUrl = bufferToDataUrl(outBuffer, outMime);
raw.bytes = outBuffer.length;
raw.width = outSize.width;
raw.height = outSize.height;
raw.optimized = true;
return raw;
} catch {
return null;
}
}
+11
View File
@@ -2,6 +2,7 @@ import electron from 'electron';
import fs from 'fs';
import path from 'path';
import { loadConfig, saveConfig } from '../config';
import { prepareBackgroundImage } from '../core/background-image';
const { ipcMain } = electron;
@@ -117,4 +118,14 @@ export function registerBackgroundHandlers() {
return null;
}
});
// 背景图降采样/重编码(程序本体资源管理):把大图在进渲染进程前压到屏幕所需尺寸
ipcMain.handle('background:prepare', async (_event, payload: { srcPath: string; maxEdge?: number }) => {
try {
const result = prepareBackgroundImage(payload.srcPath, payload.maxEdge || 4096);
return { success: true, data: result, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+2 -2
View File
@@ -81,8 +81,8 @@ export function registerSystemHandlers() {
try {
const info = await process.getProcessMemoryInfo();
mainProcess = {
workingSetSize: info.workingSetSize,
privateBytes: info.privateBytes,
workingSetSize: info.residentSet ?? 0,
privateBytes: info.private ?? 0,
};
} catch {
// 个别平台不支持 getProcessMemoryInfo,忽略
+28 -3
View File
@@ -23,6 +23,31 @@ function getFileAsDataUrl(filePath: string): string | null {
}
}
// 按当前窗口实际像素需求计算背景图长边上限(含高分屏余量),
// 避免把数 MB~数十 MB 原图原样塞进渲染进程。
function computeMaxEdge(): number {
const dpr = window.devicePixelRatio || 1;
const css = Math.max(window.innerWidth || 1280, window.innerHeight || 800);
const target = Math.round(Math.max(css, 1920) * dpr * 1.1);
return Math.min(4096, Math.max(1920, target));
}
// 经主进程降采样/重编码后返回 data URL;主进程无法处理时回退到原始文件(行为不变)。
async function prepareBackgroundDataUrl(filePath: string): Promise<string | null> {
try {
const result = (await ipcRenderer.invoke('background:prepare', {
srcPath: filePath,
maxEdge: computeMaxEdge(),
})) as { success?: boolean; data?: { dataUrl?: string | null } | null };
if (result?.success && typeof result.data?.dataUrl === 'string' && result.data.dataUrl.length > 0) {
return result.data.dataUrl;
}
} catch {
// fallthrough to raw
}
return getFileAsDataUrl(filePath);
}
contextBridge.exposeInMainWorld('electronAPI', {
// Generic IPC
invoke: (channel: string, ...args: unknown[]) =>
@@ -76,14 +101,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Copy to userData via main process
const destPath = await ipcRenderer.invoke('background:copyToUserData', srcPath, ext);
if (!destPath) return null;
return getFileAsDataUrl(destPath);
return prepareBackgroundDataUrl(destPath);
},
// Get cached background as base64 data URL
// Get cached background as base64 data URL(自动降采样到屏幕所需尺寸)
getBackgroundDataUrl: async (): Promise<string | null> => {
const filePath = await ipcRenderer.invoke('background:getCachedPath');
if (!filePath) return null;
return getFileAsDataUrl(filePath);
return prepareBackgroundDataUrl(filePath);
},
// Open external URL in system browser