Перейти к основному содержанию

Производительность

Разработчики часто спрашивают о стратегиях оптимизации производительности приложений Electron. Les ingénieurs, les consommateurs et les développeurs de framework ne sont pas toujours d'accord sur une seule définition de ce que signifie "performance". Данный документ описывает некоторые из предпочитаемых разработчиками Electron способов уменьшения объема используемых ресурсов (ОЗУ, ресурсы процессора, дисковое пространство), обеспечивая при этом отзывчивость и выполняя операции как можно быстрее. Кроме того, мы хотим, чтобы все стратегии повышения производительности соответствовали высоким стандартам безопасности.

Рекомендации по созданию производительных веб-сайтов на JavaScript в целом применимы и к приложениям на Electron. Некоторые ресурсы, посвященные созданию производительных приложений на Node.js, также применимы, однако имейте в виду, что термин "производительность" имеет другое значение для бэкенда на Node.js, нежели для клиентского приложения.

This list is provided for your convenience – and is, much like our security checklist – not meant to be exhaustive. Вполне возможно создать медленное приложение Electron, следующее всем описанным ниже рекомендациям. Electron - это мощная платформа разработки, которая позволяет вам, разработчику, делать практически все, что вы захотите. Вся эта свобода означает, что производительность в значительной степени является вашей ответственностью.

Измерение, Измерение, Измерение

В приведенном ниже списке содержится ряд шагов, которые достаточно просты и легко осуществить. Тем не менее, создание наиболее эффективной версии вашего приложения потребует вас выйти за рамки нескольких шагов. Вам придется внимательно изучить весь выполняемый в вашем приложении код путем тщательного профилирования и оценки. Что является узким местом? Какие операции затрачивают больше всего времени, когда пользователь нажимает на кнопку? Какие объекты занимают больше всего памяти, когда приложение бездействует?

Мы неоднократно убеждались, что наиболее успешной стратегией создания производительного приложения Electron является профилирование кода, поиск наиболее ресурсоемкого фрагмента и его оптимизация. Повторяя этот, трудоемкий на первый взгляд, процесс снова и снова, вы значительно повысите производительность вашего приложения. Опыт работы с такими крупными приложениями, как Visual Studio Code и Slack, доказал, что такой подход является самым надежным способом повышения производительности.

Чтобы узнать больше о профилировании кода вашего приложения, ознакомьтесь с инструментами разработчика Chrome. Для расширенного анализа сразу нескольких процессов можно использовать инструмент Chrome Tracing.

Рекомендуем к прочтению

Рекомендации по увеличению производительности

Следуя данным рекомендациям, вы сможете сделать ваше приложение более компактным, быстрым и в целом менее требовательным к ресурсам.

  1. Бездумное подключение модулей
  2. Преждевременная загрузка и выполнение кода
  3. Блокировка основного процесса
  4. Блокирование процесса рендеринга
  5. Ненужные polyfills
  6. Ненужные или блокирующие сетевые запросы
  7. Объединение кода
  8. Вызов Menu.setApplicationMenu(null), когда вам не нужно стандартное меню

1. Бездумное подключение модулей

Прежде чем добавить модуль Node.js в свое приложение, изучите этот модуль. Сколько зависимостей этот модуль имеет? Какие ресурсы требуются для того, чтобы просто вызвать его в операторе require()? Может оказаться, что модуль с наибольшим количеством загрузок в реестре пакетов NPM или наибольшим количеством звезд на GitHub на самом деле не является самым эффективным или компактным.

Почему?

Обоснование данной рекомендации лучше всего проиллюстрировать на реальном примере. During the early days of Electron, reliable detection of network connectivity was a problem, resulting in many apps using a module that exposed a simple isOnline() method.

Этот модуль определял подключение к сети, пытаясь связаться с рядом известных конечных точек. Для получения списка этих конечных точек он полагался на другой модуль, который также содержал список известных портов. В свою очередь эта зависимость сама зависела от модуля, содержащего информацию о портах, которая поступала в виде JSON-файла с более чем 100 000 строк содержимого. Всякий раз, когда модуль загружался (обычно в операторе require('module')), он загружал все свои зависимости и в конечном итоге считывал и разбирал этот JSON-файл. Парсинг нескольких тысяч строк JSON - это очень дорогая операция. На медленной машине она может занимать целые секунды времени.

