XCel 项目总结 - Electron 与 Vue 的性能优化

poster.jpg

XCEL 是一个 Excel 数据清洗工具,其经过可视化的方式让用户轻松地对 Excel 数据进行筛选。html

XCEL 基于 Electron 和 Vue 2.0 进行开发,充分利用 Electron 多进程任务处理等功能,使其拥有高性能、跨平台(windows 7+、Mac 和 Linux)的特性。前端

落地页:https://xcel.aotu.io/ ✨✨✨
项目地址:https://github.com/o2team/xcel ✨✨✨vue

项目背景

用户研究的定量研究和轻量级数据处理中,均需对数据进行清洗处理,用以剔除异常数据,保证数据结果的信度和效度。目前因调研数据和轻量级数据的多变性,对轻量级数据清洗每每采起人工清洗,缺乏统1、标准的清洗流程,但对于调研和轻量级的数据每每是须要保证数据稳定性的,所以,在对数据进行清洗的时候最好有能够标准化的清洗方式。node

特性一览

  • 基于 Electron 研发并打包成为原生应用,用户体验良好;git

  • 可视化操做 Excel 数据,支持文件的导入导出;github

  • 拥有单列运算逻辑、多列运算逻辑和双列范围逻辑三种筛选方式,而且可经过“且”、“或”和“编组”的方式任意组合。web

思路与实现

结合用研组的需求,咱们利用 Electron 和 Vue 的特性对该工具进行开发。算法

技术选型

  • Electron:桌面端跨平台框架,为 Web 提供了原生接口的权限。打包后的程序兼容 Windows 7 及以上、Mac、Linux 的 32 / 64 位系统。详情>>npm

  • Vue 全家桶:Vue 拥有数据驱动视图的特性,适合重数据交互的应用。详情>>编程

  • js-xlsx:各类电子表格格式的解析器和生成器。纯 JavaScript 实现,适用于 Node.js 和 Web 前端。详情>>

实现思路

  1. 经过 js-xlsx 解析 Excel 文件生成 JSON 格式

  2. 根据筛选条件对 JSON 数据进行筛选过滤

  3. 将过滤后的 JSON 数据生成 js-xlsx 指定的数据结构

  4. 利用 js-xlsx 对转换后的数据生成 Excel 文件


纸上得来终觉浅,绝知此事要躬行

相关技术

若是对某项技术比较熟悉可略读/跳过。

Electron

Electron 是什么?

Electron 是一个能让你经过 JavaScript、HTML 和 CSS 构建桌面应用的框架。这些应用能打包到 Mac、Windows 和 Linux 电脑上运行,固然它们也能上架到 Mac 和 Windows 的 app stores。

  • JavaScript、HTML 和 CSS 都是 Web 语言,这就意味着它们都是组成网站的一部分,浏览器(如 Chrome)能将这些代码转为可视化图像。

  • Electron 是一个框架:Electron 对底层代码进行抽象和封装,让开发者能在此之上构建项目。

为何它如此重要?

一般来讲,桌面应用都须要用每一个操做系统对应的原生语言进行开发。这意味着须要拥有 3 个团队为这个应用编写 3 个相应的版本。Electron 则容许你经过 web 语言编写一次便可。

  • 原生(操做系统)语言:用于开发主流操做系统的应用的原生语言以下(大多数状况下):Mac 对应 Objective C、Linux 对应 C、Windows 对应 C++。

它由什么组成?

Electron 结合了 ChromiumNode.js 和用于调用操做系统本地功能的 API(如打开文件窗口、通知、图标等)。

  • Chromium:Google 创造的一个开源库,并用于 Google 的浏览器 Chrome。

  • Node.js(Node):一个用于在服务器运行 JavaScript 的运行时(runtime),它拥有文件系统和网络的权限(你的电脑也能够是一台服务器!)。

Electron 的组成

开发体验如何?

