WordPress Performance API mit SHORTINIT

Auch wenn ich seit über 15 Jahren mit WordPress arbeite, lerne ich immer noch neues dazu. So z.B. in einem aktuellen Kundenprojekt. Leider ist die Konstante define('SHORTINIT', true); nicht von WordPress selber dokumentiert, aber was du damit machen kannst fasse ich hier einmal an einem Praxisbeispiel zusammen.

Das Problem

Komplexere WordPress Projekte werden schnell einmal zur Performance-Hölle, irgend ein Plugin, umfangreiche Gutenbergblöcke und WooCommerce führen schnell dazu, dass die Ladezeiten auch auf schnellen Servern mit Persistent Object Caching explodiert. Das ist sehr schade, denn im Grunde ist der Stack einfach und solide. Ich habe schon Stunden damit verbracht, das Bottleneck in einer bestehenden Installation zu finden, meist ohne grossen Erfolg. Es sind 100 kleine Teile, und wenn jedes davon 50 oder 100ms Ladezeit verursacht, hat man in Summe ein Desaster.

Also nun zum Problem: ein Kunde möchte gerne einen WooCommerce-Shop mit sehr vielen Artikeln (>5000) und Varianten (10-30 pro Artikel) und sehr vielen Produktattributen (Merkmale, Farbe, Grösse, Material etc. insgesamt 13 Dimensionen à jeweils 2-140 Werten). Jede Artikelvariante hat einen Lagerbestand.

Anforderung: nur Artikel die Verfügbar sind, sollen angezeigt werden. Filtern in allen Dimensionen und zusätzliche Freitextsuche sowie Sortierung etc. Zudem sollen nur Filter-Werte in der Auswahl angezeigt, werden für die es auch effektiv noch Artikel verfügbar hat. Normale Shop-Anforderungen also. In WooCommerce lässt sich das so einigermassen mit Boardmitteln abdecken, die Performance ist dann aber nicht akzeptabel.

Da der Import aus dem ERP und die Zahlungsanbindung bereits gebaut sind, bleiben wir also dennoch beim “Framework” Woocommerce.

WooCommerce legt zusätzliche Datenbanktabellen an “Lookup Tables” für Produkt-Attribute (wp_wc_product_attributes_lookup) und Produkt-Metas (wp_wc_product_meta_lookup), das hilft beim Suchen und Filtern im Shop.

Was nun?

Einen Headless-Shop der auf die Woocommerce Store API (obacht, nicht zu verwechseln mit der WooCommerce REST API) zugreift und dadurch schlank filtern und suchen kann. Gesagt getan, mit dem Frontend-Framework der Wahl werden alle Anforderungen abgedeckt und der Shop gebaut. Doch leider hat die Store API ein paar Einschränkungen. So muss ich z.B. selber irgendwie Buch führen, wenn ich nur noch Filter-Werte anzeigen will, bei denen es auch effektiv Produkte hat, also z.B. wenn ich Farbe: rot wähle, sollen im Grössen-Filter nur noch Werte angezeigt werden, bei denen auch effektiv Produkte verfügbar sind (lieferbar). Ich muss also für jede erdenkliche Kombination der Filter ausrechnen ob noch Produkte verfügbar sind oder nicht, bevor der Filter ausgewählt wurde (bei 13 Dimensionen à 2-140 Werten gibt das eine grosse Zahl von Möglichkeiten). Natürlich kann man auch mehrere Filter nacheinander auswählen…

Ich wollte mir nicht eingestehen, dass WooCommerce dafür nicht geeignet ist.

Die Lösung

Regel #1: don’t write your own… Ausser es ist wirklich nötig. Also habe ich kurzerhand eine eigene Query-API gebaut (stark verkürzt und vereinfacht, aber man sieht das Prinzip):


add_action( 'rest_api_init', function () {
    register_rest_route( 'kdgs-shop', '/product-query', [
            'methods'             => 'GET',
            'callback'            => 'kdgs_product_api',
            'permission_callback' => '__return_true', // allow public access
    ] );
} );

function kdgs_product_api( WP_REST_Request $request ) {
    // handle query based on query parameters and order
    // don't forget pagination and search...

    // send result as json
    return rest_ensure_response( $response );
}

