We’re again with Half 2 of our “Constructing Studio in Public” sequence! As we speak, we’re diving into the challenges we confronted whereas optimizing Studio’s efficiency. This put up might be particularly beneficial when you’re growing an Electron app and coping with efficiency points or when you’re merely interested by how the Studio app features behind the scenes.
If you happen to’re following the sequence, remember to take a look at our first put up: Utilizing WordPress Elements and Tailwind CSS in our Native Improvement App.
As a reminder, Studio is our free, open supply native improvement app. It’s based mostly on Electron (the main focus of at present’s put up!) and is at present out there for Mac and Home windows.
Overcoming the challenges of operating native improvement websites
Operating an area improvement website will be complicated, typically requiring the setup of a number of instruments. A typical strategy entails utilizing multi-container functions like Docker and Docker Compose along with organising an online server with a WordPress set up and a MySQL database. This course of can develop into much more difficult when managing a number of websites concurrently.
Studio was designed to simplify this course of, permitting customers to arrange websites rapidly with none prior configuration. This functionality is powered primarily by the WordPress Playground undertaking, which permits anybody to run a completely useful WordPress website inside a browser or Node.js surroundings.
For every website created with Studio, we run a fundamental net server utilizing ExpressJS to deal with net requests and use WordPress Playground to course of them.
Initially, this was applied within the Electron-based Studio app with out noticeable efficiency points.
Nevertheless, as we expanded our testing throughout Mac and Home windows, we noticed some slowness in UI interactions when managing and navigating websites. Every part appeared correctly configured, however one thing was clearly off.
Retaining the primary course of light-weight
As we delved into these efficiency points, we found that operating websites inside Electron’s most important course of was the first explanation for the slowdown. Processing net requests and executing the related PHP code for WordPress in the primary course of added further load, which negatively impacted different operations, aka that UI slowness we have been seeing.
Electron’s documentation is extremely beneficial for addressing efficiency points, significantly these associated to blocking the primary course of. It was clear that sustaining a light-weight most important course of is essential, and avoiding heavy or blocking operations on this context is important. Nevertheless, this realization introduced a brand new problem: how can we detach the operating websites from the primary course of?
Spawning devoted processes
To deal with the efficiency points, we adopted the tried-and-true technique of “divide and conquer.”
The concept was to run Studio’s websites in devoted processes, separate from the primary one. Since Electron is constructed on Node.js, spawning baby processes appeared like a believable resolution. Nevertheless, Electron additionally presents a utilityProcess utility, which behaves equally to Node’s baby processes, however working on the browser degree and aligning extra intently with Electron’s app mannequin.
Whereas this strategy promised to alleviate the load on the primary course of, it additionally launched further complexity. We needed to handle these new processes and deal with communication between the primary and devoted processes by way of messages. Moreover, we encountered challenges associated to the construct configuration and utilizing Webpack for constructing the appliance.
Beneath is a full instance of implementing this strategy (click on to broaden every instance to see the complete code):
Devoted Course of Supervisor (course of.js):
// This path needs to be calculated dynamically because the file may very well be in
// completely different places relying on the construct configuration
const PROCESS_MODULE_PATH = ‘./process-child.js’;
const DEFAULT_RESPONSE_TIMEOUT = 120000;
class Course of {
lastMessageId = 0;
course of;
ongoingMessages = {};
async init() {
return new Promise( ( resolve, reject ) => {
const spawnListener = async () => {
// Eradicating exit listener as we solely want it upon beginning
this.course of?.off( ‘exit’, exitListener );
resolve();
};
const exitListener = ( code ) => {
if ( code !== 0 ) {
reject( new Error( `course of exited with code ${ code } upon beginning` ) );
}
};
this.course of = utilityProcess
.fork( PROCESS_MODULE_PATH, [], {
serviceName: ‘dedicated-process’,
env: {
…course of.env,
IN_CHILD_PROCESS: ‘true’,
APP_NAME: app.identify,
// Word that Electron context will not be out there within the devoted course of.
// Add right here different surroundings variables that may be wanted.
},
} )
.on( ‘spawn’, spawnListener )
.on( ‘exit’, exitListener );
} );
}
// That is an instance operate. Be at liberty so as to add extra for different functions.
async exampleFunc( command, args ) {
const message = ‘exampleFunc’;
const messageId = this.sendMessage( message, { command, args } );
return await this.waitForResponse( message, messageId );
}
// It is vital to understand that the method might be operating
// till it is explicitly stopped.
async cease() {
await this.killProcess();
}
sendMessage( message, knowledge ) {
const course of = this.course of;
if ( ! course of ) {
throw Error( ‘The method isn’t operating’ );
}
const messageId = this.lastMessageId++;
course of.postMessage( { message, messageId, knowledge } );
return messageId;
}
async waitForResponse( originalMessage, originalMessageId, timeout = DEFAULT_RESPONSE_TIMEOUT ) {
const course of = this.course of;
if ( ! course of ) {
throw Error( ‘The method isn’t operating’ );
}
if ( this.ongoingMessages[ originalMessageId ] ) {
throw Error(
`The ‘waitForResponse’ operate was already known as for message ID ${ originalMessageId } from the message ‘${ originalMessage }’. ‘waitForResponse’ could solely be known as as soon as per message ID.`
);
}
return new Promise( ( resolve, reject ) => {
const handler = ( { message, messageId, knowledge, error } ) => {
if ( message !== originalMessage || messageId !== originalMessageId ) {
return;
}
course of.removeListener( ‘message’, handler );
clearTimeout( timeoutId );
delete this.ongoingMessages[ originalMessageId ];
if ( typeof error !== ‘undefined’ ) {
console.error( error );
reject( new Error( error ) );
return;
}
resolve( knowledge );
};
const timeoutHandler = () => {
reject( new Error( `Request for message ${ originalMessage } timed out` ) );
course of.removeListener( ‘message’, handler );
};
const timeoutId = setTimeout( timeoutHandler, timeout );
const cancelHandler = () => {
clearTimeout( timeoutId );
reject( {
error: new Error( `Request for message ${ originalMessage } was canceled` ),
canceled: true,
} );
course of.removeListener( ‘message’, handler );
};
this.ongoingMessages[ originalMessageId ] = { cancelHandler };
course of.addListener( ‘message’, handler );
} );
}
async killProcess() {
const course of = this.course of;
if ( ! course of ) {
throw Error( ‘The method isn’t operating’ );
}
this.cancelOngoingMessages();
return new Promise( ( resolve, reject ) => {
course of.as soon as( ‘exit’, ( code ) => {
if ( code !== 0 ) {
reject( new Error( `Course of exited with code ${ code } upon stopping` ) );
return;
}
resolve();
} );
course of.kill();
} ).catch( ( error ) => {
console.error( error );
} );
}
cancelOngoingMessages() {
Object.values( this.ongoingMessages ).forEach( ( { cancelHandler } ) => {
cancelHandler();
} );
}
}
module.exports = Course of;
Devoted Course of Logic (process-child.js):
console.log( `Run preliminary setup for app: ${ course of.env.APP_NAME }` );
const handlers = {
exampleFunc: createHandler( exampleFunc ),
};
async operate exampleFunc( knowledge ) {
const { command, args } = knowledge;
// Substitute this with the specified logic.
console.log( `Run heavy operation ${ command } with args: ${ args }` );
}
operate createHandler( handler ) {
return async ( message, messageId, knowledge ) => {
attempt {
const response = await handler( knowledge );
course of.parentPort.postMessage( {
message,
messageId,
knowledge: response,
} );
} catch ( error ) {
course of.parentPort.postMessage( ‘Unknown Error’,
);
}
};
}
course of.parentPort.on( ‘message’, async ( { knowledge: messagePayload } ) => {
const { message, messageId, knowledge } = messagePayload;
const handler = handlers[ message ];
if ( ! handler ) {
course of.parentPort.postMessage( {
message,
messageId,
error: Error( `No handler outlined for message ‘${ message }’` ),
} );
return;
}
await handler( message, messageId, knowledge );
} );
Run instance (most important.js):
const course of = new Course of();
await course of.init();
await course of.exampleFunc( ‘my-command’, [ ‘example’, 100 ] );
}
…
app.whenReady().then( () => {
runExample();
} );
…
Word: The code above has been tailored to be used in a generic instance Electron undertaking. You possibly can check it utilizing Electron Fiddle.
Construct configuration and Webpack
Our undertaking construct setup depends on Forge and Webpack. Implementing devoted processes launched further complexity, as we initially bundled all of the code right into a single file.
Nevertheless, since devoted processes require their code to run in isolation from the primary course of, we wanted to separate the bundles. After adjusting the Webpack configuration, we efficiently set it as much as produce the required recordsdata.
Beneath is an instance of the modifications we utilized (click on to broaden every instance to see the complete code):
Earlier than:
export const mainConfig: Configuration = {
// That is the primary entry level on your utility, it is the primary file
// that runs in the primary course of.
entry: ‘./src/index.ts’,
…
After:
import { kind Configuration, DefinePlugin } from ‘webpack’;
// Further entries are bundled individually from the primary bundle. They’re primarily used
// for employee threads and forked processes, which have to be loaded independently.
const extraEntries = [
{
name: ‘siteServerProcess’,
path: ‘./src/lib/site-server-process-child.ts’,
exportName: ‘SITE_SERVER_PROCESS_MODULE_PATH’,
},
// Here you can configure other dedicated processes
];
export default operate mainConfig( _env: unknown, args: Report< string, unknown > ) {
const isProduction = args.mode === ‘manufacturing’;
// Generates the required plugins to reveal the module path of additional entries.
const definePlugins = extraEntries.map( ( entry ) => {
// The trail calculation is predicated on how the Forge’s webpack plugin generates the trail for Electron recordsdata.
// Reference: https://github.com/electron/forge/blob/b298b2967bdc79bdc4e09681ea1ccc46a371635a/packages/plugin/webpack/src/WebpackConfig.ts#L113-L140
const modulePath = isProduction
? `require(‘path’).resolve(__dirname, ‘..’, ‘most important’, ‘${ entry.identify }.js’)`
: JSON.stringify( path.resolve( __dirname, `.webpack/most important/${ entry.identify }.js` ) );
return new DefinePlugin( {
[ entry.exportName ]: modulePath,
} );
} );
return [] ), …definePlugins ],
;
}
export const mainBaseConfig: Configuration = {
entry: {
// That is the primary entry level on your utility, it is the primary file
// that runs in the primary course of.
index: ‘./src/index.ts’,
// Inject further entries into the Webpack configuration.
// These entries are primarily used for employee threads and forked processes.
…extraEntries.cut back( ( accum, entry ) => {
return { …accum, [ entry.name ]: entry.path };
}, {} ),
},
…
Word: The code above is instantly from Studio, written in TypeScript.
Bonus tip: keep away from blocking file system operations
We additionally observed efficiency points when utilizing synchronous file system operations when constructing Studio, particularly when utilizing the synchronous variations of features, which might block the primary course of. To stop this, it’s greatest to make use of the promise-based or callback variations of those features.
For instance, as a substitute of utilizing:
Use:
Able to construct?
If this info has piqued your curiosity, or when you’re growing WordPress websites, begin leveraging the facility of Studio at present. It’s free, it’s open supply, and it seamlessly integrates into your improvement workflow.
After downloading Studio, join it to your WordPress.com account (free or paid) to unlock options like Demo Websites.
Need to contribute to Studio? Listed below are some GitHub points you possibly can dive into:
php_curl lacking and curl_exec deactivated
Whole and free dimension of filesystem reported in websites doesn’t match precise values
Permission points utilizing a theme that makes use of composer
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!