Performance性能
Developers frequently ask about strategies to optimize the performance of Electron applications. 开发人员经常询问优化Electron应用程序性能的策略。Software engineers, consumers, and framework developers do not always agree on one single definition of what "performance" means. 软件工程师、消费者和框架开发人员并不总是就“性能”的含义达成一致。This document outlines some of the Electron maintainers' favorite ways to reduce the amount of memory, CPU, and disk resources being used while ensuring that your app is responsive to user input and completes operations as quickly as possible. 本文档概述了一些Electron维护人员最喜欢的减少内存、CPU和磁盘资源使用量的方法,同时确保您的应用程序响应用户输入并尽快完成操作。Furthermore, we want all performance strategies to maintain a high standard for your app's security.此外,我们希望所有性能策略都能为您的应用程序的安全性保持高标准。
Wisdom and information about how to build performant websites with JavaScript generally applies to Electron apps, too. 关于如何使用JavaScript构建高性能网站的智慧和信息通常也适用于Electron应用程序。To a certain extent, resources discussing how to build performant Node.js applications also apply, but be careful to understand that the term "performance" means different things for a Node.js backend than it does for an application running on a client.在某种程度上,讨论如何构建性能良好的Node.js应用程序的资源也适用,但请注意,术语“性能”对Node.js后端的含义与在客户端上运行的应用程序不同。
This list is provided for your convenience – and is, much like our security checklist – not meant to exhaustive. 此列表是为了您的方便而提供的,与安全检查表非常相似,并非详尽无遗。It is probably possible to build a slow Electron app that follows all the steps outlined below. 有可能构建一个遵循以下所有步骤的慢Electron应用程序。Electron is a powerful development platform that enables you, the developer, to do more or less whatever you want. Electron是一个功能强大的开发平台,它使开发人员能够或多或少地做您想做的事情。All that freedom means that performance is largely your responsibility.所有这些自由意味着表现在很大程度上是你的责任。
Measure, Measure, Measure测量,测量,测量
The list below contains a number of steps that are fairly straightforward and easy to implement. 下面的列表包含许多步骤,这些步骤相当简单,易于实现。However, building the most performant version of your app will require you to go beyond a number of steps. 然而,构建应用程序的最高性能版本需要您完成许多步骤。Instead, you will have to closely examine all the code running in your app by carefully profiling and measuring. 但是,您必须仔细分析和测量应用程序中运行的所有代码。Where are the bottlenecks? 瓶颈在哪里?When the user clicks a button, what operations take up the brunt of the time? 当用户点击一个按钮时,哪些操作占据了大部分时间?While the app is simply idling, which objects take up the most memory?当应用程序只是空闲时,哪些对象占用的内存最多?
Time and time again, we have seen that the most successful strategy for building a performant Electron app is to profile the running code, find the most resource-hungry piece of it, and to optimize it. 我们一次又一次地看到,构建高性能Electron应用程序最成功的策略是分析运行的代码,找到其中最需要资源的部分,并对其进行优化。Repeating this seemingly laborious process over and over again will dramatically increase your app's performance. 反复重复这个看似费劲的过程将极大地提高应用程序的性能。Experience from working with major apps like Visual Studio Code or Slack has shown that this practice is by far the most reliable strategy to improve performance.使用Visual Studio Code或Slack等主要应用程序的经验表明,这种做法是迄今为止提高性能最可靠的策略。
To learn more about how to profile your app's code, familiarize yourself with the Chrome Developer Tools. 要了解有关如何配置应用程序代码的更多信息,请熟悉Chrome开发者工具。For advanced analysis looking at multiple processes at once, consider the Chrome Tracing tool.对于同时查看多个进程的高级分析,请考虑Chrome跟踪工具。
Recommended Reading推荐阅读
Get Started With Analyzing Runtime Performance开始分析运行时性能Talk: "Visual Studio Code - The First Second"演讲:“Visual Studio Code-第一秒”
Checklist: Performance recommendations清单:绩效建议
Chances are that your app could be a little leaner, faster, and generally less resource-hungry if you attempt these steps.如果您尝试这些步骤,您的应用程序可能会更精简、更快,而且通常不会太消耗资源。
Carelessly including modules粗心地包括模块Loading and running code too soon加载和运行代码过快Blocking the main process阻塞主进程Blocking the renderer process阻止渲染器进程Unnecessary polyfills不必要的填充物Unnecessary or blocking network requests不必要或阻止网络请求Bundle your code捆绑您的代码
1. Carelessly including modules粗心地包括模块
Before adding a Node.js module to your application, examine said module. How many dependencies does that module include? 在将Node.js模块添加到应用程序之前,请检查所述模块。该模块包括多少依赖项?What kind of resources does it need to simply be called in a 需要在require()
statement? require()
语句中调用哪些资源?You might find that the module with the most downloads on the NPM package registry or the most stars on GitHub is not in fact the leanest or smallest one available.您可能会发现,NPM软件包注册表上下载量最多的模块或GitHub上的星数最多的模块实际上并不是可用的最精简或最小的模块。
Why?为什么?
The reasoning behind this recommendation is best illustrated with a real-world example. 这项建议背后的理由最好用一个真实的例子来说明。During the early days of Electron, reliable detection of network connectivity was a problem, resulting many apps to use a module that exposed a simple 在Electron的早期,网络连接的可靠检测是一个问题,导致许多应用程序使用一个暴露了简单isOnline()
method.isOnline()
方法的模块。
That module detected your network connectivity by attempting to reach out to a number of well-known endpoints. 该模块检测到您的网络连接,试图连接到多个知名端点。For the list of those endpoints, it depended on a different module, which also contained a list of well-known ports. 对于这些端点的列表,它依赖于一个不同的模块,该模块还包含一个已知端口的列表。This dependency itself relied on a module containing information about ports, which came in the form of a JSON file with more than 100,000 lines of content. 这种依赖本身依赖于一个包含端口信息的模块,该模块以JSON文件的形式提供,包含超过100000行内容。Whenever the module was loaded (usually in a 无论何时加载模块(通常在require('module')
statement), it would load all its dependencies and eventually read and parse this JSON file. require('module')
语句中),它都会加载所有依赖项,并最终读取和解析这个JSON文件。Parsing many thousands lines of JSON is a very expensive operation. 解析数千行JSON是一项非常昂贵的操作。On a slow machine it can take up whole seconds of time.在速度慢的机器上,它可能会占用整秒的时间。
In many server contexts, startup time is virtually irrelevant. 在许多服务器环境中,启动时间实际上是无关紧要的。A Node.js server that requires information about all ports is likely actually "more performant" if it loads all required information into memory whenever the server boots at the benefit of serving requests faster. 如果需要所有端口信息的Node.js服务器在服务器启动时将所有所需信息加载到内存中,以更快地服务请求,那么它实际上可能“更高效”。The module discussed in this example is not a "bad" module. 本示例中讨论的模块不是“坏”模块。Electron apps, however, should not be loading, parsing, and storing in memory information that it does not actually need.然而,Electron应用程序不应该加载、解析和存储它实际上不需要的信息。
In short, a seemingly excellent module written primarily for Node.js servers running Linux might be bad news for your app's performance. 简而言之,一个主要为运行Linux的Node.js服务器编写的看似优秀的模块可能对应用程序的性能来说是个坏消息。In this particular example, the correct solution was to use no module at all, and to instead use connectivity checks included in later versions of Chromium.在这个特定示例中,正确的解决方案是完全不使用模块,而是使用Chromium的后续版本中包含的连接检查。
How?怎么做?
When considering a module, we recommend that you check:考虑模块时,我们建议您检查:
the size of dependencies included包含的依赖项的大小the resources required to load (加载(require()
) itrequire()
)所需的资源the resources required to perform the action you're interested in执行您感兴趣的操作所需的资源
Generating a CPU profile and a heap memory profile for loading a module can be done with a single command on the command line. 生成用于加载模块的CPU配置文件和堆内存配置文件可以通过命令行上的单个命令完成。In the example below, we're looking at the popular module 在下面的示例中,我们正在查看流行的模块request
.request
。
node --cpu-prof --heap-prof -e "require('request')"
Executing this command results in a 执行此命令将在执行该命令的目录中生成一个.cpuprofile
file and a .heapprofile
file in the directory you executed it in. .cpuprofile
文件和一个.heapprofile
文件。Both files can be analyzed using the Chrome Developer Tools, using the 这两个文件都可以使用Chrome开发工具分别使用“性能”和“内存”选项卡进行分析。Performance
and Memory
tabs respectively.
In this example, on the author's machine, we saw that loading 在本例中,在作者的机器上,我们看到加载request
took almost half a second, whereas node-fetch
took dramatically less memory and less than 50ms.request
几乎需要半秒,而node-fetch
占用的内存显著减少,不到50ms。
2. Loading and running code too soon加载和运行代码过快
If you have expensive setup operations, consider deferring those. 如果您有昂贵的安装操作,请考虑推迟这些操作。Inspect all the work being executed right after the application starts. 检查应用程序启动后立即执行的所有工作。Instead of firing off all operations right away, consider staggering them in a sequence more closely aligned with the user's journey.与其立即启动所有操作,不如考虑按照与用户旅程更紧密一致的顺序错开它们。
In traditional Node.js development, we're used to putting all our 在传统的Node.js开发中,我们习惯于将所有require()
statements at the top. require()
语句放在顶部。If you're currently writing your Electron application using the same strategy and are using sizable modules that you do not immediately need, apply the same strategy and defer loading to a more opportune time.如果您当前正在使用相同的策略编写Electron应用程序,并且正在使用不立即需要的大模块,请应用相同的策略并将加载推迟到更合适的时间。
Why?为什么?
Loading modules is a surprisingly expensive operation, especially on Windows. 加载模块是一项非常昂贵的操作,尤其是在Windows上。When your app starts, it should not make users wait for operations that are currently not necessary.当应用程序启动时,它不应让用户等待当前不必要的操作。
This might seem obvious, but many applications tend to do a large amount of work immediately after the app has launched - like checking for updates, downloading content used in a later flow, or performing heavy disk I/O operations.这似乎很明显,但许多应用程序往往在应用程序启动后立即执行大量工作,如检查更新、下载后续流中使用的内容,或执行繁重的磁盘I/O操作。
Let's consider Visual Studio Code as an example. 让我们以Visual Studio Code为例。When you open a file, it will immediately display the file to you without any code highlighting, prioritizing your ability to interact with the text. 当您打开一个文件时,它会立即向您显示该文件,而不会突出显示任何代码,从而优先考虑您与文本交互的能力。Once it has done that work, it will move on to code highlighting.一旦完成了这项工作,它将继续进行代码高亮显示。
How?怎么做?
Let's consider an example and assume that your application is parsing files in the fictitious 让我们考虑一个示例,假设您的应用程序正在以虚构的.foo
format. .foo
格式解析文件。In order to do that, it relies on the equally fictitious 为了做到这一点,它依赖于同样虚构的foo-parser
module. foo-parser
模块。In traditional Node.js development, you might write code that eagerly loads dependencies:在传统的Node.js开发中,您可能会编写急于加载依赖项的代码:
const fs = require('fs')
const fooParser = require('foo-parser')
class Parser {
constructor () {
this.files = fs.readdirSync('.')
}
getParsedFiles () {
return fooParser.parse(this.files)
}
}
const parser = new Parser()
module.exports = { parser }
In the above example, we're doing a lot of work that's being executed as soon as the file is loaded. 在上面的示例中,我们正在做大量的工作,这些工作在文件加载后立即执行。Do we need to get parsed files right away? Could we do this work a little later, when 我们需要立即获取解析文件吗?我们可以稍后再做这项工作吗,当实际调用getParsedFiles()
is actually called?getParsedFiles()
时?
// "fs" is likely already being loaded, so the `require()` call is cheap
const fs = require('fs')
class Parser {
async getFiles () {
// Touch the disk as soon as `getFiles` is called, not sooner.
// Also, ensure that we're not blocking other operations by using
// the asynchronous version.
this.files = this.files || await fs.readdir('.')
return this.files
}
async getParsedFiles () {
// Our fictitious foo-parser is a big and expensive module to load, so
// defer that work until we actually need to parse files.
// Since `require()` comes with a module cache, the `require()` call
// will only be expensive once - subsequent calls of `getParsedFiles()`
// will be faster.
const fooParser = require('foo-parser')
const files = await this.getFiles()
return fooParser.parse(files)
}
}
// This operation is now a lot cheaper than in our previous example
const parser = new Parser()
module.exports = { parser }
In short, allocate resources "just in time" rather than allocating them all when your app starts.简言之,在应用启动时“及时”分配资源,而不是全部分配资源。
3. Blocking the main process阻塞主进程
Electron's main process (sometimes called "browser process") is special: It is the parent process to all your app's other processes and the primary process the operating system interacts with. Electron的主进程(有时称为“浏览器进程”)非常特殊:它是所有应用程序其他进程的父进程,也是操作系统与之交互的主进程。It handles windows, interactions, and the communication between various components inside your app. 它处理窗口、交互以及应用程序内各个组件之间的通信。It also houses the UI thread.它还包含UI线程。
Under no circumstances should you block this process and the UI thread with long-running operations. 在任何情况下,都不应使用长时间运行的操作来阻止此进程和UI线程。Blocking the UI thread means that your entire app will freeze until the main process is ready to continue processing.阻止UI线程意味着整个应用程序将冻结,直到主进程准备好继续处理。
Why?为什么?
The main process and its UI thread are essentially the control tower for major operations inside your app. 主进程及其UI线程本质上是应用程序内部主要操作的控制塔。When the operating system tells your app about a mouse click, it'll go through the main process before it reaches your window. 当操作系统告诉您的应用程序鼠标点击时,它将在到达窗口之前通过主进程。If your window is rendering a buttery-smooth animation, it'll need to talk to the GPU process about that – once again going through the main process.如果你的窗口正在渲染一个黄油般平滑的动画,它将需要与GPU进程讨论这个问题-再次通过主进程。
Electron and Chromium are careful to put heavy disk I/O and CPU-bound operations onto new threads to avoid blocking the UI thread. Electron和Chromium小心地将沉重的磁盘I/O和CPU绑定操作放到新线程上,以避免阻塞UI线程。You should do the same.你也应该这样做。
How?怎么做?
Electron's powerful multi-process architecture stands ready to assist you with your long-running tasks, but also includes a small number of performance traps.Electron强大的多进程架构随时准备帮助您完成长期运行的任务,但也包括少量性能陷阱。
-
For long running CPU-heavy tasks, make use of worker threads, consider moving them to the BrowserWindow, or (as a last resort) spawn a dedicated process.对于长时间运行的CPU繁重的任务,使用工作线程,考虑将它们移动到浏览器窗口,或者(作为最后手段)生成专用进程。 -
Avoid using the synchronous IPC and the尽可能避免使用同步IPC和@electron/remote
module as much as possible.@electron/remote
模块。While there are legitimate use cases, it is far too easy to unknowingly block the UI thread.虽然存在合法的用例,但在不知不觉中阻止UI线程太容易了。 -
Avoid using blocking I/O operations in the main process.避免在主进程中使用阻塞I/O操作。In short, whenever core Node.js modules (like简言之,每当核心Node.js模块(如fs
orchild_process
) offer a synchronous or an asynchronous version, you should prefer the asynchronous and non-blocking variant.fs
或child_process
)提供同步或异步版本时,您应该首选异步和非阻塞变量。
4. Blocking the renderer process阻止渲染器进程
Since Electron ships with a current version of Chrome, you can make use of the latest and greatest features the Web Platform offers to defer or offload heavy operations in a way that keeps your app smooth and responsive.由于Electron附带了当前版本的Chrome,您可以利用Web平台提供的最新和最强大的功能来推迟或减轻繁重的操作,从而保持应用程序的流畅性和响应性。
Why?为什么?
Your app probably has a lot of JavaScript to run in the renderer process. 您的应用程序可能有很多JavaScript要在渲染器进程中运行。The trick is to execute operations as quickly as possible without taking away resources needed to keep scrolling smooth, respond to user input, or animations at 60fps.诀窍是尽可能快地执行操作,而不占用保持滚动平滑、响应用户输入或60fps动画所需的资源。
Orchestrating the flow of operations in your renderer's code is particularly useful if users complain about your app sometimes "stuttering".如果用户抱怨您的应用程序有时“结巴”,则在渲染器代码中协调操作流特别有用。
How?怎么做?
Generally speaking, all advice for building performant web apps for modern browsers apply to Electron's renderers, too. 一般来说,为现代浏览器构建高性能web应用程序的所有建议也适用于Electron的渲染器。The two primary tools at your disposal are currently 您可以使用的两个主要工具是用于小操作的requestIdleCallback()
for small operations and Web Workers
for long-running operations.requestIdleCallback()
和用于长时间运行操作的Web Worker
。
requestIdleCallback()
allows developers to queue up a function to be executed as soon as the process is entering an idle period. 允许开发人员在进程进入空闲期后立即将要执行的函数排队。It enables you to perform low-priority or background work without impacting the user experience. 它使您能够在不影响用户体验的情况下执行低优先级或后台工作。For more information about how to use it, check out its documentation on MDN.有关如何使用它的更多信息,请查看有关MDN的文档。
Web Workers are a powerful tool to run code on a separate thread. 是在单独线程上运行代码的强大工具。There are some caveats to consider – consult Electron's multithreading documentation and the MDN documentation for Web Workers. 有一些注意事项需要考虑-请参阅Electron的多线程文档和Web Workers的MDN文档。They're an ideal solution for any operation that requires a lot of CPU power for an extended period of time.对于长时间需要大量CPU功率的任何操作,它们都是理想的解决方案。
5. Unnecessary polyfills不必要的填充物
One of Electron's great benefits is that you know exactly which engine will parse your JavaScript, HTML, and CSS. Electron最大的好处之一是,您确切地知道哪个引擎将解析JavaScript、HTML和CSS。If you're re-purposing code that was written for the web at large, make sure to not polyfill features included in Electron.如果您要使用为web编写的代码,请确保不要使用Electron中包含的polyfill功能。
Why?为什么?
When building a web application for today's Internet, the oldest environments dictate what features you can and cannot use. 在为今天的Internet构建web应用程序时,最古老的环境决定了您可以和不能使用哪些功能。Even though Electron supports well-performing CSS filters and animations, an older browser might not. 尽管Electron支持性能良好的CSS筛选器和动画,但较旧的浏览器可能不支持。Where you could use WebGL, your developers may have chosen a more resource-hungry solution to support older phones.在您可以使用WebGL的地方,您的开发人员可能已经选择了一个更需要资源的解决方案来支持旧手机。
When it comes to JavaScript, you may have included toolkit libraries like jQuery for DOM selectors or polyfills like the 当涉及JavaScript时,您可能已经包含了工具包库,如用于DOM选择器的jQuery或用于支持regenerator-runtime
to support async/await
.async/await
的regenerator-runtime
的polyfill。
It is rare for a JavaScript-based polyfill to be faster than the equivalent native feature in Electron. 基于JavaScript的polyfill很少比Electron中的等效本机功能更快。Do not slow down your Electron app by shipping your own version of standard web platform features.不要通过发布自己版本的标准web平台功能来降低Electron应用程序的速度。
How?怎么做?
Operate under the assumption that polyfills in current versions of Electron are unnecessary. 在假设Electron的当前版本中的多填充物是不必要的情况下操作。If you have doubts, check caniuse.com and check if the version of Chromium used in your Electron version supports the feature you desire.如果您有疑问,请检查caniuse.com,并检查Electron版中使用的Chromium版本是否支持您想要的功能。
In addition, carefully examine the libraries you use. 此外,请仔细检查您使用的库。Are they really necessary? 它们真的有必要吗?例如,jQuery
, for example, was such a success that many of its features are now part of the standard JavaScript feature set available.jQuery
非常成功,它的许多功能现在都是可用的标准JavaScript功能集的一部分。
If you're using a transpiler/compiler like TypeScript, examine its configuration and ensure that you're targeting the latest ECMAScript version supported by Electron.如果您使用的是像TypeScript这样的transpiler/编译器,请检查其配置,并确保您的目标是Electron支持的最新ECMAScript版本。
6. Unnecessary or blocking network requests不必要或阻止网络请求
Avoid fetching rarely changing resources from the internet if they could easily be bundled with your application.避免从internet获取很少更改的资源,如果这些资源可以很容易地与应用程序捆绑在一起。
Why?为什么?
Many users of Electron start with an entirely web-based app that they're turning into a desktop application. 许多Electron用户从一个完全基于web的应用程序开始,他们将其转变为桌面应用程序。As web developers, we are used to loading resources from a variety of content delivery networks. 作为web开发人员,我们习惯于从各种内容交付网络加载资源。Now that you are shipping a proper desktop application, attempt to "cut the cord" where possible and avoid letting your users wait for resources that never change and could easily be included in your app.现在你已经发布了一个合适的桌面应用程序,在可能的情况下尝试“切断电源线”,避免让用户等待那些永远不会改变的资源,这些资源可以很容易地包含在你的应用程序中。
A typical example is Google Fonts. 一个典型的例子是谷歌字体。Many developers make use of Google's impressive collection of free fonts, which comes with a content delivery network. 许多开发人员利用谷歌令人印象深刻的免费字体集合,这些字体与内容交付网络一起提供。The pitch is straightforward: Include a few lines of CSS and Google will take care of the rest.宣传很简单:包括几行CSS,剩下的由谷歌负责。
When building an Electron app, your users are better served if you download the fonts and include them in your app's bundle.在构建Electron应用程序时,如果您下载字体并将其包含在应用程序包中,您的用户会得到更好的服务。
How?怎么做?
In an ideal world, your application wouldn't need the network to operate at all. 在理想情况下,您的应用程序根本不需要网络来运行。To get there, you must understand what resources your app is downloading - and how large those resources are.要做到这一点,您必须了解您的应用程序正在下载哪些资源,以及这些资源有多大。
To do so, open up the developer tools. 为此,请打开开发人员工具。Navigate to the 导航到“网络”选项卡并选中“禁用缓存”选项。Network
tab and check the Disable cache
option. Then, reload your renderer. 然后,重新加载渲染器。Unless your app prohibits such reloads, you can usually trigger a reload by hitting 除非您的应用程序禁止此类重新加载,否则您通常可以通过点击Cmd+R或Ctrl+R来触发重新加载,并将开发工具置于焦点。Cmd + R
or Ctrl + R
with the developer tools in focus.
The tools will now meticulously record all network requests. 这些工具现在将仔细记录所有网络请求。In a first pass, take stock of all the resources being downloaded, focusing on the larger files first. 在第一次通过时,评估所有下载的资源,首先关注较大的文件。Are any of them images, fonts, or media files that don't change and could be included with your bundle? 是否有任何图像、字体或媒体文件不会更改,并且可以包含在捆绑包中?If so, include them.如果是,请将其包括在内。
As a next step, enable 作为下一步,启用Network Throttling
. Network Throttling
(网络节流)。Find the drop-down that currently reads 找到当前Online
and select a slower speed such as Fast 3G
. Online
阅读的下拉列表,并选择较慢的速度,如Fast 3G
。Reload your renderer and see if there are any resources that your app is unnecessarily waiting for. 重新加载渲染器,查看应用程序是否有不必要等待的资源。In many cases, an app will wait for a network request to complete despite not actually needing the involved resource.在许多情况下,应用程序将等待网络请求完成,尽管实际上并不需要涉及的资源。
As a tip, loading resources from the Internet that you might want to change without shipping an application update is a powerful strategy. 作为提示,从Internet加载您可能希望更改的资源而不发送应用程序更新是一个强大的策略。For advanced control over how resources are being loaded, consider investing in Service Workers.对于如何加载资源的高级控制,请考虑投资于Service Worker。
7. Bundle your code捆绑您的代码
As already pointed out in "Loading and running code too soon", calling 正如在加载和运行代码过快中已经指出的,调用require()
is an expensive operation. require()
是一个代价高昂的操作。If you are able to do so, bundle your application's code into a single file.如果您能够这样做,请将应用程序的代码捆绑到一个文件中。
Why?为什么?
Modern JavaScript development usually involves many files and modules. 现代JavaScript开发通常涉及许多文件和模块。While that's perfectly fine for developing with Electron, we heavily recommend that you bundle all your code into one single file to ensure that the overhead included in calling 虽然这对于使用Electron开发来说非常好,但我们强烈建议您将所有代码捆绑到一个文件中,以确保调用require()
is only paid once when your application loads.require()
所包含的开销仅在应用程序加载时支付一次。
How?怎么做?
There are numerous JavaScript bundlers out there and we know better than to anger the community by recommending one tool over another. 有很多JavaScript捆绑包,我们知道最好不要通过推荐一种工具而激怒社区。We do however recommend that you use a bundler that is able to handle Electron's unique environment that needs to handle both Node.js and browser environments.但是,我们建议您使用能够处理Electron独特环境的捆绑器,该环境需要同时处理Node.js和浏览器环境。
As of writing this article, the popular choices include Webpack, Parcel, and rollup.js.在撰写本文时,流行的选择包括Webpack、Parcel和rollup.js。