Electron 窗口池管理实践指南

本文首发同名微信公众号:前端徐徐 

大家好,我是徐徐,今天我们聊聊如何在 Electron 中优雅得进行窗口管理。

前言

在 Electron 应用中,我们经常会遇到新窗口打开显示缓慢的问题,大多数解决方案是先把要打开的窗口先隐藏起来,然后在需要打开的时候再展示出来,不得不说这个是一个简单高效的方法。但是这样会面临两个问题,第一个问题是如果用户不打开新窗口,那么就会有多余的进程消耗 CPU 和 内存;第二个问题是窗口只有显示和隐藏,所有隐藏窗口的状态不会按需得到初始化和销毁(当然可以通过事件监听或者主进程发送消息的方式,这样负担有点重),基于上面的场景和问题便有了窗口池的概念。

窗口池原理

窗口池的原理的确与线程池和数据库连接池类似,都是通过预先创建一定数量的资源来提升系统的响应速度和资源利用率。具体来说,窗口池的工作流程如下:

  1. 初始化窗口池
    • 在应用程序启动时,预先创建 n 个隐藏的窗口,这些窗口加载一个空白页面。如果使用的是 Vue 或 React,可以在这些窗口中提前初始化实例,甚至渲染自定义的窗口标题栏,只是不渲染内容区域。
    • 这些窗口被隐藏,并且保存在一个池子中,等待被使用。
  1. 请求窗口
    • 当用户需要一个窗口时,程序从窗口池中取出一个备用窗口。
    • 将该窗口的内容区域路由到用户指定的页面,并显示该窗口。
    • 由于窗口已经初始化,内容区域的路由跳转非常快,一般不会超过0.5秒。
  1. 释放窗口
    • 当用户关闭窗口时,窗口实例被释放。
    • 程序监听窗口的关闭事件,一旦检测到一个窗口被释放,就马上创建一个新的隐藏窗口补充到窗口池中,以确保窗口池中始终有 n 个备用窗口。

先看效果

我们分别用两种方式打开同一个窗口,每隔 1s 打开一次,连续打开10次。分别看看使用窗口池和不使用窗口池的时间差。

  • 使用窗口池
const openNewWindow = () => {
    let count = 0;
    const openWindows = () => {
      if (count < 10) {
        Log4.info("openNewWindow");
        window.electronAPI.openNewWindow("/testWindow");
        count += 1;
        setTimeout(openWindows, 1000);
      }
    };
    openWindows();
  };
ipcMain.handle('openNewWindow', (event: IpcMainInvokeEvent, url: string) => {
    openWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: join(__dirname, "../preload/index.cjs"),
      },
      url,
      brandNew: true,
    });
  })

平均时间为 13 ms

  • 不使用窗口池
const openNewWindowByDefaultHandle = () => {
    let count = 0;
    const openWindows = () => {
      if (count < 10) {
        Log4.info("openNewWindowByDefaultHandle");
        window.electronAPI.openNewWindowByDefaultHandle("/testWindow");
        count += 1;
        setTimeout(openWindows, 1000);
      }
    };
    openWindows();
  };
ipcMain.handle('openNewWindowByDefaultHandle', (event: IpcMainInvokeEvent, url: string) => {
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        preload: join(__dirname, "../preload/index.cjs"),
      },
    });
    const newUrl = getOpenUrl(url)
    win.loadURL(newUrl);
  })

平均时间为 218.4 ms

核心实现

import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
import { join,resolve } from 'path';

export type WindowPoolOptions = BrowserWindowConstructorOptions & {
  url: string,
  brandNew?: boolean,
}

export const getOpenUrl = (url:string) => {
  const baseUrl =
    import.meta.env.MODE === "dev"
    ? import.meta.env.VITE_DEV_SERVER_URL
      : `file://${resolve(__dirname, "../render/index.html")}`;
  const newUrl = url.startsWith('http') || url.startsWith('https')? url : `${baseUrl}#${url}`;
  return newUrl;
}

class WindowPoolManager {
  private static instance: WindowPoolManager;
  private windowPoolSize: number;
  private windowPools: Map<string, BrowserWindow>;

  private constructor(poolSize: number) {
    this.windowPoolSize = poolSize;
    this.windowPools = new Map<string, BrowserWindow>();
    this.initPool();
  }

  public static getInstance(poolSize: number = 2): WindowPoolManager {
    if (!WindowPoolManager.instance) {
      WindowPoolManager.instance = new WindowPoolManager(poolSize);
    }
    return WindowPoolManager.instance;
  }

