Публичное создание студии: Поддержание высокой производительности нашего приложения для локальной разработки для улучшения UX
В этом посте мы расскажем о стратегиях и методах, которые мы внедрили, чтобы обеспечить бесперебойную работу нашего приложения на базе Electron Studio и высокую производительность для пользователей.
Мы возвращаемся ко второй части нашей серии “Создание студии на публике”! Сегодня мы расскажем о проблемах, с которыми мы столкнулись при оптимизации производительности Studio. Этот пост будет особенно полезен, если вы разрабатываете приложение Electron и сталкиваетесь с проблемами производительности или если вам просто интересно узнать, как работает приложение Studio за кулисами.
Если вы следите за новостями, обязательно ознакомьтесь с нашим первым постом:
Напоминаем, что Studio – это наше бесплатное приложение для локальной разработки с открытым исходным кодом. Он основан на (основной теме сегодняшнего поста!) и в настоящее время доступен для Mac и Windows.
Решение проблем, связанных с запуском локальных сайтов разработки
Запуск локального сайта разработки может быть сложным процессом, часто требующим настройки нескольких инструментов. Типичный подход предполагает использование многоконтейнерных приложений, таких как Docker и Docker Compose, в дополнение к настройке веб-сервера с установленным WordPress и базой данных MySQL. Этот процесс может стать еще более сложным при одновременном управлении несколькими сайтами.
Studio было разработано, чтобы упростить этот процесс, позволяя пользователям создавать сайты быстро и без каких-либо предварительных настроек. Эта возможность питаться в первую очередь , что позволяет им запускать полностью функциональный веб-сайт WordPress в браузере или Node.js среды.
Для каждого сайта, созданного с помощью Studio, мы запускаем базовый веб-сервер, который обрабатывает веб-запросы, и используем WordPress Playground для их обработки.
Изначально это было реализовано в приложении Electron Studio без заметных проблем с производительностью.
Однако, когда мы расширили наше тестирование на Mac и Windows, мы заметили некоторую замедленность взаимодействия пользовательского интерфейса при управлении сайтами и навигации по ним. Казалось, что все настроено правильно, но что-то явно было не так.
Упрощение основного процесса
Изучив эти проблемы с производительностью, мы обнаружили, что основной причиной замедления был запуск сайтов в рамках основного процесса Electron. Обработка веб-запросов и выполнение связанного с ними PHP-кода для WordPress в основном процессе добавили дополнительную нагрузку, что негативно сказалось на других операциях, а также на медлительности пользовательского интерфейса, которую мы наблюдали.
Документация Electron невероятно ценна для решения проблем с производительностью, особенно связанных с . Было ясно, что поддержание упрощенного основного процесса имеет решающее значение, и в этом контексте важно избегать тяжелых или блокирующих операций. Однако осознание этого факта поставило перед нами новую задачу: как нам отделить работающие сайты от основного процесса?
Создание специализированных процессов
Для решения проблем с производительностью мы применили проверенную стратегию “разделяй и властвуй”.
Идея заключалась в том, чтобы запускать сайты Studio в выделенных процессах, отдельно от основного. Поскольку Electron построен на базе Node.js, создание дочерних процессов казалось разумным решением. Однако Electron также предлагает утилиту, которая ведет себя аналогично дочерним процессам Node, но работает на уровне браузера и более точно соответствует модели приложения Electron.
Хотя этот подход обещал снизить нагрузку на основной процесс, он также привнес дополнительные сложности. Нам пришлось управлять этими новыми процессами и поддерживать связь между основным и выделенным процессами с помощью сообщений. Кроме того, мы столкнулись с проблемами, связанными с конфигурацией сборки и использованием для сборки приложения.
Ниже приведен полный пример реализации этого подхода (нажмите, чтобы развернуть каждый пример, чтобы увидеть полный код):
Выделенный менеджер процессов (process.js):
const { app, utilityProcess } = требуется( `electron` );// Этот путь должен вычисляться динамически, поскольку файл может находиться в// разных местах в зависимости от конфигурации сборки const PROCESS_MODULE_PATH = `./process-child.js `;const DEFAULT_RESPONSE_TIMEOUT = 120000;класс Process {lastMessageId = 0;процесс;текущие сообщения = {};async init() _BOS_ возвращает новое обещание( ( разрешить, отклонить ) => _BOS_ const spawnListener = async () => _BOS_// Удаляем прослушиватель выхода, поскольку он нужен нам только при запуске этого процесса?.off( `выход`, exitListener );разрешить();};const exitListener = (код ) => {if ( код !== 0 ) {отклонить(новая ошибка( `процесс завершился с кодом ${ код } при запуске` ) );}};этот.process = utilityProcess.fork( PROCESS_MODULE_PATH, [], {Имя службы: `выделенный процесс`,env: _BOS_...process.env,IN_CHILD_PROCESS: `истина`,ИМЯ_ПРИЛОЖЕНИЯ: app.name ,// Обратите внимание, что электронный контекст не будет доступен в выделенном процессе.// Добавьте сюда другую среду переменные, которые могут понадобиться.},} ).on( `spawn`, spawnListener ).on( `exit`, exitListener );} );}// Это пример функции. Не стесняйтесь добавлять больше для других целей.async exampleFunc( команда, аргументы ) _BOS_const message = `exampleFunc`;const MessageId = this.SendMessage( сообщение, { команда, аргументы } );return await this.waitForResponse(сообщение, идентификатор сообщения );}// Важно учитывать имейте в виду, что процесс будет запущен// до тех пор, пока он не будет явно остановлен.async stop() {await this.KillProcess();}SendMessage(сообщение, данные ) {const process = this.process;if ( ! process ) {выдает ошибку("Процесс не запущен" );}const MessageId = this.lastMessageId++;process.postMessage( { сообщение, идентификатор сообщения, данные } );возвращает идентификатор сообщения;}асинхронное ожидание ответа(исходное сообщение, идентификатор исходного сообщения, тайм-аут = DEFAULT_RESPONSE_TIMEOUT ) {const process = this.process;if ( ! process ) {выдает ошибку( `Процесс не запущен` );}if ( this.ongoingMessages[ originalMessageId ] ) _BOS_выдает ошибку(`Функция `waitForResponse` уже была вызвана для идентификатора сообщения ${ originalMessageId } из сообщения `${ originalMessage }`. `waitForResponse` может быть вызван только один раз для каждого идентификатора сообщения.`);}возвращает новое обещание( ( разрешить, отклонить ) => {обработчик констант = ( {сообщение, идентификатор сообщения, данные, ошибка } ) => {if ( сообщение !== Исходное сообщение || Идентификатор сообщения !== Исходный идентификатор сообщения ) {return;}process.removeListener( `сообщение`, обработчик );очистить время ожидания(timeoutId );удалить this.ongoingMessages[ Исходный идентификатор сообщения ];если ( тип ошибки !== `неопределенный` ) {console.error( ошибка );отклонить(новая ошибка ) );return;}разрешить( данные );};const TimeOutHandler = () => {отклонить( новая ошибка(время ожидания запроса сообщения ${ originalMessage } истекло) );process.removeListener(`сообщение`, обработчик );};const timeoutId = setTimeout( TimeOutHandler, тайм-аут );const cancelHandler = () => {clearTimeout( timeoutId );отклонить( {ошибка: новая ошибка( `Запрос на сообщение ${ originalMessage } был отменен` ),отменено: true,} );process.removeListener( `сообщение`, обработчик );};this.ongoingMessages[ originalMessageId ] = { cancelHandler };process.addListener(`сообщение`, обработчик );} );}асинхронный процесс уничтожения() {const process = this.process;if ( ! process ) {выдает ошибку(`Процесс не запущен` );}this.cancelOngoingMessages();возвращает новое обещание( ( разрешить, отклонить ) => {process.once( `выход`, ( код ) => {if( код !== 0 ) _BOS_отклонить(новая ошибка( `Процесс завершился с кодом ${ код } при остановке` ) );return;}разрешить();} );process.kill();} ).catch( ( error ) => {console.error( ошибка );} );}cancelOngoingMessages() {Object.values(this.ongoingMessages ).forEach( ( { cancelHandler } ) => {cancelHandler();} );}}модуль.экспорт = Процесс;
Выделенная логика процесса (process-child.js):
// При необходимости замените логику начальной настройки на основе переменных среды.console.log( `Запустите начальную настройку приложения: ${ process.env.APP_NAME }` );const handlers = {exampleFunc: createHandler( exampleFunc ),};асинхронная функция exampleFunc( data ) {const { command, args } = data;// Замените это на желаемый logic.console.log( `Выполните сложную операцию ${ command } с помощью args: ${ args }` );}функция createHandler( обработчик ) _BOS_возвращает асинхронный (сообщение, идентификатор сообщения, данные ) => {try {const response = ожидание обработчика( данных );process.parentPort.postMessage( {сообщение,идентификатор сообщения,данные: ответ,} );} catch ( ошибка ) _BOS_процесс.parentPort.postMessage( {сообщение, идентификатор сообщения,ошибка: error?.message || `Неизвестная ошибка`,} );}};}process.parentPort.on( `сообщение`, асинхронный ( { данные: messagePayload } ) => {const {сообщение, идентификатор сообщения, данные } = messagePayload;const handler = обработчики[ сообщения ];if ( ! обработчик ) {process.parentPort.postMessage( {сообщение, идентификатор сообщения,ошибка: Ошибка("Обработчик не определен для сообщения "${ сообщение }" ),} );return;}ожидающий обработчик( сообщение, идентификатор сообщения, данные );} );
Запустите пример (main.js):
асинхронная функция runExample() _BOS_const process = новый процесс();ожидание process.init();ожидание process.exampleFunc( `моя команда`, [ `пример`, 100 ] );}...app.whenReady().then( () => _BOS_runExample();} );…
Примечание: Приведенный выше код был адаптирован для использования в типовом примере проекта Electron. Вы можете протестировать его с помощью .
Конфигурация сборки и Webpack
Настройка сборки нашего проекта основана на и . Внедрение выделенных процессов привело к дополнительной сложности, поскольку изначально мы объединили весь код в один файл.
Однако, поскольку выделенные процессы требуют, чтобы их код выполнялся изолированно от основного процесса, нам потребовалось разделить пакеты.