基于 Electron 的开发,就好像开发一个网页同样,并且可以无缝地 使用 Node。或者说:就好像构建一个 Node app,并经过 HTML 和 CSS 构建界面。另外,你只需为一个浏览器(最新的 Chrome)进行设计(即无需考虑兼容性)。

  • 使用内置的 Node:这还不是所有!除了 Node API,你还可使用托管在 npm 上,超过 350,000 个的模块。

  • 一个浏览器:并不是全部浏览器都提供一致的样式,所以 web 设计师和开发者时常不得不花费更多的精力去让一个网站在不一样的浏览器上看起来一致。

  • 最新的 Chrome:可以使用超过 90% 的 ES2015 特性和其它很酷的特性(如 CSS 变量)。

两个进程(重点)

Electron 有两个种进程:『主进程』和『渲染进程』。有些模块只能工做在其中一个进程上,而有些则能工做在两个进程上。主进程更多地充当幕后角色,而渲染进程则是应用的每一个窗口。
PS:可经过任务管理器(PC)/活动监视器(Mac)查看进程的相关信息。

  • 模块:Electron 的 API 是根据它们的功能进行分组。例如:dialog 模块拥有全部原生 dialog 的 API,如打开文件、保存文件和弹窗。

主进程

主进程,一般是一个命名为 main.js 的文件,该文件是每一个 Electron 应用的入口。它控制了应用的生命周期(从打开到关闭)。它能调用原生元素和建立新的(多个)渲染进程,并且整个 Node API 是内置其中的。

  • 调用原生元素:打开 diglog 和其它操做系统交互均是资源密集型操做(注:出于安全考虑,渲染进程是不能直接调用本地资源的),所以都须要在主进程完成。

主进程

渲染进程

渲染进程是应用的一个浏览器窗口。与主进程不一样,它能存在多个(注:一个 Electron 应用只能有一个主进程)而且是相互独立的。它们也能是隐藏的。它一般被命名为 index.html。它们就像典型的 HTML 文件,但在 Electron 中,它们能获取完整的 Node API 特性。所以,这也是它与其它浏览器不一样的地方。

  • 相互独立:每一个渲染进程都是独立的,这意味着就算它们某个崩溃了,也不会影响其他的渲染进程。

  • 隐藏的:你能够设置一个窗口是隐藏的,而后让它只在背后执行代码(?)。

渲染进程

把它们想象成这样

在 Chrome(或其它浏览器)中的每一个标签页(tab) 和其内的页面,就比如 Electron 中的一个单独渲染进程。若是你关闭全部标签页,Chrome 依然存在,这比如 Electron 的主进程,并且你能打开一个新的窗口或关闭这个应用。

注:通常状况下,在 Chrome 浏览器中,一个标签页(tab)中的页面(即除了浏览器自己部分,如搜索框、工具栏等)就是一个渲染进程。

把它们想象成这样

相互通信

尽管主进程和渲染进程都有各自的任务,但它们之间也有须要协同完成的任务。所以它们之间须要通信。IPC就为此而生,它提供了进程间的通信。但它只能在主进程与渲染进程之间传递信息。

  • IPC:主进程和渲染进程都有一个 IPC 模块。

此处输入图片的描述

汇成一句话

Electron 应用就像 Node 应用,它也依赖一个 package.json 文件。该文件定义了哪一个文件做为主进程,并所以让 Electron 知道从何启动你的应用。而后主进程能建立渲染进程,并能使用 IPC 让二者间进行消息传递。

汇成一句话

至此,Electron 的基础部分介绍完毕。该部分是基于我以前翻译的一篇文章《Essential Electron》,译文可点击 这里

-----

Vue 全家桶

目前,该工具应用了 Vue、Vuex、Vuex-router。在工具基本定型阶段,由 1.x 升级到了 2.0 (Vuex 暂未升级)。

为何选择 Vue

对于我来讲:

  • 简单易用,通常使用只需看官方文档。

  • 数据驱动视图,因此基本不用操做 DOM 了。

  • 框架的存在是为了帮助咱们应对复杂度。

  • 全家桶的好处是:对于通常场景,我就不须要考虑用哪些个库(插件)。

