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...

相關文章
相關標籤/搜索