Ein durchschnittlicher Request für den 1. Level (also ein Filter ist gesetzt) dauerte ca. 800-1000ms. Obwohl die Resultate aus dem Cache geliefert wurden. Irgendwie immer noch nicht berauschend. Also habe ich das mit einem kleineren Datenset probiert, gleiches Ergebnis. Und dann kam die Erleuchtung. Bei jedem API-Call wird der gesamte WP-Stack hochgefahren, also alle Plugins, alle Hooks, User-Session etc. aber das brauch ich alles gar nicht für eine öffentlich zugängliche Information.

SHORTINIT to the rescue!

Also habe ich in meinem Theme ein neues minimales File angelegt, welche komplett “ausserhalb” der WP Welt lebt: product-api.php

<?php
// this skips the entire WordPress stack being booted up and therefore 
// has a huge performance impact. we can query the cache (get_transient)

define( 'SHORTINIT', true );

require_once( $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php' );


$cache_key = 'kdgs_products_' . hash( 'sha256', json_encode( $_GET ) );
$cached    = get_transient( $cache_key );

if ( $cached !== false ) {
	header( 'Content-Type: application/json' );
	header( 'X-Cache-Hit: ' . $cache_key );

	echo json_encode( $cached );
	exit;
} else {
	// run the query and save it to the cache
	// global $wpdb is available at this point, but not WP_Query or get_posts()
	...
}

Dieses File kann nun vom Headless-Shop aus aufgerufen werden ohne, dass der ganze WordPress-Stack angeworfen wird. Das führt dazu, dass die Response Time auf <50ms (im Cache-Fall) reduziert wurde. Und noch obendrauf kann man Spezialfälle in der Query selber abwickeln, wenn also z.B. Sale-Artikel immer zuerst oder saisonale Artikel spezifisch gehandhabt werden sollen oder in der Freitextsuche auch nach Artikelnummer und Marke gesucht werden soll.

Der Nachteil? Man hat keinen Zugriff auf die ganze WP API, also alle Helper / Sanitizer usw. stehen einem nicht zur Verfügung. Das ist der Kompromiss den man eingeht. Ich finde aber, in begründeten Fällen, wie in meinem Beispiel, ist das gerechtfertigt und der Performance-Impact so gross, dass es vertretbar ist.


Rrrrrrrrmate

After rsync the next amazing tool is TextMate’s rmate. What for? If you are connected to a remote host via ssh you can use rmate as a text-editor which is rather comfortable when you’re working on remote server for example. Install tuts and a nice guide can be found at Ernie Miller’s Blog.


Transfer Songs from iDevice

Ever wanted to sync back songs or other media from your iDevice? Or you have a new computer to sync to? Try iRip 2, it’ll help you big time. Or read the lifehacker article on how to sync your iDevice on a new computer.


For the Win Users

[msconfig] Microsoft Windows has a lot of crap in the startup process. Autostart can be pretty confusing, when you don’t know what you need. A good list with explanations of what you don’t need to start and what you should start can be found here:

http://www.sysinfo.org/startuplist.php

A reference which should be kept in mind. I use it not very often but I’m always very glad I have it.


Use your MacDrive as an external device

Long story:

I added my bargain SSD into my MacBook and was overwhelmed about the speed improvement. Unfortunately, SSD’s are still expensive so I decided to get rid of my MacDrive and put my ‘old’ HDD in there instead. This will cost you (without mentioning the risk of loosing warranty) about 15 bucks on ebay (+the USB adapter case for the MacDrive).

However after adding the HDD and being happy about 620GB in my MacBook I had a small setback. The external device didn’t work. I’ve read, that some ChinaShipping goods may be not manufactured very well. Someone wrote that the contacts inside the case were blocked by a plastic pieces. I checked this out but it seemed to be more something like a software issue.

Short:

I found the solution in this Blog and for my memory I will recap it briefly.

  1. Go get this awesome tool (0xED) to edit the needed systemfile
  2. Go to this folder
  3. /System/Library/Frameworks/DVDPlayback.framework/Versions/A

  4. Backup the file ‘DVDPlayback’ and yes, do back it up! If you mess up, you’ll have a problem
  5. Open a copy of the (backup) file with the awesome editor
  6. Search for ‘Internal’ and replace it with ‘External’ should be found about 4 times
  7. Now save the file and try to somehow move it back to the system folder
    This might not work without root access, try Pathfinder (which is something you should try anyway) or terminal
  8. sudo mv /dir/to/file/DVDPlayback /System/Library/Frameworks/DVDPlayback.framework/Versions/A/
  9. That’s it, your device should work now.