Vue 1.x -> Vue 2.0 的版本迁移用 vue-migration-helper 便可分析出大部分须要更改的地方。

网上已经有不少关于 Vue 的信息了。至此,Vue 部分介绍完毕。


js-xlsx

该库支持各类电子表格格式的解析和生成。它由纯 JavaScript 实现,适用于前端和 Node。详情>>

支持读入的格式有:

  • Excel 2007+ XML Formats (XLSX/XLSM)

  • Excel 2007+ Binary Format (XLSB)

  • Excel 2003-2004 XML Format (XML "SpreadsheetML")

  • Excel 97-2004 (XLS BIFF8)

  • Excel 5.0/95 (XLS BIFF5)

  • OpenDocument Spreadsheet (ODS)

支持写的格式有:

  • XLSX

  • CSV (and general DSV)

  • JSON and JS objects (various styles)

只要能提供读(解析)和写,剩下的就是靠 JavaScript 处理解析出来的数据(JSON)了。目前该库提供了 sheet_to_json 方法,该方法能将读入的 Excel 数据转为 JSON 格式。因为导出时须要提供特定的 JSON 格式,所以这部分须要咱们本身实现。

更多关于 Excel 在 JavaScript 中处理的知识可关注:凹凸实验室的《Node读写Excel文件探究实践》。但该文章存在两处问题(均在 js-xlsx 实战的导出表格部分):

  1. 生成头部时,Excel 的列信息简单地经过 String.fromCharCode(65+j) 生成,但列大于 26 时就会出现问题。这个问题会在后面章节中给出解决方案;

  2. 转换成 worksheet 须要的结构处,出现逻辑性错误,而且会致使严重的性能问题。逻辑问题在此不讲述,咱们讲下性能问题:
    ECMAScript 的不断更新,让 JavaScript 更增强大和易用。尽管如此,咱们仍是要作到『物尽所用』,而不要『大材小用』,不然会获得反效果。这里致使性能问题的正是 Object.assign() 方法,该方法能够把任意多个的源对象自身的可枚举属性拷贝给目标对象,而后返回目标对象。因为该方法自身的实现机制,会在这里产生大量的冗余操做。而这里的单元格信息是惟一的,因此直接经过 forEach 为一个空对象赋值便可。提高 N 倍性能的同时,也把逻辑性错误解决了。

原来的:

var result = 某数组.reduce((prev, next) => Object.assign({}, prev, {[next.position]: {v: next.v}}), {});

改成:

var result = 某数组.forEach((v, i) => data[v.position]= {v: v.v})

实践是检验真理的惟一标准
在理解上述知识的前提下,下面就谈谈一些在实践中总结出来的技巧、难点和重点

CSS、JavaScript 和 Electron 相关的知识和技巧

高亮 table 的列

Excel 单元格采用 table 展现。在 Excel 中,被选中的单元格会高亮相应的『行』和『列』,以提醒用户。在该应用中也有作相应处理,横向高亮采用 tr:hover 实现,而纵向呢?这里所采用的一个技巧是:

假设 HTML 结构以下:

div.container
  table
    tr
      td

CSS 代码以下:

.container { overflow:hidden; }
td { position: relative; }
td:hover::after { 
  position: absolute; 
  left: 0; 
  right: 0; 
  top: -1个亿px; // 小目标达成,不过是负的?
  bottom: -1个亿px; 
  z-index: -1; // 避免遮住自身和同列 td 的内容、border 等
}

斜分割线

如图:斜分割线

分割线能够经过 ::after/::before 伪类元素实现一条直线,而后经过 transform:rotate(); 旋转特定角度实现。但这种实现的一个问题是:因为宽度是不定的,所以须要经过 JavaScript 运算才能获得准确的对角分割线。

