嗨,我是Takuya,一个独立开发者,正在构建一个名为Inkdrop的 Markdown 笔记应用程序。

该应用程序建立在Electron之上,该框架允许您构建基于 NodeJS 和 Chromium(浏览器)的跨平台桌面应用程序。

它基本上是一个很棒的框架,因为您无需学习原生框架或语言,而是使用 JavaScript、HTML 和 CSS 即可构建桌面应用程序。如果您是 Web 开发人员,您可以快速构建桌面应用程序。

另一方面,人们经常提到 Electron 的缺点——应用程序启动时间往往很慢。

我的应用程序也遇到了这个问题,因为我收到了一些用户对启动速度慢的抱怨。

是啊,慢启动太有压力了。

但我非常高兴我完成了解决它。

在我的 Mac 上,该应用程序的 TTI(交互时间)已从 4 秒提高到 3 秒。

我会说“快 1,000 毫秒”而不是“快 1 秒”,因为这是一项重大改进,我为此付出了很大努力!

请看以下比较截屏视频:

[启动速度对比](https://res.cloudinary.com/practicaldev/image/fetch/s--SLNalaGU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to -uploads.s3.amazonaws.com/i/j1okykcymr9h7oc64jp2.gif)

你可以感觉到它比以前的版本快很多。

正如您在上面看到的,应用程序主窗口显示得更快一些,并且在浏览器窗口中加载应用程序包也很快完成。

它目前处于测试阶段,用户告诉我他们对提高的启动速度感到满意。

我等不及要正式推出了。

我想有很多开发人员都在努力解决同样的问题,所以我想分享一下我是如何做到的。

让我们提升您的 Electron 应用程序!

TL;DR

  • 加载 JavaScript 太慢

  • 在需要之前不要拨打require()(提高 300 毫秒)

  • 使用 V8 快照(700ms 改进)

加载JavaScript太慢

那么,为什么 Electron 应用程序往往启动缓慢?

应用启动的最大瓶颈显然是加载 JavaScript 的过程。

您可以在开发人员工具的性能分析器中检查您的应用程序包是如何加载的。

  • 另请参阅:开始分析运行时性能 | Chrome 开发工具

按 Cmd-E 或红点记录按钮开始捕获运行时性能,然后重新加载应用程序。

你会看到一个类似这样的时间线:

[性能分析](https://res.cloudinary.com/practicaldev/image/fetch/s--M-YD_k2L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/0carwgb6isilf0s4ml44.png)

您应该看到要求模块在时间线上花费了很长时间。

需要多长时间取决于您的应用程序所依赖的模块/库的数量。

就我而言,我的应用程序具有大量依赖项,以提供其插件功能、可扩展的降价编辑器和渲染器等。

为了启动速度,似乎很难删除这些依赖项。

如果您有一个新项目,则必须仔细选择性能库。

更少的依赖总是更好。

在需要之前不要拨打require()

为了避免过长的加载时间,您可以做的第一件事是将调用require()为您的依赖项推迟到必要时。

我的应用程序主窗口现在显示速度比旧版本快一点。

那是因为它在启动时在主进程中加载了jsdom

我添加它来解析 HTML,但发现它是一个巨大的库,需要数百毫秒才能加载。

有几种方法可以解决此类问题。

1\。使用更轻的替代品

如果您发现它的加载很重,您可以使用一个小的替代库(如果存在)。

原来我不需要jsdom来解析 HTML,因为 Web API 中有DOMParser。你可以像这样解析 HTML:

const dom = new DOMParser().parseFromString(html, 'text/html')

进入全屏模式 退出全屏模式

2\。避免对评估时间的要求

而不是要求库评估您的代码:

import { JSDOM } from 'jsdom'

export function parseHTML(html) {
  const dom = new JSDOM(html);
  // ...
}

进入全屏模式 退出全屏模式

推迟需要它,直到您真正需要该库:

var jsdom = null

function get_jsdom() {
  if (jsdom === null) {
    jsdom = require('jsdom')
  }
  return jsdom
}

export function parseHTML(html) {
  const { JSDOM } = get_jsdom()
  const dom = new JSDOM(html);
  // ...
}

进入全屏模式 退出全屏模式

它会在不放弃依赖关系的情况下改善您的启动时间。

请注意,如果您使用像 Webpack 这样的模块捆绑器,则必须从应用程序包中排除这些依赖项。

使用V8快照

现在我的应用程序启动速度快了 200-300 毫秒,但在渲染器过程中仍然加载缓慢。

大多数依赖项不能推迟到需要,因为它们会立即使用。

Chromium 必须读取和评估您的 JS 和模块,这需要比您想象的更长的时间,即使是从本地文件系统(在我的应用程序中为 1-2 秒)。

大多数本机应用程序不需要这样做,因为它们已经是二进制代码,您的操作系统可以在不翻译成机器语言的情况下运行它们。

Chromium 的 JavaScript 引擎是 v8。

v8 中有一种技术可以加快速度:V8 快照。

V8 快照允许 Electron 应用程序执行一些任意 JavaScript 代码并输出包含序列化堆的二进制文件,其中包含在提供的脚本结束时运行 GC 后留在内存中的所有数据。

Atom Editor3 年前利用了 V8 快照并改进了启动时间:

  • 提高启动时间 |原子博客

Atom 团队成功地将他们机器上的启动时间提高了大约 500 毫秒。

看起来很有希望。

V8 快照的工作原理

让我直截了当——它对我的应用程序也很有效。

例如,加载remark-parse已大幅缩减。

没有 v8 快照:

[在没有 v8 快照的情况下加载注释解析](https://res.cloudinary.com/practicaldev/image/fetch/s--RMOjD5D_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https:/ /dev-to-uploads.s3.amazonaws.com/i/58m7lkfoen4k77uh1yue.png)

使用 v8 快照:

[加载注释解析并启用 v8 快照](https://res.cloudinary.com/practicaldev/image/fetch/s--B0bNyNWN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https: //dev-to-uploads.s3.amazonaws.com/i/hcz49yb502e7hkp5ivv3.png)

凉爽的!!!

我可以从以下方面改善评估browser-main.js的加载时间:

[在没有 v8 快照的情况下加载 browser-main.js](https://res.cloudinary.com/practicaldev/image/fetch/s--SpU1L0wM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https ://dev-to-uploads.s3.amazonaws.com/i/xfnqga6trzcjpo8aakel.png)

至:

[使用 v8 快照加载 browser-main.js](https://res.cloudinary.com/practicaldev/image/fetch/s--VAyPLXK5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https ://dev-to-uploads.s3.amazonaws.com/i/zffli0yapamv4kcmbmw0.png)

这是加载首选项窗口的截屏视频,展示了 v8 快照在多大程度上提高了应用程序包的加载速度:

[首选项窗口](https://res.cloudinary.com/practicaldev/image/fetch/s--GPikp2eY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/i/ifcm6w6mut3d7hwfaebt.gif)

但是如何从 V8 快照加载模块呢?

在具有自定义 V8 快照的 Electron 应用程序中,您会在全局范围内获得snapshotResult变量。

它包含已预先执行的 JavaScript 的预加载缓存数据,如下所示:

[snapshotResult](https://res.cloudinary.com/practicaldev/image/fetch/s--wWOQEVz8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads .s3.amazonaws.com/i/ygjr48x8uh82w1onsndk.png)

您无需拨打require()即可使用这些模块。

这就是 V8 快照运行速度非常快的原因。

在下一节中,我将解释如何创建您的自定义 V8 快照。

如何创建自定义V8快照

您必须执行以下步骤:

1.安装工具

  1. 预处理 JavaScript 源文件electron-link

  2. 使用mksnapshot创建 v8 快照

  3. 在 Electron 中加载快照

我为本教程创建了一个简单的示例项目。在这里查看我的存储库:

  • inkdropapp/electron-v8snapshots-example:在 Electron 应用程序中使用自定义 v8 快照的示例

安装工具

需要以下软件包:

包裹

描述

电子

运行

电子链接

预处理 JavaScript 源文件

电子-mksnapshot

下载mksnapshot二进制文件

mksnapshot是一个工具,用于从您的预处理 JavaScript 文件中创建 V8 快照,其中包含electron-link

electron-mksnapshot帮助为 Electron 下载兼容的mksnapshot二进制文件。

但是如果您使用的是旧版本的 Electron,则必须将ELECTRON_CUSTOM_VERSION环境变量设置为您的 Electron 版本:

# Install mksnapshot for Electron v8.3.0
ELECTRON_CUSTOM_VERSION=8.3.0 npm install

进入全屏模式 退出全屏模式

下载二进制文件需要很长时间。您可以通过设置ELECTRON_MIRROR个环境变量来使用 Electron 镜像,如下所示:

# Electron mirror for China
ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"

进入全屏模式 退出全屏模式

electron-link预处理 JavaScript 源文件

electron-link可帮助您生成可以快照的 JavaScript 文件。

为什么你需要它是你不能在 V8 上下文中require一些模块,如 NodeJS 内置模块和本机模块。

如果你有一个简单的应用程序,你可以通过你的应用程序的入口点。

就我而言,我的应用程序过于复杂,无法生成可快照的文件。

所以,我决定创建另一个 JS 文件来生成快照,它只需要一些库,如下所示:

// snapshot.js
require('react')
require('react-dom')
// ...require more libraries

进入全屏模式 退出全屏模式

然后,将其保存为snapshot.js在您的项目目录中。

创建以下脚本,将 JS 文件传递到electron-link:

const vm = require('vm')
const path = require('path')
const fs = require('fs')
const electronLink = require('electron-link')

const excludedModules = {}

async function main () {
  const baseDirPath = path.resolve(__dirname, '..')

  console.log('Creating a linked script..')
  const result = await electronLink({
    baseDirPath: baseDirPath,
    mainPath: `${baseDirPath}/snapshot.js`,
    cachePath: `${baseDirPath}/cache`,
    shouldExcludeModule: (modulePath) => excludedModules.hasOwnProperty(modulePath)
  })

  const snapshotScriptPath = `${baseDirPath}/cache/snapshot.js`
  fs.writeFileSync(snapshotScriptPath, result.snapshotScript)

  // Verify if we will be able to use this in `mksnapshot`
  vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true})
}

main().catch(err => console.error(err))

进入全屏模式 退出全屏模式

它将一个可快照的脚本输出到<PROJECT_PATH>/cache/snapshot.js

这个从electron-link派生的 JS 文件直接包含库,就像 webpack 生成的包一样。

在输出中,禁用模块(即path)被推迟到需要,以便它们不会在 v8 上下文中加载(有关详细信息,请参阅电子链接的文档。

使用mksnapshot创建 v8 快照

现在我们有了一个可快照的脚本来生成 V8 快照。

运行以下脚本来执行此操作:

const outputBlobPath = baseDirPath
console.log(`Generating startup blob in "${outputBlobPath}"`)
childProcess.execFileSync(
  path.resolve(
    __dirname,
    '..',
    'node_modules',
    '.bin',
    'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')
  ),
  [snapshotScriptPath, '--output_dir', outputBlobPath]
)

进入全屏模式 退出全屏模式

在示例存储库](https://github.com/inkdropapp/electron-v8snapshots-example/blob/master/tools/create-v8-snapshots.js)中查看整个脚本[。

最后,您将在项目目录中获得v8_context_snapshot.bin文件。

在 Electron 中加载快照

让我们在您的 Electron 应用程序中加载您的 V8 快照。

Electron 在其二进制文件中有一个默认的 V8 快照文件。

你必须用你的覆盖它。

这是 Electron 中 V8 快照的路径:

  • macOS:node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/

  • Windows/Linux:node_modules/electron/dist/

您可以将您的v8_context_snapshot.bin复制到那里。

这里是复制文件的脚本。

然后,启动您的应用程序,您应该在全局上下文中获得snapshotResult变量。

在控制台中输入snapshotResult以检查它是否存在。

现在,您已经在 Electron 应用程序中加载了自定义快照。

如何从它们加载依赖库?

您必须覆盖默认的require函数,如下所示:

const path = require('path')

console.log('snapshotResult:', snapshotResult)
if (typeof snapshotResult !== 'undefined') {
  console.log('snapshotResult available!', snapshotResult)

  const Module = require('module')
  const entryPointDirPath = path.resolve(
    global.require.resolve('react'),
    '..',
    '..',
    '..'
  )
  console.log('entryPointDirPath:', entryPointDirPath)

  Module.prototype.require = function (module) {
    const absoluteFilePath = Module._resolveFilename(module, this, false)
    let relativeFilePath = path.relative(entryPointDirPath, absoluteFilePath)
    if (!relativeFilePath.startsWith('./')) {
      relativeFilePath = `./${relativeFilePath}`
    }
    if (process.platform === 'win32') {
      relativeFilePath = relativeFilePath.replace(/\\/g, '/')
    }
    let cachedModule = snapshotResult.customRequire.cache[relativeFilePath]
    if (snapshotResult.customRequire.cache[relativeFilePath]) {
      console.log('Snapshot cache hit:', relativeFilePath)
    }
    if (!cachedModule) {
      console.log('Uncached module:', module, relativeFilePath)
      cachedModule = { exports: Module._load(module, this, false) }
      snapshotResult.customRequire.cache[relativeFilePath] = cachedModule
    }
    return cachedModule.exports
  }

  snapshotResult.setGlobals(
    global,
    process,
    window,
    document,
    console,
    global.require
  )
}

进入全屏模式 退出全屏模式

请注意,您必须在加载库之前运行它。

如果工作正常,您应该会在开发人员控制台中看到类似“快照缓存命中:反应”的输出。

在示例项目中,您应该会看到类似以下的结果:

[示例结果](https://res.cloudinary.com/practicaldev/image/fetch/s--6tpR3LlB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/i/jk6dg7io4s2ij97qyma7.png)

恭喜!您已经从 V8 快照加载了应用程序的依赖项。

急切地构建您的应用程序实例

不仅从缓存中加载依赖项,您还可以使用快照来构建您的应用程序实例,例如Atom 执行。

一些应用程序构建任务将是静态的并且可以快照,即使读取用户配置等其他任务是动态的。

通过使用快照预先执行这些初始化任务,可以进一步提高启动速度。

但这取决于您的代码库。

例如,您可以在快照中预先构建 React 组件。


而已!希望对您的应用开发有所帮助。谢谢您阅读此篇。

我正准备推出具有此改进的新版本的 Inkdrop。

希望你喜欢它!

[Inkdrop v5](https://res.cloudinary.com/practicaldev/image/fetch/s--EAze4Tfc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/i/lebsvcgd622wesyossry.png)

另请参阅

  • 我如何保持我的个人项目超过 3 年

  • 放慢节奏——迈向长效产品

  • 我如何为我的 SaaS 吸引前 500 名付费用户,成本为 5 美元/月

感谢大家的支持!

  • 墨滴网址:https://www.inkdrop.app/

  • 发送反馈:https://forum.inkdrop.app/

  • 联系我们:contact@inkdrop.app

  • 推特:https://twitter.com/inkdrop_app

  • Instagram:https://www.instagram.com/craftzdog/

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