Electron 白屏是最容易让人骂娘的一种故障:开发环境跑得好好的,打包给用户就是一块白。更折磨的是它没有报错弹窗、没有日志、什么都看不出来,你只能猜。这篇文章把我自己应用(一个塞了十几个功能模块的本地优先桌面应用)上线后遇到过的白屏逐一定位的过程写出来,照着走,大部分白屏半小时内能定出根因。

白屏的四种形态,先对号入座

排查前先分清你遇到的是哪一种,不同形态的根因分布完全不一样:

形态典型场景最可能的根因
秒白,窗口一开就是纯白开发模式也白preload 抛错 / 资源路径错
闪一下才白开发正常,打包后白asar 内资源加载失败 / 路由 base 路径
偶发白,重启又好只在部分用户机器上GPU / 驱动 / 内存不足被杀
白屏但能点、能拖界面框架没画出来GPU 合成问题 / CSS 挂 / JS 半路崩

记住一个判据:「白屏但能交互」基本不是页面没加载,是合成层出了问题,往 GPU 方向查;「纯白点什么都不灵」是渲染进程压根没起来,往 preload 和路径方向查。

主进程先挂两条日志:console-message 与 render-process-gone

白屏排查最大的困难是故障发生在渲染进程里,而你的日志只写在了主进程。渲染进程一崩,它自己那份日志跟着没了。正确做法是在主进程把渲染进程的 console 和死亡事件都接住:

// main.js
const log = require('./log'); // 你的文件日志模块

