替换WordPress核心元框以加快编辑速度

Avatar of Andy Adams
Andy Adams 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

CSS-Tricks 的前端通常非常快,因为大多数页面都已缓存(并且在请求时无需动态生成)。但是,直到最近,CSS-Tricks 的 WordPress 后台管理界面还没有那么幸运。

特别是,文章编辑屏幕很慢。慢得令人痛苦。保存草稿需要几秒钟,这足以打断您在撰写文章过程中的思路。

根据 Pete Sorensen 的提示,他在 WordPress StackExchange 上发布了一个关于 性能问题的帖子(并且很友好地告诉了我们),我开始着手找出是什么导致文章编辑页面如此缓慢。

准备工具

在我们之前关于 修复缓慢的 WordPress 查询 的文章中,我们回顾了一些可用于识别缓慢 SQL 的工具。为了解决此特定问题,我使用了 Debug Bar,并确保 SAVEQUERIES 已启用

查明减速原因

在跟踪 SQL 查询后,我打开了 Debug Bar 界面(使用右上角管理栏中的按钮)并切换到“查询”选项卡。我看到了以下内容:

Slow Query Time

这里有些不对劲!数据库中任何超过 250 毫秒的操作都令人担忧;超过 1.5 秒执行 SQL 查询会让我感到心痛。

作为参考,以下是在其他后台管理页面上的查询负载情况:

Faster Query Time

我扫描了查询列表,看看是否有任何明显错误。我发现了一个可疑对象:

A Really Slow Query

对于单个查询来说,2000 多毫秒是一个坏消息。WordPress,你为什么要这样做呢?

你的功能是什么,它做了什么?

我需要查询的一些上下文信息。Pete Sorensen 的提示给了我一个很好的想法,它来自哪里,但始终需要验证是否存在相同的问题。是时候深入研究了。

在这种情况下,它位于 meta_form 函数中,该函数位于 `wp-admin/includes/template.php` 中。在该函数的顶部附近,我们看到了我们臭名昭著的查询:

	$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 ) );

变量 $keys 存储着我们缓慢查询的结果。在 meta_form 函数中进一步查看,我们可以看到 $keys 在哪里使用:

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>";
}

这些键是 wp_postmeta 表中的 meta_keys,它们用于填充文章编辑屏幕上“自定义字段”元框中的 <select> 选项。

Custom Fields Select

为什么这么慢?

为了更好地了解为什么此查询如此缓慢,我使用了 phpMyAdmin 对缓慢的 SQL 运行了一个 EXPLAIN 查询。EXPLAIN 使我们能够深入了解 MySQL 如何执行查询。对于复杂的查询,EXPLAIN 可以帮助您查明 SQL 中的缓慢点——有时是缓慢的子查询或低效的操作导致查询出错。

即使对于像我们上面提到的元数据 SQL 这样的简单查询,EXPLAIN 仍然可以提供一些信息来帮助我们了解发生了什么。

EXPLAIN of meta box query

对于此查询,需要查看的重要部分是:

  1. 行数(超过 100 万)
  2. “Extra”列,它为我们提供了模棱两可的短语“Using filesort”

使用 filesort(#2)实际上是一个大问题,因为它意味着每次运行此查询时都会对行进行排序,这是一个代价高昂的操作。

那么我们该怎么办呢?感谢 WordPress 钩子,我们可以用我们自己的代码替换缓慢的代码!

替换元框

在对 WordPress 核心进行任何更改时,都应该查看是否有任何可以挂接的动作或过滤器来进行更改。钩子是一种非侵入性的方式,可以对不属于您的代码进行更改。

不幸的是,您并非总能找到可以执行所需操作的钩子——尤其是在 WordPress 的较旧部分中。搜索 `wp-admin/includes/meta-boxes.php` 和 `wp-admin/includes/template.php`(我们的缓慢代码所在的位置)没有找到我们可以挂接的动作或过滤器。

当我们无法挂接到现有的元框代码时,我们仍然有一个选择:我们可以用我们自己的元框替换它。

第一步是删除核心元框并告诉 WordPress 我们新的元框。我们可以使用 add_meta_boxes 钩子来做到这一点:

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' );

remove_meta_box 的调用告诉 WordPress“嘿,别担心那个‘postcustom’元框。我们会处理它。”

删除核心元框后,对 add_meta_box 的调用告诉 WordPress 使用 admin_speedup_post_custom_meta_box 函数为我们的新元框生成 HTML。

admin_speedup_post_custom_meta_box 基本上是 WordPress 核心 post_custom_meta_box 函数的副本,并进行了一些更改;最值得注意的是,我们用我们自己的自定义函数 admin_speedup_meta_form 替换了对 meta_form 的调用。

大规模复制粘贴警告:几乎相同的代码替换代码块并不是理想的做法;如果原始代码发生更改,我们必须确保更新我们版本中的相同代码。

但在这种情况下,自定义字段元框是:

  1. 不太可能发生重大变化
  2. 不是关键功能——即使在更新 WordPress 时它暂时失效,网站也不会崩溃

鉴于涉及的风险最小,并且考虑到我们可以从页面加载中节省 2 秒以上的时间,我认为更换核心元框是合理的。

编写新的元框 HTML

既然我们已经说服了自己替换核心元框的道德论证,我们就可以编写显示自定义字段框的变异版本的函数了。

首先,我们将定义 admin_speedup_post_custom_meta_box,这是 WordPress 用于编写新元框 HTML 的函数:

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
}

