Austausch einer WordPress Core Meta Box zur Beschleunigung der Bearbeitung

Avatar of Andy Adams
Andy Adams am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Das Frontend von CSS-Tricks ist normalerweise ziemlich schnell, da die meisten Seiten gecacht (und bei Anforderung nicht dynamisch generiert) werden. Bis vor kurzem hatte die CSS-Tricks WordPress-Admin-Oberfläche jedoch nicht das gleiche Glück.

Insbesondere der Beitragsbearbeitungsbildschirm war langsam. Qualvoll langsam. Das Speichern eines Entwurfs dauerte mehrere Sekunden, was ausreicht, um Ihren Arbeitsfluss mitten beim Schreiben eines Artikels zu unterbrechen.

Aufbauend auf einem Tipp von Pete Sorensen, der eine Performance-Frage auf dem WordPress StackExchange gestellt hat (und so freundlich war, uns davon zu erzählen), habe ich mich daran gemacht herauszufinden, was die Beitragsbearbeitungsseite so langsam machte.

Laden des Werkzeugkastens

In unserem vorherigen Artikel über langsame WordPress-Abfragen finden und beheben haben wir eine Handvoll Tools überprüft, mit denen Sie langsame SQL-Abfragen identifizieren können. Um dieses spezielle Problem zu lösen, habe ich Debug Bar verwendet und sichergestellt, dass SAVEQUERIES aktiviert war.

Eingrenzung der Verlangsamung

Mit den verfolgten SQL-Abfragen habe ich die Debug-Bar-Oberfläche geöffnet (über den Button in der oberen rechten Admin-Leiste) und zum Reiter "Queries" gewechselt. Hier ist, was ich gesehen habe:

Slow Query Time

Hier stimmt etwas nicht! Jede Zeit über 250 Millisekunden in der Datenbank ist alarmierend; über 1,5 Sekunden für SQL-Abfragen bringen mir Tränen in die Augen.

Als Referenz hier die Abfragelast auf anderen Admin-Seiten:

Faster Query Time

Ich habe die Liste der Abfragen durchgesehen, um zu sehen, ob etwas offensichtlich falsch war. Ich fand einen Verdächtigen:

A Really Slow Query

2.000+ Millisekunden für *eine einzige* Abfrage sind schlechte Nachrichten. Warum, WordPress, warum?

Was ist Ihre Funktion und was tut sie?

Ich brauchte Kontext für die Abfrage. Petes Sorensens Tipp gab mir eine gute Vorstellung davon, woher sie kam, aber es ist immer gut, zu überprüfen, ob man dasselbe Problem hat. Zeit, tiefer einzutauchen.

In diesem Fall ist sie in der Funktion meta_form enthalten, die sich in `wp-admin/includes/template.php` befindet. Gleich am Anfang dieser Funktion sehen wir unsere berüchtigte Abfrage:

	$limit = apply_filters( 'postmeta_form_limit', 30 );
	$sql = "SELECT meta_key
		FROM $wpdb->postmeta
		GROUP BY meta_key
		HAVING meta_key NOT LIKE %s
		ORDER BY meta_key
		LIMIT %d";
	$keys = $wpdb->get_col( $wpdb->prepare( $sql, $wpdb->esc_like( '_' ) . '%', $limit ) );

Die Variable $keys speichert die Ergebnisse unserer langsamen Abfrage. Weiter unten in der Funktion meta_form sehen wir, wo $keys verwendet werden:

foreach ( $keys as $key ) {
	if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'add_post_meta', $post->ID, $key ) )
		continue;
	echo "\n<option value='" . esc_attr($key) . "'>" . esc_html($key) . "</option>";
}

Diese Schlüssel sind meta_keys in der Tabelle wp_postmeta und sie werden verwendet, um Optionen für ein <select> im Meta-Box "Benutzerdefinierte Felder" auf dem Beitragsbearbeitungsbildschirm zu befüllen.

Custom Fields Select

Warum ist es so langsam?

Um eine bessere Vorstellung davon zu bekommen, warum diese Abfrage langsam ist, habe ich phpMyAdmin verwendet, um eine EXPLAIN-Abfrage für die langsame SQL-Abfrage auszuführen. EXPLAIN gibt uns Einblicke, wie MySQL eine Abfrage ausführt. Bei komplizierten Abfragen kann EXPLAIN helfen, langsame Punkte in Ihrem SQL zu finden – manchmal ist es eine langsame Unterabfrage oder eine ineffiziente Operation, die Ihre Abfrage beeinträchtigt.

