Performance Awareness and Optimization


A talk by @thorstenfrommen

Once upon a time...

Disclaimer: What This Is NOT

  • This is not an AI talk. 😅
  • This is not a talk about front-end performance (specifically).
  • This is not a list of hard rules that everyone needs to follow, always.

Disclaimer: What This Is

  • This is a collection of best practices, guidelines and background information.
  • This is about adopting a performance-oriented mindset.
    • Code examples are just for illustration.
    • Understanding the higher-level concept is key.
    • Translate this into your specific context.
  • Depending on your context, some things might not be as important or even true.
    • Garbage collection?
    • Compiler, optimizer, transpiler etc.?

Disclaimer: Competing interests

  • In software development, there are competing interests and aspects to consider:
    • usability;
    • readability;
    • debugging;
    • maintainability;
    • processing capacity vs. memory capacity;
    • ...
  • In the end, it’s not always about performance, first and foremost.

Definition

Performance

"How well is a task being executed?"

  • Response time?
  • Processing speed?
  • Resources used?
  • Availability?
  • Personal preference?
  • ...

Guiding Principles

Guiding Principles

  • Don’t do work too early.
  • Don’t do work over and over again.
  • Don’t do unnecessarily complex work.
  • Don’t optimize prematurely.
  • Do measure performance.
  • Clean up after yourself.
  • Use the right tool for the job.

Failing Early

Checking the Context

"Should I do anything?"

Checking the Context


				function foo() {

					if ( ! check_condition() ) {
						return /* data */;
					}

					// Perform actual task...
				}
			

Checking the Context


				function foo( WP_Post $post ) {

					if ( $post->post_status !== 'publish' ) {
						return /* data */;
					}

					// Perform actual task...
				}
			

Checking the Context


				function foo( WP_Post $post ) {

					if ( $post->post_status !== 'publish' ) {
						return /* data */;
					}

					if ( $post->post_type !== 'special' ) {
						return /* data */;
					}

					// Perform actual task...
				}
			

Checking the Context


				function foo( WP_Post $post ) {

					if ( $post->post_type !== 'special' ) {
						return /* data */;
					}

					if ( $post->post_status !== 'publish' ) {
						return /* data */;
					}

					// Perform actual task...
				}
			

Checking the Context


				function foo( WP_Post $post ) {

					if ( $post->post_type !== 'special' ) {
						return /* data */;
					}

					if ( ! expensive_check() ) {
						return /* data */;
					}

					// Perform actual task...
				}
			

Checking Data

"Is there anything to do?"

Checking Data


				function process( int $post_id ) {

					// Kick off some processes, maybe fire a WordPress action.

					// Get $data from somewhere...

					update_option( 'option_name', /* initial data */ );

					foreach ( $data as $item ) {
						// Process item...

						update_option( 'option_name', /* updated data */ );
					}

					delete_option( 'option_name' );
				}
			

Checking Data


				function process( int $post_id ) {

					// Get $data from somewhere...
					if ( ! $data ) {
						return;
					}

					// ...
				}
			

Caching

Cache Expensive Operations

"Let’s not spend any more time on this any time soon."

Cache Expensive Operations

  • Local cache (e.g., static variable, instance property).
  • Hyper-local cache (e.g., variable in the current scope).
  • Session.
  • Request (e.g., global variable).
  • Long-lived cache (with expiration and invalidation).

Cache Repetitive Operations

"Let’s keep this handy."

Cache Repetitive Operations

  • Cache function calls.
    
    						$foo = get_foo();
    					
  • Cache array/object lookups (i.e., accessing properties).
    
    						$foo = $data->foo_bar['foo'];
    					

Memoization

"Memo-what?"

Memoization

  • Special form of caching.
  • Cache return value of (pure) functions, given arguments.
  • Examples:
    • wp.element.memo (React.memo)
    • React.useMemo()
    • _.memoize()

Avoiding Intermediaries

Mutate Local Variables


				$data = get_data();

				$filtered_data = array_filter( filter_data, $data );

				$normalized_data = array_map( normalize_data, $filtered_data );
			

vs.


				$data = get_data();

				$data = array_filter( filter_data, $data );

				$data = array_map( normalize_data, $data );
			

Unnecessary Arrays/Objects


						items.reduce( ( a, item ) => {
							return [
								...a,
								Item,
							];
						}, [] );
					

vs.


						items.reduce( ( a, item ) => {
							a.push( item );

							return a;
						}, [] );
					

						items.reduce( ( o, item ) => {
							return {
								...o,
								[item.name]: item,
							];
						}, {} );
					

vs.


						items.reduce( ( o, item ) => {
							o[ item.name ] = item;

							return o;
						}, {} );
					

Prefer Objects over IDs


				function action1( $post_or_id ) { /* ... */ }
				function action2( $post_or_id ) { /* ... */ }
				function action3( $post_or_id ) { /* ... */ }

				/** @var int $post_id */
			


				action1( $post_id );
				action2( $post_id );
				action3( $post_id );
			