以上大部分代码都是从 WordPress 核心复制粘贴的(在此处查看原始代码)。我们代码中的 2 个区别:

  1. 我们用我们自己的函数 admin_speedup_meta_form 替换了对 meta_form 的调用,我们将在稍后定义该函数。
  2. 我们添加了一个带有 ID postcustom 的额外包装 div——此 ID 由提供元框 AJAX 功能的一些 JavaScript 期望,并且尽可能保持标记与原始标记一致要容易得多,以免破坏某些内容。

修复缓慢的查询

最后,我们到了可以修复这个缓慢的错误的地方。我们的 meta_key SQL 的 SQL 位于 meta_form 函数中 在 `wp-admin/includes/template.php` 中。我们将在 admin_speedup_meta_form 函数中解决此问题,该函数基本上与原始 meta_form 函数相同,但有一个重要更改。

这是我们的新代码:

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

}

“重要更改”是对 get_transientset_transient 的调用。如果您不知道什么是“瞬态”,CSS-Tricks 最近发布了一篇 方便的文章,解释了 WordPress 瞬态的细微差别

本文的目的在于,将瞬时数据(transients)简单地理解为一种存储代价高昂的代码执行结果的方式,从而避免后续重复计算。在本例中,我们使用瞬时数据键 admin_speedup_meta_keys 存储代价高昂的 SQL 查询结果。首先,我们使用 get_transient 检查 WordPress 是否已经为我们存储了该值。如果是,则可以跳过 SQL 查询并继续执行;否则,执行缓慢的 SQL 查询并将结果存储一个小时(60 * 60 秒)。

使用瞬时数据代码后,我们的缓慢 SQL 查询大约每小时只执行一次,这比每次请求都执行要好得多!

代码就位后,Query Monitor 显示缓慢查询不再发生在每个页面加载时。

Faster Edit Screen

成功!只剩下一个问题:假设有人向列表中添加了一个新的 meta_key。我们作为瞬时数据存储的列表将在大约 1 小时内失效。我们需要一种方法来清除缓存的查询结果。

添加“清除元键”按钮

计算机科学里只有两件难事:缓存失效和命名。

— Phil Karlton
来源

确定缓存值何时失效是一个难题。meta_keys 列表可以通过多种方式进行更改或添加,包括插件自动添加键或管理员手动输入。

每当我遇到难题时,我都会尝试做最懒惰的事情:放任不管。

元框仅在 WP 后台使用,并且此特定查询结果不会经常更改。因此,在本例中,我决定只添加一个按钮来清除缓存(瞬时数据)值,以便管理员用户在发现 meta_keys 列表已过期时可以轻松地重新生成它。这种方法不优雅,但有效且比尝试使缓存失效更简单。

要添加按钮,我们需要两部分代码。

  1. 元框中按钮的 HTML 代码
  2. 后端清除瞬时数据值的代码

首先,我将按钮添加到新创建的 admin_speedup_post_custom_meta_box 函数中。

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
}

此代码将一个新链接添加到我们的新元框中,如下所示。

Refresh Meta Keys Link

现在,添加一些代码来处理元键的实际刷新。

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' );

现在,当单击“刷新元键”链接时,会发生以下情况:

  1. 检查 nonce 以防止 CSRF(此处了解更多信息)。
  2. 如果 nonce 检查通过,则通过 delete_transient 删除瞬时数据值。
  3. 删除瞬时数据后,将重新执行(缓慢的)SQL 查询,并且 meta_keys 列表将是最新的。

回顾我们做了什么

我们发现 WordPress 核心元框之一存在一个缓慢的 SQL 查询。在考虑了修复缓慢 SQL 的选项后,我们决定简单地缓存查询结果以保持简单。

为此,我们必须取消挂钩并用一个几乎相同的自定义元框替换核心元框,该元框缓存了缓慢查询的结果。最后,为了防止缓存结果过时,我们添加了一种方法供管理员用户清除缓存。

结果:典型“编辑文章”页面加载时间缩短了 1.5 秒,这是一个明显的改进。