  private createPoolWindow(windowOptions: WindowPoolOptions): { id: string, win: BrowserWindow } {
    const win = new BrowserWindow({ ...windowOptions, show: false });
    const idData = windowOptions.url;
    const newUrl = getOpenUrl(windowOptions.url);
    win.loadURL(newUrl);
    this.windowPools.set(idData, win);
    return {
      id: idData,
      win,
    };
  }

  private initPool() {
    for (let i = 0; i < this.windowPoolSize; i++) {
      this.createPoolWindow({
        width: 800,
        height: 600,
        show: false,
        webPreferences: {
          preload: join(__dirname, '../preload/index.cjs'),
        },
        url: `/preWindow/${i}`, // 使用占位符 URL
      });
    }
  }

  private usePreWindow(): { id: string, win: BrowserWindow } | null {
    const preWindowKey = Array.from(this.windowPools.keys()).find((key) => key.startsWith('/preWindow') && this.windowPools.get(key)?.isVisible() === false);
    if (preWindowKey) {
      const preWindow = this.windowPools.get(preWindowKey);
      if (preWindow) {
        this.windowPools.delete(preWindowKey);
        return {
          id: preWindowKey,
          win: preWindow,
        };
      }
    }
    return null;
  }

  public openWindow(windowOptions: WindowPoolOptions): BrowserWindow {
    let windowEntry;
    const idData = windowOptions.url;
    if (idData && this.windowPools.has(idData) && !windowOptions.brandNew) {
      const win = this.windowPools.get(idData);
      if (win) {
        win.show();
        win.moveTop();
        win.focus();
        return win;
      }
    } else {
      windowEntry = this.usePreWindow();
    }

    if (!windowEntry) {
      windowEntry = this.createPoolWindow(windowOptions);
    }

    const { win } = windowEntry;
    win.show();
    win.moveTop();
    win.focus();
    win.loadURL(windowOptions.url);

    this.windowPools.set(idData, win);

    win.on('closed', () => {
      this.windowPools.delete(idData);
      this.ensurePreWindowPool();
    })

    this.ensurePreWindowPool();

    return win;
  }

  private ensurePreWindowPool() {
    const currentPreWindowCount = Array.from(this.windowPools.keys()).filter((key) => key.startsWith('/preWindow')).length;
    if (currentPreWindowCount < this.windowPoolSize) {
      this.createPoolWindow({
        width: 800,
        height: 600,
        show: false,
        webPreferences: {
          preload: join(__dirname, '../preload/index.cjs'),
        },
        url: `/preWindow/${currentPreWindowCount + 1}`,
      });
    }
  }
}

export default WindowPoolManager;

整段代码实现了一个单例模式的WindowPoolManager类,目的是通过维护一个窗口池来管理 Electron 应用中的窗口,从而优化性能。该类在初始化时根据指定的池大小创建一定数量的预置窗口,这些窗口初始状态为不可见。通过这种方式,可以在需要新窗口时首先尝试复用这些预置窗口,避免频繁的创建和销毁窗口。

WindowPoolManager类提供了多个关键方法,包括initPool用于初始化窗口池、createPoolWindow用于创建新窗口、usePreWindow用于查找并复用预置窗口、openWindow用于打开新的或复用已有的窗口,以及ensurePreWindowPool用于确保池中始终有足够数量的预置窗口。在打开窗口时,如果有符合条件的预置窗口,则会重用该窗口,否则创建新的窗口并将其添加到池中。

整个设计有效地提升了窗口管理的效率和性能,特别适用于需要频繁创建和销毁窗口的场景。

结语

通过窗口池的设计,的确显著提高了 Electron 应用中窗口管理的效率和性能。在实际应用中,尤其是在需要频繁创建和销毁窗口的场景下,采用窗口池可以有效减少窗口的初始化时间,避免了每次打开新窗口时都需要重新加载和渲染内容的开销。与传统的每次创建新窗口的方法相比,窗口池方法不仅能够加速窗口的展示,还能更好地利用系统资源,减少 CPU 和内存的浪费。

通过本文的实现和验证,我们可以看到使用窗口池后,打开窗口的时间显著缩短,尤其在连续操作时,时间差更为明显。无论是桌面应用还是 Electron 的后台管理系统,窗口池模式都是一种非常值得应用的优化策略,可以极大提升用户体验,特别是在需要高频次窗口切换的场景中。

希望这篇文章能够帮助到想要优化 Electron 窗口加载优化的朋友。

源码

https://github.com/Xutaotaotao/electron-proplay/tree/feature-window

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值