vs.


				$post = get_post( $post_id );

				action1( $post );
				action2( $post );
				action3( $post );
			

get_post() Queries


				get_post( 42 );

				↪ WP_Post::get_instance( 42 );

				↪ $wpdb->query( "SELECT * FROM wp_posts WHERE ID = 42 LIMIT 1" );
			

No caching, by design!

Function Execution Control

Throttling

"Let’s do this at a reasonable interval only."

Throttling


				// Execute callback for every single position change.
				window.addEventListener( 'dragover', onDragOver );
			

vs.


				// Execute callback every 200ms, at maximum.
				throttledHandler = throttle( onDragOver, 200 );

				window.addEventListener( 'dragover', throttledHandler );
			

Debouncing

"Let’s do this after some cool-down only."

Debouncing


				// Perform search request for every single user input.
				input.addEventListener( 'change', searchPosts );
			

vs.


				// Perform search once the user stopped typing for 300ms.
				debouncedSearch = debounce( searchPosts, 300 );

				input.addEventListener( 'change', debouncedSearch );
			

debounce vs. throttle

Visualization: debounce vs. throttle

Cleaning Up


(Memory Leaks)

Remove Event Listeners

"Let’s clean up leftover event handlers, potentially causing issues."

Manual Removal


				useEffect( () => {
					window.addEventListener( 'dragover', onDragOver );

					return () => {
						window.removeEventListener( 'dragover', onDragOver );
					};
				}, [ onDragOver ] );
			

Self-Removal


				// Automatic self-removal after having been invoked.
				button.addEventListener( 'click', doStuff, {
					once: true,
				} );
			

Remove Time-Based Functions

"Let’s clean up leftover timed callbacks, potentially causing issues."

setInterval


				useEffect( () => {
					const id = setInterval( doSomething, 600 );

					return () => {
						clearInterval( id );
					};
				}, [ doSomething ] );
			

setTimeout


				useEffect( () => {
					const id = setTimeout( doSomething, 3000 );

					return () => {
						clearTimeout( id );
					};
				}, [ someValue ] );
			

setTimeout


				import { withSafeTimeout } from '@wordpress/components';

				class MyComponent extends React.Component {

				scheduleAction() {
					this.setTimeout( this.doAction, 5000 );
				}

				// No need to clear the timeout.

				// ...
				}

				export default withSafeTimeout( MyComponent );
			

Abort Pending Requests

"Let’s prevent receiving/acting on data we don’t need anymore."

Abort Requests


				useEffect( () => {
					const abortController = new AbortController();

					apiFetch( {
						path: '/some/path',
						signal: abortController.signal,
					} )
						.then( /* ... */ )
						.catch( () => {
							if ( abortController.signal.aborted ) {
								return;
							}

							// ...
						} );

					return () => {
						abortController.abort();
					};
				}, [] );
			

Cancel Scheduled Callbacks

"Let’s prevent executing tasks we don’t need anymore."

Cancel Scheduled Callbacks


				const debouncedFetchData = useMemo( () => {
					debounce( fetchData, 300 );
				}, [ fetchData ] );
			

Cancel Scheduled Callbacks


				const debouncedFetchData = useMemo( () => {
					debounce( fetchData, 300 );
				}, [ fetchData ] );

				useEffect( () => {
					return () => {
						debouncedFetchData.cancel();
					};
				}, [ debouncedFetchData ] );
			

Measuring Performance

Query Monitor

Query Monitor

Xdebug Profiler

Xdebug Profiler

AWS X-Ray

AWS X-Ray

Browser Dev Tools

  • Network
  • Performance
  • React Dev Tools
  • Redux Dev Tools
  • ...

MySQL Server/Console


				mysql> SET profiling = 1;
				Query OK, 0 rows affected (0.00 sec)

				mysql> DROP TABLE IF EXISTS t1;
				Query OK, 0 rows affected, 1 warning (0.00 sec)

				mysql> CREATE TABLE T1 (id INT);
				Query OK, 0 rows affected (0.01 sec)

				mysql> SHOW PROFILES;
				+----------+----------+--------------------------+
				| Query_ID | Duration | Query                    |
				+----------+----------+--------------------------+
				|        0 | 0.000088 | SET PROFILING = 1        |
				|        1 | 0.000136 | DROP TABLE IF EXISTS t1  |
				|        2 | 0.011947 | CREATE TABLE t1 (id INT) |
				+----------+----------+--------------------------+
				3 rows in set (0.00 sec)
			

Know Your Tools


(or Environment)

Know Your Tools/Environment

  • Unique for everyone.
  • Unique for environment.
  • Unique for every use case.
  • Be interested in how things work, at least high-level.
  • Make use of established libraries (e.g., lodash).
  • Don't use *_meta tables for value-based querying.
  • Make use of DB indexes (and allow MySQL to actually use these).
  • ...

Questions?


(How was my performance? 😬)