Selbst bei einfachen Abfragen wie unserer obigen Meta-SQL gibt EXPLAIN immer noch einen kleinen Einblick, um zu verstehen, was vor sich geht.

EXPLAIN of meta box query

Die wichtigen Teile, die für diese Abfrage betrachtet werden müssen, sind:

  1. Die Anzahl der Zeilen (über 1 Million)
  2. Die Spalte "Extra", die uns die mehrdeutige Phrase "Using filesort" liefert.

"Using filesort" (#2) ist tatsächlich eine große Sache, denn es bedeutet, dass die Zeilen jedes Mal sortiert werden, wenn diese Abfrage ausgeführt wird, was eine kostspielige Operation ist.

Was können wir also dagegen tun? Dank WordPress Hooks können wir den langsamen Code durch eigenen ersetzen!

Ersetzen der Meta-Box

Jedes Mal, wenn Sie Änderungen am WordPress-Kern vornehmen, sollten Sie prüfen, ob es Aktionen oder Filter gibt, in die Sie eingreifen können, um Ihre Änderungen vorzunehmen. Hooks sind eine nicht-intrusive Methode, um Änderungen an Code vorzunehmen, der Ihnen nicht gehört.

Leider finden Sie nicht immer einen Hook, um das zu tun, was Sie suchen – insbesondere in älteren Teilen von WordPress. Eine Suche in `wp-admin/includes/meta-boxes.php` und `wp-admin/includes/template.php` (wo sich unser langsamer Code befindet) ergab keine Aktionen oder Filter, in die wir uns einklinken könnten.

Wenn wir uns nicht in bestehenden Meta-Box-Code einklinken können, haben wir immer noch eine Option: Wir können die Meta-Box mit einer eigenen *ersetzen*.

Der erste Schritt ist, die Kern-Meta-Box zu entfernen und WordPress über unsere neue Meta-Box zu informieren. Dies können wir mit dem add_meta_boxes Hook tun:

function admin_speedup_remove_post_meta_box() {
	global $post_type;

	if ( is_admin() && post_type_supports( $post_type, 'custom-fields' ) ) {
		remove_meta_box( 'postcustom', 'post', 'normal' );
		add_meta_box( 'admin-speedup-postcustom', __('Admin Speedup Custom Fields'), 'admin_speedup_post_custom_meta_box', null, 'normal', 'core' );
	}
}

add_action( 'add_meta_boxes', 'admin_speedup_remove_post_meta_box' );

Der Aufruf von remove_meta_box sagt WordPress: "Hey, kümmere dich nicht um diese 'postcustom'-Meta-Box. Wir kümmern uns darum."

Nachdem die Kern-Meta-Box entfernt wurde, sagt der Aufruf von add_meta_box WordPress, dass die Funktion admin_speedup_post_custom_meta_box zum Generieren des HTML für unsere neue Meta-Box verwendet werden soll.

admin_speedup_post_custom_meta_box ist im Grunde eine Kopie der Funktion post_custom_meta_box aus dem WordPress-Kern mit einigen Änderungen; am bemerkenswertesten ist, dass wir den Aufruf von meta_form durch unsere eigene benutzerdefinierte Funktion admin_speedup_meta_form ersetzen.

Große Warnung vor Copy + Paste: Codeblöcke durch *nahezu identischen* Code zu ersetzen, ist nicht ideal; wenn sich etwas im Originalcode ändert, müssen wir sicherstellen, dass wir unsere Version desselben Codes aktualisieren.

Aber in diesem Fall ist die Meta-Box "Benutzerdefinierte Felder":

  1. Wahrscheinlich wird sie sich nicht viel ändern.
  2. Keine kritische Funktion – selbst wenn sie bei der Aktualisierung von WordPress kurzzeitig ausfallen würde, würde die Website nicht zusammenbrechen.

Angesichts der minimalen Risiken und der Tatsache, dass wir über 2 Sekunden von der Seitenladezeit entfernen können, hielt ich es für vertretbar, die Kern-Meta-Box auszutauschen.

Schreiben des HTML der neuen Meta-Box

Nachdem wir uns vom moralischen Argument für den Austausch einer Kern-Meta-Box überzeugt haben, können wir die Funktionen schreiben, die unsere mutierte Version der "Benutzerdefinierten Felder"-Box anzeigen.

Zuerst definieren wir admin_speedup_post_custom_meta_box, die Funktion, die WordPress zum Schreiben des HTML für unsere neue Meta-Box verwendet:

function admin_speedup_post_custom_meta_box( $post ) {
?>
<div id="postcustom">
<div id="postcustomstuff">
<div id="ajax-response"></div>
<?php
$metadata = has_meta($post->ID);
foreach ( $metadata as $key => $value ) {
	if ( is_protected_meta( $metadata[ $key ][ 'meta_key' ], 'post' ) || ! current_user_can( 'edit_post_meta', $post->ID, $metadata[ $key ][ 'meta_key' ] ) )
		unset( $metadata[ $key ] );
}
list_meta( $metadata );
admin_speedup_meta_form( $post ); ?>
<p><?php _e('Custom fields can be used to add extra metadata to a post that you can use in your theme.'); ?></p>
</div>
</div>
<?php
}

Der Großteil des obigen Codes ist aus dem WordPress-Kern kopiert und eingefügt (Originalcode hier). Die 2 Unterschiede in unserem Code:

  1. Wir haben den Aufruf von meta_form durch unsere eigene Funktion admin_speedup_meta_form ersetzt, die wir gleich definieren werden.
  2. Wir haben ein zusätzliches Wrapper-Div mit der ID postcustom hinzugefügt – diese ID wird von einigen JavaScripts erwartet, die der Meta-Box AJAX-Funktionalität verleihen, und es ist viel einfacher, die Dinge so *nah wie möglich* am Original-Markup zu halten, um nichts kaputt zu machen.

Behebung der langsamen Abfrage

Endlich sind wir an dem Punkt, an dem wir diesen langsamen Störenfried beheben können. Die SQL für unsere meta_key SQL befindet sich in der Funktion meta_form in `wp-admin/includes/template.php`. Wir werden dies in unserer Funktion admin_speedup_meta_form lösen, die *im Grunde* dasselbe ist wie die ursprüngliche Funktion meta_form mit einer wichtigen Änderung.

Hier ist unser neuer Code:

function admin_speedup_meta_form( $post = null ) {
	global $wpdb;
	$post = get_post( $post );

	if ( false === ( $keys = get_transient( 'admin_speedup_meta_keys' ) ) ) {
		$limit = apply_filters( 'postmeta_form_limit', 30 );
		$sql = "SELECT meta_key
			FROM $wpdb->postmeta
			GROUP BY meta_key
			HAVING meta_key NOT LIKE %s
			ORDER BY meta_key
			LIMIT %d";
		$keys = $wpdb->get_col( $wpdb->prepare( $sql, $wpdb->esc_like( '_' ) . '%', $limit ) );

		set_transient( 'admin_speedup_meta_keys', $keys, 60 * 60 );
	}

	if ( $keys ) {
		natcasesort( $keys );
		$meta_key_input_id = 'metakeyselect';
	} else {
		$meta_key_input_id = 'metakeyinput';
	}
?>
<p><?php _e( 'Add New Custom Field:' ) ?></p>
<?php // ...snip a bunch of duplicated code... ?>
</tbody>
</table>
<?php

}

Die "wichtigen Änderungen" sind die Aufrufe von get_transient und set_transient. Wenn Sie nicht wissen, was ein "Transient" ist, hat CSS-Tricks kürzlich einen hilfreichen Artikel veröffentlicht, der die Nuancen von WordPress Transients erklärt.

Für die Zwecke dieses Artikels sind Transients einfach eine Möglichkeit, die Ergebnisse eines aufwändigen Code-Teils zu speichern, damit sie später nicht neu generiert werden müssen. In diesem Fall speichern wir die Ergebnisse einer aufwändigen SQL-Abfrage mit dem Transient-Schlüssel admin_speedup_meta_keys. Wir prüfen zuerst, ob WordPress den Wert bereits für uns gespeichert hat, indem wir get_transient verwenden. Wenn ja, können wir die SQL-Abfrage überspringen und normal fortfahren. Wenn nicht, führen wir die langsame SQL-Abfrage aus und speichern die Ergebnisse für eine Stunde (60 * 60 Sekunden).

Mit dem Transients-Code in der Hand wird unsere langsame SQL-Abfrage nur etwa einmal pro Stunde ausgeführt – viel besser als bei jeder Anfrage!

Mit unserem Code zeigt die Abfrageüberwachung, dass die langsame Abfrage nicht bei jedem Seitenaufruf stattfindet:

Faster Edit Screen

Erfolg! Es bleibt nur noch ein Problem: Angenommen, jemand fügt einen neuen meta_key zur Liste hinzu. Unsere als Transient gespeicherte Liste wäre etwa 1 Stunde lang *ungültig*. Wir brauchen eine Möglichkeit, die gecachten Abfrageergebnisse zu löschen.

Hinzufügen des Buttons "Meta-Schlüssel löschen"

Es gibt nur zwei schwierige Dinge in der Informatik: Cache-Invalidierung und Benennung von Dingen.

— Phil Karlton
Quelle

Zu wissen, wann ein gecachter Wert ungültig ist, ist ein schwieriges Problem. Es gibt viele Möglichkeiten, wie die Liste der meta_keys geändert oder hinzugefügt werden kann, z. B. wenn Plugins automatisch Schlüssel hinzufügen oder Admin-Benutzer sie manuell eingeben.

Wenn ich auf ein schwieriges Problem stoße, versuche ich, die faulste Sache zu tun: Sie in Ruhe lassen.

Meta-Boxen werden nur im WP-Admin verwendet, und das Ergebnis dieser speziellen Abfrage ändert sich nicht oft. In diesem Fall habe ich beschlossen, einfach einen Button hinzuzufügen, um den gecachten (Transient) Wert zu löschen, damit ein Admin-Benutzer die Liste der meta_keys einfach neu generieren kann, wenn er feststellt, dass sie veraltet ist. Es ist nicht elegant, aber es funktioniert und ist einfacher, als zu versuchen, den Cache zu invalidieren.

Um den Button hinzuzufügen, benötigen wir 2 Codezeilen:

  1. HTML für den Button in der Meta-Box
  2. Code im Backend zum Löschen des Transient-Werts

Zuerst habe ich den Button zur neu erstellten Funktion admin_speedup_post_custom_meta_box hinzugefügt:

function admin_speedup_post_custom_meta_box($post) {
?>
<div id="postcustom">
<div id="postcustomstuff">
<div id="ajax-response"></div>
<?php
$metadata = has_meta($post->ID);
foreach ( $metadata as $key => $value ) {
	if ( is_protected_meta( $metadata[ $key ][ 'meta_key' ], 'post' ) || ! current_user_can( 'edit_post_meta', $post->ID, $metadata[ $key ][ 'meta_key' ] ) )
		unset( $metadata[ $key ] );
}
list_meta( $metadata );
admin_speedup_meta_form( $post ); ?>
<?php // Here's the new lines: ?>
<?php $current_url = add_query_arg( 'admin_speedup_refresh_meta_keys', '1' ); ?>
<div style="padding: 20px; margin: 20px 0; background: #CCC">
	
</div>
<p><?php _e('Custom fields can be used to add extra metadata to a post that you can use in your theme.'); ?></p>
</div>
</div>
<?php
}

Dieser Code fügt einen neuen Link zu unserer neuen Meta-Box hinzu, der wie folgt aussieht:

Refresh Meta Keys Link

Fügen Sie nun Code hinzu, um das eigentliche Aktualisieren der Meta-Schlüssel zu handhaben:

function admin_speedup_clear_meta_keys() {
	if ( is_admin() && isset( $_GET['admin_speedup_refresh_meta_keys'] ) && wp_verify_nonce( $_GET['_admin_speedup_nonce'], 'admin_speedup_refresh_meta_keys' ) ) {
		delete_transient( 'admin_speedup_meta_keys' );
	}
}

add_action( 'admin_init', 'admin_speedup_clear_meta_keys' );

Wenn nun auf den Link "Meta-Schlüssel aktualisieren" geklickt wird, geschieht Folgendes:

  1. Ein Nonce wird überprüft, um CSRF zu verhindern (mehr dazu hier).
  2. Wenn die Nonce-Prüfung erfolgreich ist, wird der Transient-Wert über delete_transient gelöscht.
  3. Nachdem der Transient gelöscht wurde, wird die (langsame) SQL-Abfrage erneut ausgeführt und die Liste der meta_keys ist aktuell.

Zusammenfassung dessen, was wir getan haben

Wir fanden ein langsames SQL, das in einer der Kern-Meta-Boxen von WordPress lebte. Nachdenken über unsere Optionen zur Behebung langsamer SQL-Abfragen entschieden wir uns, die Ergebnisse der Abfrage einfach zu cachen, um es einfach zu halten.

Dafür mussten wir die Kern-Meta-Box ausklinken und durch eine nahezu identische benutzerdefinierte Meta-Box ersetzen, die die Ergebnisse der langsamen Abfrage cach te. Schließlich haben wir, um zu verhindern, dass die gecachten Ergebnisse veraltet sind, eine Möglichkeit für Admin-Benutzer hinzugefügt, den Cache zu löschen.

Die Ergebnisse: 1,5 Sekunden weniger Ladezeit für die typische Beitragsbearbeitungsseite, was eine spürbare Verbesserung darstellt.