Using The Interactivity API instead of React (part 1)

When I first built the Veterans Archive, I was very proud of it—it was a whole React app. Years later, I’m wiser, regret my decisions and a better alternative has arrived: the interactivity API + the interactivity router.

You will see references to “i11y” which is my shorthand for “interactivity”

Why rebuild?

Unbeknownst to me at the time, WordPress ships React as the following packages:

  1. react-dom: 43.9kb (ouch)
  2. veteranArchive (the code for the page): 6.8kb
  3. element: 5.7kb
  4. react: 4.8kb
  5. url: 4.4kb
  6. api-fetch: 3.3kb
  7. regenerator-runtime: 3.0kb
  8. i18n: 2.9kb
  9. hooks: 2.3kb
  10. react-jsx-runtime: 1.2kb
  11. escape-html: 1.0kb

That’s 11 packages at 79.3kb.

The interactivity + router ships:

  1. veteranSearch (the code for the page): 1.4kb
  2. interactivity: 15.8kb
  3. interactivity router: 12.0kb (loaded when needed)

2 packages at a little over half the javascript. Huge performance win for page loads.

Also, the interactivity api + router gives us a few things for free:

  1. Mostly javascript-less fallback for those who don’t use it
  2. Server-side hydration (no initial loading state)
  3. Less overall code because #1 is handled so gracefully

So, not only do we get a performance win, we also get cleaner, DRY-er code that is also more accessible. It’s a no-brainer from a technical standpoint, so let’s get into the brainy bit.

DRY Code Stats (click if you’re a nerd)
  • Total Diff (excluding package-lock.json): +642 | -1601
  • That’s 959 lines removed, and (maybe more importantly) a whole directory removed (veteran-archive)
    • This included 4 subdirectories and 15 files in favor of a much simpler 3 files of Javascript.

Loosely based on the Choctaw Small Business Calendar, here’s how I rebuilt the search app with the interactivity API.

Summary of Performance Gains

The Process

  • Page tested: /veterans (archive & search page)
  • Env: Incognito, no optimize plugins, PHP 8.4
  • Disable Cache: ✅

Getting Started

  1. Navigate to page
  2. reload once to warm cache, then run tests:

The Tests

  1. Lighthouse (Desktop performance)
  2. Page Load x5
  3. Pagination 3 & 4, reset to main
  4. Legion of Merit
  5. Chad
  6. Stephen
  7. Lighthouse (Desktop Performance)

The Results

MetricStagingDevelopmentDifference
Lighthouse Desktop55, 59 (avg 57)94, 96 (avg 95)+38 points
Avg page load finish1.06s746ms~30% faster
Page 3 fetch6.11s593ms~90% faster
Page 4 fetch5.44s4.42s~19% faster
“Legion of Merit” search6.14s391ms~94% faster
“Chad” search8.52s2.86s~66% faster
“Stephen” search6.60s2.60s~61% faster
JS requests196~68% fewer
JS transferred866kb36.1kb~96% smaller
JS resources2,057kb99.2kb~95% smaller

The Assets

To enqueue the assets properly, a few things need to be set up:

  1. npm run start/build needs to be using the --experimental-modules flag,
  2. Webpack should be configured to handle module and classic builds concurrently (see the starter-theme for example), and
  3. The theme needs to properly enqueue the module script.

Let’s start with #2.

In the updatedConfig.entry object, drop in your script:

const updatedConfig = {
		...webpackConfig,
		entry: {
			...webpackConfig.entry,
			veteranSearch: path.resolve(
				THEME_SRC,
				'js/veteran-search/view.ts'
			),
		},
		output: {
			...webpackConfig.output,
			path: THEME_DIST,
			filename: '[name].js',
		},
	};

Pretty simple, but if you don’t do this, Query Monitor should complain that you’re doing it wrong because the dependency array will build out wp-interactivity and not @wordpress/interactivity, type=>module like it expects.

#3, the enqueue

Similarly, it’s a pretty minor tweak, but instead of wp_enqueue_script, you’ll use wp_enqueue_script_module and wp_interactivity()->add_client_navigation_support_to_script_module()in the wp_enqueue_scripts callback function.

<?php
if ( is_post_type_archive( 'veterans' ) ) {
  $veteran_search_assets = require get_template_directory() . '/dist/veteranSearch.asset.php';
  wp_enqueue_script_module('veteran-search', get_template_directory_uri() . '/dist/veteranSearch.js', $veteran_search_assets['dependencies'], $veteran_search_assets['version']);
  wp_interactivity()->add_client_navigation_support_to_script_module('veteran-search');
}

Process the interactivity directives

In a classic theme like Veterans Archive and CSBD, WordPress needs some way to process the directives and handle server-side render + client-side hydration. Since the search app is basically the whole archive template, it’s pretty easy using the process_directives function:

<?php
/**
 * Archive: Veterans
 *
 * @package ChoctawNation
 */

use ChoctawNation\VeteranSearch\Interactivity_Helper;

get_header();
$i11y = new Interactivity_Helper();
ob_start();
?>
<div data-wp-interactive="<?php echo $i11y::STORE; ?>" data-wp-router-region="<?php echo $i11y::ROUTER_REGION; ?>">
	<!-- rest of page... -->
</div>
<?php
$html = ob_get_clean();
echo wp_interactivity_process_directives( $html );
get_footer();

The main thing here is the ob_start() + echo wp_interactivity_process_directives($html) pattern. Of note, I’ve also got a helper class ($i11y) that stores the keys for the data-wp-interactive and data-wp-router-region values.

See something inaccurate?