2019-10-21 20:39:54 +00:00
|
|
|
|
# Performance
|
|
|
|
|
|
|
|
|
|
Developers frequently ask about strategies to optimize the performance of
|
|
|
|
|
Electron applications. 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. 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. 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.
|
|
|
|
|
|
|
|
|
|
This list is provided for your convenience – and is, much like our
|
|
|
|
|
[security checklist][security] – not meant to exhaustive. It is probably possible
|
|
|
|
|
to build a slow Electron app that follows all the steps outlined below. Electron
|
|
|
|
|
is a powerful development platform that enables you, the developer, to do more
|
|
|
|
|
or less whatever you want. 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. 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.
|
|
|
|
|
|
|
|
|
|
To learn more about how to profile your app's code, familiarize yourself with
|
|
|
|
|
the Chrome Developer Tools. For advanced analysis looking at multiple processes
|
|
|
|
|
at once, consider the [Chrome Tracing] tool.
|
|
|
|
|
|
|
|
|
|
### Recommended Reading
|
|
|
|
|
|
|
|
|
|
* [Get Started With Analyzing Runtime Performance][chrome-devtools-tutorial]
|
|
|
|
|
* [Talk: "Visual Studio Code - The First Second"][vscode-first-second]
|
|
|
|
|
|
|
|
|
|
## Checklist
|
|
|
|
|
|
|
|
|
|
Chances are that your app could be a little leaner, faster, and generally less
|
|
|
|
|
resource-hungry if you attempt these steps.
|
|
|
|
|
|
2019-10-31 15:41:18 +00:00
|
|
|
|
1. [Carelessly including modules](#1-carelessly-including-modules)
|
|
|
|
|
2. [Loading and running code too soon](#2-loading-and-running-code-too-soon)
|
|
|
|
|
3. [Blocking the main process](#3-blocking-the-main-process)
|
|
|
|
|
4. [Blocking the renderer process](#4-blocking-the-renderer-process)
|
|
|
|
|
5. [Unnecessary polyfills](#5-unnecessary-polyfills)
|
|
|
|
|
6. [Unnecessary or blocking network requests](#6-unnecessary-or-blocking-network-requests)
|
|
|
|
|
7. [Bundle your code](#7-bundle-your-code)
|
2019-10-21 20:39:54 +00:00
|
|
|
|
|
|
|
|
|
## 1) Carelessly including modules
|
|
|
|
|
|
|
|
|
|
Before adding a Node.js module to your application, examine said module. How
|
|
|
|
|
many dependencies does that module include? What kind of resources does
|
|
|
|
|
it need to simply be called in a `require()` statement? 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.
|
|
|
|
|
|
|
|
|
|
### 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 `isOnline()` method.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
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. Parsing many thousands lines of JSON is a very expensive operation. 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. 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.
|
|
|
|
|
|
|
|
|
|
In short, a seemingly excellent module written primarily for Node.js servers
|
|
|
|
|
running Linux might be bad news for your app's performance. 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.
|
|
|
|
|
|
|
|
|
|
### How?
|
|
|
|
|
|
|
|
|
|
When considering a module, we recommend that you check:
|
|
|
|
|
|
|
|
|
|
1. the size of dependencies included
|
|
|
|
|
2) the resources required to load (`require()`) it
|
|
|
|
|
3. 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. In the example below, we're looking at
|
|
|
|
|
the popular module `request`.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
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. Both files can be analyzed using
|
|
|
|
|
the Chrome Developer Tools, using the `Performance` and `Memory` tabs
|
|
|
|
|
respectively.
|
|
|
|
|
|
|
|
|
|
![performance-cpu-prof]
|
|
|
|
|
|
|
|
|
|
![performance-heap-prof]
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
## 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 `require()`
|
|
|
|
|
statements at the top. 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.
|
|
|
|
|
|
|
|
|
|
### Why?
|
|
|
|
|
|
|
|
|
|
Loading modules is a surprisingly expensive operation, especially on 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.
|
|
|
|
|
|
|
|
|
|
Let's consider Visual Studio Code as an example. 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. In order to do that, it relies on the
|
|
|
|
|
equally fictitious `foo-parser` module. In traditional Node.js development,
|
|
|
|
|
you might write code that eagerly loads dependencies:
|
|
|
|
|
|
|
|
|
|
```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?
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
// "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. It handles windows, interactions, and the
|
|
|
|
|
communication between various components inside your app. It also houses the
|
|
|
|
|
UI thread.
|
|
|
|
|
|
|
|
|
|
Under no circumstances should you block this process and the UI thread with
|
|
|
|
|
long-running operations. Blocking the UI thread means that your entire app
|
|
|
|
|
will freeze until the main process is ready to continue processing.
|
|
|
|
|
|
|
|
|
|
### Why?
|
|
|
|
|
|
|
|
|
|
The main process and its UI thread are essentially the control tower for major
|
|
|
|
|
operations inside your app. 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.
|
|
|
|
|
|
|
|
|
|
Electron and Chromium are careful to put heavy disk I/O and CPU-bound operations
|
|
|
|
|
onto new threads to avoid blocking the UI thread. 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.
|
|
|
|
|
|
|
|
|
|
1) For long running CPU-heavy tasks, make use of
|
|
|
|
|
[worker threads][worker-threads], consider moving them to the BrowserWindow, or
|
|
|
|
|
(as a last resort) spawn a dedicated process.
|
|
|
|
|
|
|
|
|
|
2) Avoid using the synchronous IPC and the `remote` module as much as possible.
|
|
|
|
|
While there are legitimate use cases, it is far too easy to unknowingly block
|
|
|
|
|
the UI thread using the `remote` module.
|
|
|
|
|
|
|
|
|
|
3) Avoid using blocking I/O operations in the main process. In short, whenever
|
|
|
|
|
core Node.js modules (like `fs` or `child_process`) offer a synchronous or an
|
|
|
|
|
asynchronous version, you should prefer the asynchronous and non-blocking
|
|
|
|
|
variant.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
### Why?
|
|
|
|
|
|
|
|
|
|
Your app probably has a lot of JavaScript to run in the renderer process. 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.
|
|
|
|
|
|
|
|
|
|
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. The two primary tools at your
|
|
|
|
|
disposal are currently `requestIdleCallback()` for small operations and
|
|
|
|
|
`Web Workers` for long-running operations.
|
|
|
|
|
|
|
|
|
|
*`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][request-idle-callback].
|
|
|
|
|
|
|
|
|
|
*Web Workers* are a powerful tool to run code on a separate thread. There are
|
|
|
|
|
some caveats to consider – consult Electron's
|
|
|
|
|
[multithreading documentation][multithreading] and the
|
|
|
|
|
[MDN documentation for Web Workers][web-workers]. They're an ideal solution
|
|
|
|
|
for any operation that requires a lot of CPU power for an extended period of
|
|
|
|
|
time.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 5) Unnecessary polyfills
|
|
|
|
|
|
|
|
|
|
One of Electron's great benefits is that you know exactly which engine will
|
|
|
|
|
parse your JavaScript, HTML, and CSS. If you're re-purposing code that was
|
|
|
|
|
written for the web at large, make sure to not polyfill features included in
|
|
|
|
|
Electron.
|
|
|
|
|
|
|
|
|
|
### Why?
|
|
|
|
|
|
|
|
|
|
When building a web application for today's Internet, the oldest environments
|
|
|
|
|
dictate what features you can and cannot use. Even though Electron supports
|
|
|
|
|
well-performing CSS filters and animations, an older browser might not. Where
|
|
|
|
|
you could use WebGL, your developers may have chosen a more resource-hungry
|
|
|
|
|
solution to support older phones.
|
|
|
|
|
|
|
|
|
|
When it comes to JavaScript, you may have included toolkit libraries like
|
|
|
|
|
jQuery for DOM selectors or polyfills like the `regenerator-runtime` to support
|
|
|
|
|
`async/await`.
|
|
|
|
|
|
|
|
|
|
It is rare for a JavaScript-based polyfill to be faster than the equivalent
|
|
|
|
|
native feature in Electron. Do not slow down your Electron app by shipping your
|
|
|
|
|
own version of standard web platform features.
|
|
|
|
|
|
|
|
|
|
### How?
|
|
|
|
|
|
|
|
|
|
Operate under the assumption that polyfills in current versions of Electron
|
|
|
|
|
are unnecessary. If you have doubts, check [caniuse.com][https://caniuse.com/]
|
|
|
|
|
and check if the [version of Chromium used in your Electron version](../api/process.md#processversionschrome-readonly)
|
|
|
|
|
supports the feature you desire.
|
|
|
|
|
|
|
|
|
|
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-need].
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 6) Unnecessary or blocking network requests
|
|
|
|
|
|
|
|
|
|
Avoid fetching rarely changing resources from the internet if they could easily
|
|
|
|
|
be bundled with your application.
|
|
|
|
|
|
|
|
|
|
### Why?
|
|
|
|
|
|
|
|
|
|
Many users of Electron start with an entirely web-based app that they're
|
|
|
|
|
turning into a desktop application. As web developers, we are used to loading
|
|
|
|
|
resources from a variety of content delivery networks. 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.
|
|
|
|
|
|
|
|
|
|
When building an Electron app, your users are better served if you download
|
|
|
|
|
the fonts and include them in your app's bundle.
|
|
|
|
|
|
|
|
|
|
### 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`
|
|
|
|
|
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`. Find the drop-down that currently
|
|
|
|
|
reads `Online` and select a slower speed such as `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. For advanced
|
|
|
|
|
control over how resources are being loaded, consider investing in
|
|
|
|
|
[Service Workers][service-workers].
|
|
|
|
|
|
|
|
|
|
## 7) Bundle your code
|
|
|
|
|
|
|
|
|
|
As already pointed out in
|
|
|
|
|
"[Loading and running code too soon](#2-loading-and-running-code-too-soon)",
|
|
|
|
|
calling `require()` is an expensive operation. 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. 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 `require()` is only paid once when your application loads.
|
|
|
|
|
|
|
|
|
|
### How?
|
|
|
|
|
|
|
|
|
|
There are numerous JavaScript bundlers out there and we know better than to
|
|
|
|
|
anger the community by recommending one tool over another. 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.
|
|
|
|
|
|
|
|
|
|
As of writing this article, the popular choices include [Webpack][webpack],
|
|
|
|
|
[Parcel][parcel], and [rollup.js][rollup].
|
|
|
|
|
|
|
|
|
|
[security]: ./security.md
|
|
|
|
|
[performance-cpu-prof]: ../images/performance-cpu-prof.png
|
|
|
|
|
[performance-heap-prof]: ../images/performance-heap-prof.png
|
|
|
|
|
[chrome-devtools-tutorial]: https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/
|
|
|
|
|
[worker-threads]: https://nodejs.org/api/worker_threads.html
|
|
|
|
|
[web-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
|
|
|
|
|
[request-idle-callback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
|
|
|
|
|
[multithreading]: ./multithreading.md
|
|
|
|
|
[caniuse]: https://caniuse.com/
|
|
|
|
|
[jquery-need]: http://youmightnotneedjquery.com/
|
|
|
|
|
[service-workers]: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
|
|
|
|
|
[webpack]: https://webpack.js.org/
|
|
|
|
|
[parcel]: https://parceljs.org/
|
|
|
|
|
[rollup]: https://rollupjs.org/
|
|
|
|
|
[vscode-first-second]: https://www.youtube.com/watch?v=r0OeHRUCCb4
|