function createWindow() {
  const win = new BrowserWindow({
    width: 1280,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  // 渲染进程的 console 全部转发到主进程日志
  win.webContents.on('console-message', (e, level, message, line, sourceId) => {
    log.write(`[renderer L${level}] ${message} (${sourceId}:${line})\n`);
  });

  // 渲染进程死亡:reason 直接告诉你为什么
  win.webContents.on('render-process-gone', (e, details) => {
    log.write(`[renderer-gone] reason=${details.reason} exitCode=${details.exitCode}\n`);
  });

  // 页面资源加载失败
  win.webContents.on('did-fail-load', (e, errorCode, errorDescription, validatedURL) => {
    if (errorCode !== -3) { // -3 是被用户取消的,忽略
      log.write(`[load-fail] ${errorCode} ${errorDescription} ${validatedURL}\n`);
    }
  });

  win.loadFile(path.join(__dirname, 'dist/index.html'));
}

render-process-gone 的 details.reason 是排查白屏最有价值的一个字段,它的取值直接对应根因:

reason含义往哪查
crashed渲染进程真崩了看 console-message 崩溃前最后一条输出
oom内存超限被杀页面泄漏,查 DOM 节点数、大数组
launch-failed进程没起来preload 抛错,见下一节
integrity-failure代码签名校验失败打包配置 / asar 完整性
killed被外部杀掉安全软件 / 系统内存压力

这两条日志加上 did-fail-load,覆盖我遇到过的大部分白屏。它们必须在上线版本里就带上,等用户报白屏你本地复现不了的时候,只能靠这份日志回溯。日志写到 app.getPath('userData') 下面,别写到安装目录(只读)。

根因一:preload 抛错,渲染进程整个起不来

这是我遇到次数最多的白屏原因,而且只在打包后出现,开发模式永远正常。典型症状:窗口是白的,render-process-gone 报 launch-failed,或者干脆连窗口都不显示。

机制是:preload 在渲染进程启动最早阶段执行,它 require 的模块如果找不到、或者抛异常,整个渲染进程就起不来,页面根本没有机会渲染。开发模式不暴露是因为开发时 preload 里的相对路径恰好能解析,打包后 asar 内路径变了。三个高发写法:

定位方法:把 console-message 日志打开,白屏复现后看渲染进程死前最后一条日志,九成会指着 preload 里某一行。修复原则:preload 只做最小初始化,任何重活、任何可能失败的原生调用都不许在 preload 顶层执行,全部包成 contextBridge 暴露出去的函数,等渲染进程真正调用时再执行。这也是我在 Win32 FFI 那篇里把原生库做成懒加载的原因——不只是为了启动速度,也是为了让 preload 永远不可能成为白屏源。

根因二:contextIsolation 与 nodeIntegration 的版本差

Electron 12 起 contextIsolation 默认打开,20 起 nodeIntegration 默认关闭。如果你按老教程写代码、又在新版本运行,preload 里的 window.xxx = ... 赋值会被 contextBridge 的隔离层挡掉,渲染进程拿到 undefined,第一行业务代码就崩,白屏。

判断很简单:开发者工具的 console 里如果看到 Cannot read properties of undefined 且指向你最早上线的某个 window.api.xxx 调用,就是这个。修法不是把 contextIsolation 关回去(那是把门拆了防贼),而是 preload 改用 contextBridge.exposeInMainWorld:

// preload.js —— contextIsolation 下的正确写法
const { contextBridge } = require('electron');

contextBridge.exposeInMainWorld('api', {
  // 只暴露白名单方法,且都是懒执行
  readConfig: (name) => ipcRenderer.invoke('config:read', name),
  pickFolder: () => ipcRenderer.invoke('dialog:pickFolder'),
});

顺带一句:nodeIntegration: false 是安全底线也是排错边界——渲染进程摸不到 Node,所有「渲染进程里 require 一个包」的代码在白屏里都是先杀无赦。跨层拿不到能力的问题,基本都出在 preload 没配对。

根因三:资源路径,file:// 下相对路径全灭

打包后用 loadFile 加载,页面是 file:// 协议。这类页面里两类写法会白:

对策:构建工具把资源路径全改成相对路径且不打 leading slash(Vite 设 base: './'),路由用 hash 模式(#/settings)。file:// 协议下这两条是保命组合。判断也很直接:did-fail-load 日志里一片 -6(FILE_NOT_FOUND),就是路径问题,跟 Electron 本身没关系。

根因四:GPU,白屏但界面还能交互

如果窗口是白的,但你能拖动、能点、标题栏正常,甚至盲按快捷键有反应——这是 GPU 合成挂了,页面 DOM 其实活着。某些老旧显卡、虚拟机和远程桌面环境高发。三个档位的处理:

// 1. 先试全局关硬件加速(最省事,代价是动画略卡)
app.disableHardwareAcceleration();

// 2. 或者只关 GPU 合成
app.commandLine.appendSwitch('disable-gpu');
app.commandLine.appendSwitch('disable-gpu-compositing');

// 3. 针对性处理:白屏只发生在特定显卡时,按条件动态加开关
if (process.env.FORCE_SOFTWARE_GL === '1') {
  app.commandLine.appendSwitch('use-gl', 'swiftshader');
}

这类问题不要在主进程里猜,让用户帮一个忙就能确认:白屏时按 Ctrl+Shift+I 打开开发者工具,如果 console 有输出、Elements 树完整,100% 是 GPU 合成问题。关掉硬件加速后白屏消失,就实锤了。

根因五:asar 内资源、中文路径与权限

剩下几个零碎但都真实遇到过的:

预防清单:上线前把白屏概率压到最低

白屏排查做得再多,也不如让它少发生。这是我现在的上线前自检清单:

  1. 主进程常驻 console-message / render-process-gone / did-fail-load 三条日志,写 userData。
  2. preload 顶层零副作用,原生模块全部懒加载,启动路径上不许有可能失败的调用。
  3. 纯净环境实测:全新 Windows 虚拟机、C:\Program Files 安装、中文路径安装,各跑一遍。
  4. 构建产物 base: './' + hash 路由,禁掉 history 模式。
  5. 保留一个环境变量开关,出问题能一键切软件渲染,不用重新发版。

最后说个心态问题:白屏的复现率往往在用户机器上最高、在你机器上最低,所以日志必须比用户更早知道出事。上面第一条做好了,用户截图发你一个白窗口,你也能从他机器上的日志里告诉他为什么白。