Во многих случаях время запуска сервера не имеет значения. Сервер Node.js, которому требуется информация обо всех портах, будет более производительным, если он загрузит всю необходимую информацию в память при старте и сможет быстрее обслуживать поступающие запросы. Модуль, рассмотренный в данном примере, не является "плохим" модулем. Однако приложениям Electron не следует загружать, разбирать и хранить в памяти информацию, которая им на самом деле не нужна.

Казалось бы, отличный модуль, написанный для серверов Node.js под управлением Linux, может негативно сказаться на производительности вашего приложения. В данном случае правильным решением было вообще не подключать отдельный модуль, а использовать функцию проверки соединения, включенную в более поздние версии Chromium.

Как?

При выборе модуля мы рекомендуем вам проверить:

  1. размер включаемых зависимостей
  2. ресурсы, необходимые для его загрузки (require())
  3. ресурсы, необходимые для выполнения интересующего вас действия

Профилирование процессора и кучи при загрузке модуля можно выполнить с помощью одной команды в командной строке. В примере ниже мы рассматриваем популярный модуль request.

node --cpu-prof --heap-prof -e "require('request')"

Выполнение этой команды приводит к созданию файлов .cpuprofile и .heapprofile в директории, в которой она была выполнена. Оба файла можно проанализировать с помощью инструментов разработчика Chrome, используя вкладки Производительность и Память соответственно.

Профиль производительности процессора

Профиль производительности памяти

В данном примере можно увидеть, что загрузка request на компьютере автора заняла почти полсекунды, в то время как node-fetch потребовалось значительно меньше памяти и менее 50 мс.

2. Преждевременная загрузка и выполнение кода

Если у вас есть затратные процессы на этапе настройки, подумайте о том, чтобы отложить их. Проверьте все выполняемые операции сразу после запуска приложения. Вместо того чтобы запускать все задачи сразу, подумайте о том, чтобы распределить их в порядке, более точно соответствующем пути пользователя.

В традиционной разработке на Node.js мы привыкли помещать все наши операторы require() в самом верху. Если вы сейчас пишете свое приложение Electron по той же стратегии и используете объемные модули, которые вам не требуются немедленно, примените ту же стратегию и отложите загрузку на более подходящее время.

Почему?

Загрузка модулей является на удивлениезатратной операцией, особенно в Windows. При старте ваше приложение не должно заставлять пользователей ждать выполнения операций, в которых в данный момент нет необходимости.

Это может показаться очевидным, однако многие приложения имеют тенденцию выполнять большой объем работы сразу после запуска приложения - например, проверка обновлений, загрузка содержимого, используемого в более позднем процессе, или выполнение тяжелых операций ввода-вывода на диск.

В качестве примера рассмотрим Visual Studio Code. При открытии файла он сразу же отображает его без какой-либо подсветки кода, отдавая приоритет взаимодействию с текстом. Как только он выполнит эту задачу, он перейдет к подсветке кода.

Как?

Давайте рассмотрим пример и предположим, что ваше приложение обрабатывает файлы в вымышленном формате .foo. Для этого оно полагается на столь же вымышленный модуль foo-parser. При традиционной разработке на Node.js вы могли бы написать код, который загружает зависимости так:

parser.js
const fs = require('node: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 }

В приведенном выше примере мы делаем много работы, которая выполняется сразу после загрузки файла. Действительно ли нам нужно сразу же получить обработанные файлы? Можем ли мы выполнить эту работу немного позже, когда getParsedFiles() фактически будет вызвана?

parser.js
// "fs" is likely already being loaded, so the `require()` call is cheap
const fs = require('node: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.promises.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 }

Одним словом, выделяйте ресурсы "just in time", а не все сразу при запуске приложения.

3. Блокировка основного процесса

Основной процесс Electron (иногда называемый "процесс браузера") является особенным: это родительский процесс для всех других процессов вашего приложения и именно с ним взаимодействует операционная система. Он управляет окнами, взаимодействием и связью между различными компонентами внутри вашего приложения. В нем также находится поток пользовательского интерфейса.

Ни в коем случае не блокируйте этот процесс и поток пользовательского интерфейса длительными операциями. Блокировка потока пользовательского интерфейса приведет к тому, что все ваше приложение зависнет, пока основной процесс не будет готов продолжить обработку.

Почему?

Основной процесс и его поток пользовательского интерфейса, по сути, являются диспетческой вышкой для основных операций внутри вашего приложения. Когда операционная система сообщает вашему приложению о щелчке мыши, он пройдет через основной процесс, прежде чем достигнет вашего окна. Если ваше окно отрисовывает плавную анимацию, то ему нужно будет поговорить об этом с процессом GPU - опять же через главный процесс.

Electron и Chromium тщательно следят за тем, чтобы тяжелые операции ввода-вывода на диск и CPU-bound операции выполнялись в отдельных потоках, для того чтобы избежать блокировки потока пользовательского интерфейса. Вы должны делать то же самое.

Как?

Мощная многопроцессная архитектура Electron готова помочь вам в решении долго выполняемых задач, но также содержит небольшое количество ловушек производительности.

  1. Для длительных и тяжелых задач следует использовать рабочие потоки (worker threads), переместить их в BrowserWindow или (в крайнем случае) создать отдельный процесс.

  2. По возможности избегайте использования синхронного IPC и модуля @electron/remote. Хотя существуют вполне оправданные сценарии их использования, они могут привести к неосознанному блокированию потока пользовательского интерфейса.

  3. Избегайте использования блокирующих операций ввода-вывода в основном процессе. Другими словами, когда основные модули Node.js (например, fs или child_process) предлагают синхронную и асинхронную версии, вам следует отдать предпочтение асинхронному и неблокирующему варианту.

4. Блокирование процесса рендеринга

Поскольку Electron поставляется с актуальной версией Chrome, вы можете использовать новейшие и наилучшие возможности Web-платформы чтобы сделать приложение плавным и отзывчивым, отложив или разгрузив тяжелые операции.

Почему?

Ваше приложение, вероятно, имеет много JavaScript кода выполняющегося в процессе рендеринга. Хитрость заключается в том, чтобы выполнять операции как можно быстрее, не отнимая ресурсы, необходимые для плавной прокрутки, реагирования на ввод пользователя или анимации с частотой 60 кадров в секунду.

Организация потока операций в коде вашего рендерера особенно полезна, если пользователи жалуются на то, что ваше приложение иногда тормозит.

Как?

В целом, все советы по созданию производительных веб-приложений для современных браузеров применимы и к рендерерам Electron. Два основных инструмента в вашем распоряжении - requestIdleCallback() для небольших операций и Web Workers для длительных операций.

requestIdleCallback() позволяет разработчикам поставить функцию в очередь для исполнения, как только процесс перейдет в состояние простоя. Это позволяет выполнять низкоприоритетную или фоновую работу, не оказывая влияния на работу пользователей. Для получения дополнительной информации вы можете ознакомиться с его документацией на MDN.

Web Workers - это мощный инструмент для выполнения кода в отдельном потоке. Существуют некоторые оговорки, которые следует учитывать. Для этого ознакомьтесь с документацией по многопоточности Electron и документацией MDN по Web Workers. Они являются идеальным решением для любых операций, требующих большой мощности процессора в течение длительного времени.

5. Ненужные polyfills

Одним из преимуществ Electron является то, что вы точно знаете, какой движок будет парсить ваш JavaScript, HTML и CSS. Если вы повторно используете код, который был написан для веб в целом, убедитесь, что вы не должны полифилить функции, включенные в Electron.

Почему?

При создании веб-приложения для современного Интернета самые старые среды диктуют, какие функции вы можете использовать, а какие нет. Даже если Electron поддерживает хорошо работающие CSS-фильтры и анимацию, старый браузер может не поддерживать их. Там, где вы могли бы использовать WebGL, ваши разработчики могли выбрать более требовательное к ресурсам решение для обеспечения поддержки старых телефонов.

Что касается JavaScript, вы могли использовать такие инструментальные библиотеки, как jQuery для селекторов DOM или полифиллы наподобие regenerator-runtime для поддержки async/await.

Редко бывает, что полифилл на JavaScript работает быстрее, чем эквивалентная нативная функция в Electron. Не замедляйте работу вашего приложения Electron, поставляя собственную версию стандартных функций веб-платформы.

Как?

Исходите из того, что полифиллы в текущих версиях Electron не нужны. Если у вас имеются сомнения, зайдите на caniuse.com и проверьте поддержку желаемой функции в используемой в Electron версии Chromium.

Кроме того, внимательно изучите используемые вами библиотеки. Действительно ли они необходимы? Например, jQuery, был настолько успешным, что многие его функции теперь являются частью стандартного набора возможностей JavaScript.

Если вы используете транспилятор/компилятор, например, TypeScript, убедитесь, что вы используете последнюю поддерживаемую Electron версию ECMAScript.

6. Ненужные или блокирующие сетевые запросы

Избегайте получения редко изменяемых ресурсов из Интернета, если они могут быть легко встроены в ваше приложение.

Почему?

Многие пользователи Electron начинают с веб-приложения, которое они превращают в настольное приложение. Как веб-разработчики, мы привыкли загружать ресурсы из различных сетей доставки контента. Теперь, когда вы создаете полноценное настольное приложение, постарайтесь по возможности "перерезать шнур" и не заставлять пользователей ждать ресурсы, которые никогда не меняются и могут быть легко встроены в ваше приложение.

Типичный пример - шрифты Google. Многие разработчики используют впечатляющую коллекцию бесплатных шрифтов Google, которая поставляется вместе с сетью доставки контента. Идея проста: Включите несколько строк CSS, а Google позаботится обо всем остальном.

При создании приложения Electron вашим пользователям будет удобнее, если вы загрузите шрифты и добавите их в пакет вашего приложения.

Как?

В идеальном мире вашему приложению для работы вообще не нужна была бы сеть. Для этого вам необходимо определить, какие ресурсы загружает ваше приложение и насколько они велики.

Откройте инструменты разработчика. Перейдите на вкладку Сеть и включите опцию Отключить кеш. Затем перезагрузите ваш рендерер. Если ваше приложение не запрещает такую перезагрузку, ее обычно можно вызвать, нажав Cmd + R или Ctrl + R, когда инструменты разработчика находятся в фокусе.

Теперь инструменты будут скрупулезно записывать все сетевые запросы. При первом проходе проанализируйте все загружаемые ресурсы, в первую очередь обращая внимание на большие файлы. Есть ли среди них изображения, шрифты или медиафайлы, которые не меняются и могут быть добавлены в ваш пакет? Если да, добавьте их.

В качестве следующего шага включите функцию ограничения пропускной способности сети. Найдите выпадающий список с надписью Без ограничения и выберите более медленную скорость, например 3G (высокая скорость). Перезагрузите ваш рендерер и посмотрите, не ожидает ли ваше приложение каких-либо ресурсов без необходимости. Во многих случаях приложение будет ждать завершения сетевого запроса, хотя на самом деле ему не нужен запрашиваемый ресурс.

В принципе, загрузка ресурсов из Интернета, которые вы захотите изменить без необходимости обновления приложения, является эффективной стратегией. Для расширенного контроля над процессом загрузки ресурсов подумайте об использовании Service Workers.

7. Объединение кода

Как уже отмечалось в разделе "Преждевременная загрузка и выполнение кода", вызов require() является дорогостоящей операцией. Если у вас есть такая возможность, соберите код вашего приложения в один файл.

Почему?

Современная разработка на JavaScript обычно осуществляется с использованием множества файлов и модулей. Хотя это совершенно нормально при разработке на Electron, мы настоятельно рекомендуем собирать весь ваш код в один файл, чтобы гарантировать, что накладные расходы, связанные с вызовом require(), будут понесены только один раз при загрузке вашего приложения.

Как?

Существует множество JavaScript-сборщиков. Мы не будем злить сообщество, рекомендуя один инструмент вместо другого, однако мы рекомендуем использовать сборщик, способный работать с уникальной средой Electron, которая должна поддерживать как Node.js, так и браузерное окружение.

На момент написания этой статьи популярными вариантами являются Webpack, Parcel и rollup.js.

8. Вызов Menu.setApplicationMenu(null), когда вам не нужно стандартное меню

При запуске Electron создает стандартное меню с некоторыми основными пунктами. Вы можете захотеть изменить данное поведение, что положительно скажется на производительности.

Почему?

Если вы создаете собственное меню или используете безрамочное окно без меню, вам следует заранее сообщить об этом Electron, чтобы он не создавал стандартное меню.

Как?

Вызовите Menu.setApplicationMenu(null) перед app.on("ready"). Это предотвратит создание стандартного меню. Смотрите также https://github.com/electron/electron/issues/35512 для соответствующего обсуждения.