所以,这里能够经过 CSS 线性渐变 linear-gradient(to top right, transparent, transparent calc(50% - .5px), #d3d6db calc(50% - .5px), #d3d6db calc(50% + .5px), transparent calc(50% + .5px)) 实现。不管宽高如何变,依然妥妥地自适应。

Excel 的列转换

  • Excel 的列须要用『字母』表示,但不能简单地经过 String.fromCharCode() 实现,由于当超出 26列 时会产生问题(如:第 27 列,String.fromCharCode(65+26) 获得的是 [,而不是 AA)。所以,这须要经过『十进制和26进制转换』算法来实现。

// 将指定的天然数转换为26进制表示。映射关系:[0-25] -> [A-Z]。
function getCharCol(n) {
    let temCol = '',
        s = '',
        m = 0
    while (n >= 0) {
        m = n % 26 + 1
        s = String.fromCharCode(m + 64) + s
        n = (n - m) / 26
    }
    return s
}
// 将指定的26进制转换为天然数。映射关系:[A-Z] ->[0-25]。
function getNumCol(s) {
    if (!s) return 0
    let n = 0
    for (let i = s.length - 1, j = 1; i >= 0; i-- , j *= 26) {
        let c = s[i].toUpperCase()
        if (c < 'A' || c > 'Z') return 0
        n += (c.charCodeAt() - 64) * j
    }
    return n - 1
}

为 DOM 的 File 对象增长了 path 属性

Electron 为 File 对象额外增了 path 属性,该属性可获得文件在文件系统上的真实路径。所以,你能够利用 Node 随心所欲?。应用场景有:拖拽文件后,经过 Node 提供的 File API 读取文件等。

支持常见的编辑功能,如粘贴和复制

Electron 应用在 MacOS 中默认不支持『复制』『粘贴』等常见编辑功能,所以须要为 MacOS 显式地设置复制粘贴等编辑功能的菜单栏,并为此设置相应的快捷键。

// darwin 就是 MacOS
if (process.platform === 'darwin') {
    var template = [{
      label: 'FromScratch',
      submenu: [{
        label: 'Quit',
        accelerator: 'CmdOrCtrl+Q',
        click: function() { app.quit(); }
      }]
    }, {
      label: 'Edit',
      submenu: [{
        label: 'Undo',
        accelerator: 'CmdOrCtrl+Z',
        selector: 'undo:'
      }, {
        label: 'Redo',
        accelerator: 'Shift+CmdOrCtrl+Z',
        selector: 'redo:'
      }, {
        type: 'separator'
      }, {
        label: 'Cut',
        accelerator: 'CmdOrCtrl+X',
        selector: 'cut:'
      }, {
        label: 'Copy',
        accelerator: 'CmdOrCtrl+C',
        selector: 'copy:'
      }, {
        label: 'Paste',
        accelerator: 'CmdOrCtrl+V',
        selector: 'paste:'
      }, {
        label: 'Select All',
        accelerator: 'CmdOrCtrl+A',
        selector: 'selectAll:'
      }]
    }];
    var osxMenu = menu.buildFromTemplate(template);
    menu.setApplicationMenu(osxMenu);
}

更贴近原生应用

Electron 的一个缺点是:即便你的应用是一个简单的时钟,但它也不得不包含完整的基础设施(如 Chromium、Node 等)。所以,通常状况,打包后的程序至少会达到几十兆(根据系统类型进行浮动)。当你的应用越复杂,就越能够忽略这部分了。

众所周知,页面的渲染不免会致使『白屏』,并且这里采用了 Vue 框架,状况就更加糟糕了。另外,Electron 应用也避免不了『先打开浏览器,再渲染页面』的步骤。下面提供几种方法来减轻这种状况,以让程序更贴近原生应用。

  1. 指定 BrowserWindow 的背景颜色;

  2. 先隐藏窗口,直到页面加载后再显示;

  3. 保存窗口的尺寸和位置,以让程序下次被打开时,依然保留的一样大小和出如今一样的位置上。

对于第一点,若程序的背景不是纯白(#fff)的,那么可指定窗口的背景颜色与其一致,以免突变。

mainWindow = new BrowserWindow({
    title: 'XCel',
    backgroundColor: '#f5f5f5',
};

对于第二点,因为 Electron 本质是一个浏览器,须要加载非网页部分的资源。所以,咱们能够先隐藏窗口。

var mainWindow = new BrowserWindow({
    title: 'ElectronApp',
    show: false,
};

等到渲染进程开始渲染页面的那一刻,在 ready-to-show 的回调函数中显示窗口。

mainWindow.on('ready-to-show', function() {
    mainWindow.show();
    mainWindow.focus();
});

对于第三点,我并无实现,缘由以下:

  1. 用户通常是根据当时的状况对程序的尺寸和位置进行调整,即视状况而定。

  2. 以上是我我的臆测,主要是我懒?。

其实现方式,可参考《4 must-know tips for building cross platform Electron apps》

如何在渲染进程调用原生弹框?

在渲染进程中调用本来专属于主进程中的 API (如弹框)的方式有两种:

  1. IPC 通信模块:先在主进程经过 ipcMain 进行监听,而后在渲染进程经过 ipcRenderer 进行触发;

  2. remote 模块:该模块提供了一种在渲染进程(网页)和主进程之间进行进程间通信(IPC)的简便途径。

  • 对于第一种,有须要就在评论区留言;

  • 对于第二种, 在渲染进程中,运行如下代码便可:

    const remote = require('electron').remote
    
    remote.dialog.showMessageBox({
        type: 'question',
        buttons: ['不告诉你', '没有梦想'],
        defaultId: 0,
        title: 'XCel',
        message: '你的梦想是什么?'
    }

自动更新

若是 Electron 应用没有了自动更新的功能,那么意味着用户想体验你新开发的功能或用上修复 Bug 后的新版本,只能靠本身主动地去官网下载,这无疑是糟糕的体验。Electron 提供的 autoUpdater 模块可实现自动更新功能,该模块提供了第三方框架 Squirrel 的接口,但 Electron 目前只内置了 Squirrel.Mac,且它与 Squirrel.Windows(须要额外引入)的处理方式也不一致(在客户端与服务器端两方面),所以若是刚接触该模块,会发现处理起来相对比较繁琐。具体能够参考个人一篇译文《Electron 自动更新的完整教程(Windows 和 OSX)》

目前 Electron 的 autoUpdater 模块不支持 Linux 系统。

另外,XCel 目前并无采用 autoUpdater 模块实现自动更新功能,而是利用 Electron 的 DownloadItem 模块实现。而服务器端则采用 Nuts

为 Electron 应用生成 Windows 安装包

经过 electron-builder 便可直接生成常见的 MacOS 安装包,但它生成的 Windows 的安装包却略显简洁。

常见的MacOS 安装包
Mac 常见的安装模式,将“左侧的应用图标”拖拽到“右侧的 Applications”便可

经过 electron-builder 生成的 Windows 安装包与咱们在 Windows 上常见的软件安装界面不太同样,它没有安装向导和点击“下一步”的按钮,只有一个安装时的 gif 动画(默认的 gif 动画以下图,固然你也能够指定特定的 gif 动画),所以也就没有让用户选择安装路径等权利。

Windows 安装时默认的动画
Windows 安装时 默认显示的 gif 动画

若是你想为打包后的 Electron 应用(即经过 electron-packager/electron-builder 生成的 、可直接运行的程序目录)生成须要点击“下一步”和可以让用户指定安装路径的常见安装包,能够经过 NSIS 程序,具体可看这篇教程 《[教學]只要10分鐘學會使用 NSIS 包裝您的桌面軟體–安裝程式打包。彻底免費。》

NSIS(Nullsoft Scriptable Install System)是一个开源的 Windows 系统下安装程序制做程序。它提供了安装、卸载、系统设置、文件解压缩等功能。这如其名字所指出的那样,NSIS 是经过它的脚本语言来描述安装程序的行为和逻辑的。NSIS 的脚本语言和一般的编程语言有相似的结构和语法,但它是为安装程序这类应用所设计的。

至此,CSS、JavaScript 和 Electron 相关的知识和技巧 部分阐述完毕。


性能优化

下面谈谈『性能优化』,这部分涉及到运行效率内存占用量
注:如下内容均基于 Excel 样例文件(数据量为:1913 行 x 180 列)得出的结论。

执行效率和渲染的优化

Vue 性能真的好?

Vue 一直标榜着本身性能优异,但当数据量上升到必定量级时(如 1913 x 180 ≈ 34 万个数据单元),会出现严重的性能问题(不作相应优化的前提下)。

如直接经过列表渲染 v-for 渲染数据时,会致使程序卡死。
答:经过查阅相关资料可得(猜想), v-for 是经过一条条数据在构建后插入 DOM 的,这对于数据量较大时,无疑会形成严重的性能问题。

当时,我想到了两种解决思路:

  1. Vue 是数据驱动视图的,对数据分段 push,即将一个庞大的任务分割为 N 份。

  2. 本身拼接 HTML 字符串,再经过 innerHTML 一次性插入。

最终,我选择了第二条,理由是:

  1. 性能最佳,由于每次执行数据过滤时,Vue 都要进行 diff,性能不佳。

  2. 更符合当前应用的需求:纯展现且无需动画过渡等。

  3. 实现更简单

将本来繁重的 DOM 操做转移到了 JavaScript 的拼接字符串后,性能获得了很大提高(不会致使程序卡死而渲染不出视图)。这种实现原理难道不就是 Vue、React 等框架解决的问题之一吗?只不过框架考虑的场景更广,有些地方须要咱们本身根据实际状况进行优化而已。

在浏览器当中,JavaScript 的运算在现代的引擎中很是快,但 DOM 自己是很是缓慢的东西。当你调用原生 DOM API 的时候,浏览器须要在 JavaScript 引擎的语境下去接触原生的 DOM 的实现,这个过程有至关的性能损耗。因此,本质的考量是,要把耗费时间的操做尽可能放在纯粹的计算中去作,保证最后计算出来的须要实际接触真实 DOM 的操做是最少的。 —— 《Vue 2.0——渐进式前端解决方案》

固然,因为 JavaScript 天生单线程,即便执行数速度再快,也会致使页面有短暂的时间拒绝用户的输入。此处可经过 Web Worker 或其它方式解决,这也将是咱们后续讲到的问题。

也有网友提供了优化大量列表的方法:https://clusterize.js.org/。 但在这里我并无采用此方式。

强大的 GPU 加速

插入 DOM 后,又会出现了另一个问题:滚动会很卡。猜测这是渲染问题,毕竟 34 万个单元格同时存在于界面中。

添加 transform: translate3d(0, 0, 0) / translateZ(0) 属性启动 GPU 渲染,便可解决这个渲染性能问题。再次感叹该属性的强大。?

后来,考虑到用户并不须要查看所有数据,只需展现部分数据让用户进行参考便可。咱们对此只渲染前 30/50 行数据。这样便可提高用户体验,也能进一步优化性能(又是纯属臆测)。

记得关闭 Vuex 的严格模式

另外,因为本身学艺不精和粗枝大叶,忘记在生产环境关闭 Vuex 的『严格模式』。
Vuex 的严格模式要在生产中关闭,不然会对 state 树进行一个深观察 (deep watch),产生没必要要的性能损耗。也许在数据量少时,不会注意到这个问题。

我当时的状况是:导入 Excel 数据后,再进行交互(涉及 Vuex 的读写操做),则须要等几秒才会响应,而直接经过纯 DOM 监听的事件则无此问题。由此,判断出是 Vuex 问题。

const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

多进程!!!

前面说道,JavaScript 天生单线程,即便再快,对于须要处理数据量较大的状况,也会出现拒绝响应的问题。所以须要 Web Worker 或相似的方案去解决。

在这里我不选择 Web worker 的缘由有以下几点:

  1. 有其它更好的替代方案:一个主进程能建立多个渲染进程,经过 IPC 便可进行数据交互;

  2. Electron 不支持 Web Worker!

Electron 做者在 2014.11.7 在《state of web worker support?》 issue 中回复了如下这一段:

Node integration doesn't work in web workers, and there is no plan to do. Workers in Chromium are implemented by starting a new thread, and Node is not thread safe. Back in past we had tried to add node integration to web workers in Atom, but it crashed too easily so we gave up on it.

所以,咱们最终采用了建立一个新的渲染进程 background process 进行处理数据。由 Electron 章节可知,每一个 Electron 渲染进程是独立的,所以它们不会互相影响。但这也带来了一个问题:它们不能相互通信?

错!下面有 3 种方式进行通信:

  1. Storage API:对某个标签页的 localStorage/sessionStorage 对象进行增删改时,其余标签页能经过 window.storage 事件监听到。

  2. IndexedDB:IndexedDB 是一个为了可以在客户端存储可观数量的结构化数据,而且在这些数据上使用索引进行高性能检索的 API。

  3. 经过主进程做为中转站:设主界面的渲染进程是 A,background process 是 B,那么 A 先将 Excel 数据传递到主进程,而后主进程再转发到 B。B 处理完后再原路返回,具体以下图。固然,也能够将数据存储在主进程中,而后在多个渲染进程中使用 remote 模块来访问它。

该工具采用了第三种方式的第一种状况:
Multiprocessing

一、主页面渲染进程 A 的代码以下:

//①
ipcRenderer.send('filter-start', {
    filterTagList: this.filterTagList,
    filterWay: this.filterWay,
    curActiveSheetName: this.activeSheet.name
})

// ⑥ 在某处接收 filter-response 事件
ipcRenderer.on("filter-response", (arg) => {
    // 获得处理数据
})

二、做为中转站的主进程的代码以下:

//②
ipcMain.on("filter-start", (event, arg) => {
    // webContents 用于渲染和控制 web page
    backgroundWindow.webContents.send("filter-start", arg)
})

// ⑤ 用于接收返回事件
ipcMain.on("filter-response", (event, arg) => {
    mainWindow.webContents.send("filter-response", arg)
})

三、处理繁重数据的 background process 渲染进程 B 的代码以下:

// ③
ipcRenderer.on('filter-start', (event, arg) => {
    // 进行运算
    ... 
    
    // ④ 运算完毕后,再经过 IPC 原路返回。主进程和渲染进程 A 也要创建相应的监听事件
    ipcRenderer.send('filter-response', {
        filRow: tempFilRow
    })
})

至此,咱们将『读取文件』、『过滤数据』和『导出文件』三大耗时的数据操做均转移到了 background process 中处理。

这里,咱们只建立了一个 background process,若是想要作得更极致,咱们能够新建『CPU 线程数- 1 』 个的 background process 同时对数据进行处理,而后在主进程对处理后数据进行拼接,最后再将拼接后的数据返回到主页面的渲染进程。这样就能够充分榨干 CPU 了。固然,在此我不会进行这个优化。

不要为了优化而优化,不然得不偿失。 —— 某网友

内存占有量过大

解决了执行效率和渲染的问题,发现也存在内存占用量过大的问题。当时猜想是如下几个缘由:

  1. 三大耗时操做均放置在 background process 处理。在通信传递数据的过程当中,因为不是共享内存(由于 IPC 是基于 Socket 的),致使出现多份数据副本(在写该篇文章时才有了这相对确切的答案)。

  2. Vuex 是以一个全局单例的模式进行管理,但它会是否是对数据作了某些封装,而致使性能的损耗呢?

  3. 因为 JavaScript 目前不具备主动回收资源的能力,因此只能主动对闲置对象设置为 null,而后等待 GC 回收。

因为 Chromium 采用多进程架构,所以会涉及到进程间通讯问题。Browser 进程在启动 Render 进程的过程当中会创建一个以 UNIX Socket 为基础的 IPC 通道。有了 IPC 通道以后,接下来 Browser 进程与 Render 进程就以消息的形式进行通讯。咱们将这种消息称为 IPC 消息,以区别于线程消息循环中的消息。

——《Chromium的IPC消息发送、接收和分发机制分析》

定义:为了易于理解,如下『Excel 数据』均指 Excel 的所有有效单元格转为 JSON 格式后的数据。

最容易处理的无疑是第三点,手动将再也不须要的变量及时设置为 null。但这效果并不明显。

后来,经过系统的『活动监视器』对该工具的每阶段(打开时、导入文件时、筛选时和导出时)进行粗略的内存分析,获得如下报告(以前分析的、未做修改):

---------------- S:报告分割线 ----------------
经观察,主要耗内存的是页面进程。下面经过截图说明:
PID 15243 是主进程
PID 15246 是页面渲染进程
PID 15248 是 background 渲染进程

a、首次启动程序时(第 4 行是主进程;第 1 行是页面渲染进程;第 3 行是 background 渲染进程 )

启动程序时

b、导入文件(第 5 行是主进程;第 2 行是页面渲染进程;第 4 行是 background 渲染进程 )
导入文件时

c、筛选数据(第 4 行是主进程;第 1 行是页面渲染进程;第 3 行是 background 渲染进程 )
筛选数据时

因为 JS 目前不具备主动回收资源的功能,因此只能主动将对象设置为 null,而后等待 GC 回收。

所以,通过一段时间等待后,内存占用以下:
d、一段时间后(第 4 行是主进程;第 1 行是页面渲染进程;第 3 行是 background 渲染进程 )
一段时间后

由上述可得,页面渲染进程因为页面元素和 Vue 等 UI 相关资源是固定的,占用内存较大且不能回收。主进程占用资源也不能获得很好释放,暂时不知道缘由,而 background 渲染进程则较好地释放资源。

---------------- E:报告分割线 ----------------

根据报告,初步得出的结论是 Vue 和通信时占用资源较大。

根据该工具的实际应用场景:因为 Excel 数据只在『导入』和『过滤后』两个阶段须要展现,并且展现的只是经过 JavaScript 拼接的 HTML 字符串构成的 DOM 而已。所以将表格数据放置在 Vuex 中,有点滥用资源的嫌疑。

另外,在 background process 中也有存有一份 Excel 数据副本。所以,索性只在 background process 存储一份 Excel 数据,而后每当数据变化时,经过 IPC 让 background process 返回拼接好的 HTML 字符串便可。这样一来,内存占有量马上降低许多。并且这也是一个一举多得的优化:

  1. 字符串拼接操做也转移到了 background process,页面的渲染进程进一步减小耗时的操做;

  2. 内存占有量大大减少,响应速度也获得了提高。

其实,这也有点像 Vuex 的『全局单例模式管理』,一份数据就好。

固然,对于 Excel 的基本信息,如行列数、SheetName、标题组等均依然保存在 Vuex。

优化后的内存占有量以下图。与上述报告的第三张图相比(同一阶段),内存占有量降低了 44.419%:
优化后内存占有量
另外,对于不须要响应的数据,可经过 Object.freeze() 冻结起来。这也是一种优化手段。但该工具目前并无应用到。

至此,优化部分也阐述完毕了!


该工具目前是开源的,欢迎你们使用或推荐给用研组等有须要的人。

大家的反馈(可提交 issues / pull request)能让这个工具在使用和功能上不断完善。

最后,感谢 LV 在产品规划、界面设计和优化上的强力支持。全文完!

感谢您的阅读,本文由 Jc 原创提供。如若转载,请注明出处:凹凸实验室(https://aotu.io/notes/2016/11...

相关文章
相关标签/搜索