CSS-Tricks 的前端通常非常快,因为大多数页面都已缓存(并且在请求时无需动态生成)。但是,直到最近,CSS-Tricks 的 WordPress 后台管理界面还没有那么幸运。
特别是,文章编辑屏幕很慢。慢得令人痛苦。保存草稿需要几秒钟,这足以打断您在撰写文章过程中的思路。
根据 Pete Sorensen 的提示,他在 WordPress StackExchange 上发布了一个关于 性能问题的帖子(并且很友好地告诉了我们),我开始着手找出是什么导致文章编辑页面如此缓慢。
准备工具
在我们之前关于 修复缓慢的 WordPress 查询 的文章中,我们回顾了一些可用于识别缓慢 SQL 的工具。为了解决此特定问题,我使用了 Debug Bar,并确保 SAVEQUERIES
已启用。
查明减速原因
在跟踪 SQL 查询后,我打开了 Debug Bar 界面(使用右上角管理栏中的按钮)并切换到“查询”选项卡。我看到了以下内容:

这里有些不对劲!数据库中任何超过 250 毫秒的操作都令人担忧;超过 1.5 秒执行 SQL 查询会让我感到心痛。
作为参考,以下是在其他后台管理页面上的查询负载情况:

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

对于单个查询来说,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>
选项。

为什么这么慢?
为了更好地了解为什么此查询如此缓慢,我使用了 phpMyAdmin 对缓慢的 SQL 运行了一个 EXPLAIN
查询。EXPLAIN
使我们能够深入了解 MySQL 如何执行查询。对于复杂的查询,EXPLAIN
可以帮助您查明 SQL 中的缓慢点——有时是缓慢的子查询或低效的操作导致查询出错。
即使对于像我们上面提到的元数据 SQL 这样的简单查询,EXPLAIN 仍然可以提供一些信息来帮助我们了解发生了什么。

对于此查询,需要查看的重要部分是:
- 行数(超过 100 万)
- “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
的调用。
大规模复制粘贴警告:用几乎相同的代码替换代码块并不是理想的做法;如果原始代码发生更改,我们必须确保更新我们版本中的相同代码。
但在这种情况下,自定义字段元框是:
- 不太可能发生重大变化
- 不是关键功能——即使在更新 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 个区别:
- 我们用我们自己的函数
admin_speedup_meta_form
替换了对meta_form
的调用,我们将在稍后定义该函数。 - 我们添加了一个带有 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_transient
和 set_transient
的调用。如果您不知道什么是“瞬态”,CSS-Tricks 最近发布了一篇 方便的文章,解释了 WordPress 瞬态的细微差别。
本文的目的在于,将瞬时数据(transients)简单地理解为一种存储代价高昂的代码执行结果的方式,从而避免后续重复计算。在本例中,我们使用瞬时数据键 admin_speedup_meta_keys
存储代价高昂的 SQL 查询结果。首先,我们使用 get_transient
检查 WordPress 是否已经为我们存储了该值。如果是,则可以跳过 SQL 查询并继续执行;否则,执行缓慢的 SQL 查询并将结果存储一个小时(60 * 60 秒)。
使用瞬时数据代码后,我们的缓慢 SQL 查询大约每小时只执行一次,这比每次请求都执行要好得多!
代码就位后,Query Monitor 显示缓慢查询不再发生在每个页面加载时。

成功!只剩下一个问题:假设有人向列表中添加了一个新的 meta_key
。我们作为瞬时数据存储的列表将在大约 1 小时内失效。我们需要一种方法来清除缓存的查询结果。
添加“清除元键”按钮
计算机科学里只有两件难事:缓存失效和命名。
— Phil Karlton
来源
确定缓存值何时失效是一个难题。meta_keys
列表可以通过多种方式进行更改或添加,包括插件自动添加键或管理员手动输入。
每当我遇到难题时,我都会尝试做最懒惰的事情:放任不管。
元框仅在 WP 后台使用,并且此特定查询结果不会经常更改。因此,在本例中,我决定只添加一个按钮来清除缓存(瞬时数据)值,以便管理员用户在发现 meta_keys
列表已过期时可以轻松地重新生成它。这种方法不优雅,但有效且比尝试使缓存失效更简单。
要添加按钮,我们需要两部分代码。
- 元框中按钮的 HTML 代码
- 后端清除瞬时数据值的代码
首先,我将按钮添加到新创建的 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
}
此代码将一个新链接添加到我们的新元框中,如下所示。

现在,添加一些代码来处理元键的实际刷新。
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' );
现在,当单击“刷新元键”链接时,会发生以下情况:
- 检查 nonce 以防止 CSRF(此处了解更多信息)。
- 如果 nonce 检查通过,则通过
delete_transient
删除瞬时数据值。 - 删除瞬时数据后,将重新执行(缓慢的)SQL 查询,并且
meta_keys
列表将是最新的。
回顾我们做了什么
我们发现 WordPress 核心元框之一存在一个缓慢的 SQL 查询。在考虑了修复缓慢 SQL 的选项后,我们决定简单地缓存查询结果以保持简单。
为此,我们必须取消挂钩并用一个几乎相同的自定义元框替换核心元框,该元框缓存了缓慢查询的结果。最后,为了防止缓存结果过时,我们添加了一种方法供管理员用户清除缓存。
结果:典型“编辑文章”页面加载时间缩短了 1.5 秒,这是一个明显的改进。
由于这是一个(长期运行的)安装带有许多自定义元信息的问题,所以在遇到这种情况时了解这一点很有用。感谢您的工作和解释。
您能否将您的代码做成插件并在 github 上发布?
干杯!
这不能用 AJAX 来完成吗,类似于分类元框的组合方式?
最近我有一种感觉,WordPress 处理元数据的方式非常错误。
我的意思是,每次您修改常规文章(如果您使用 WP 做其他事情而不是博客,则总是会发生这种情况)时,都必须将所有数据放入 postmeta 表中。
结果是该表变得异常臃肿,根据我的经验,比任何其他表都要多得多。
我没有证据证明这一点,我也不是 php/mySQL 专家,但在我看来,它可能会损害长期的性能。
我很乐意听到来自真正专家的意见。这是否类似于一个问题?其他平台(Drupal、Joomla 等)如何处理文章元数据?
附注:我也无法忍受术语“表”——我的意思是,管理分类需要三个表?必须有更好的方法。
首先,将所有数据拉入元表是如何增加表臃肿的?
其次,分类将在未来几个版本中只使用 2 个表。他们在过去几个版本中一直在准备更改。
难怪该查询很慢。它执行了一个 GROUP BY/HAVING,但根本不需要 GROUP BY。大概是为了获取不同的值。但是,他们以可能最慢的方式执行它。
执行完全相同的功能,并且对数据库的负担更轻。它也比缓存和缓存失效等操作更容易修复。
看看 WordPress 官方开发者解释该查询的作用会很有趣;看看这个建议是否有效。
“刷新元键”按钮的另一种替代方法是在更新或删除文章元字段时删除瞬时数据。在这种情况下,瞬时数据可以存储超过一个小时。
我会用这个代替刷新!谢谢 :)
第二个想法是创建一个作为插件的解决方案。如果您在那里添加了正确的钩子,那么也许此代码可以进入核心…… :)
感谢这篇文章。
Patrick
您可以更进一步,添加 TLC 瞬时数据:https://github.com/markjaquith/WP-TLC-Transients
使用它,您可以告诉瞬时数据在后台更新。这样,当瞬时数据需要更新时,您无需等待它花费多长时间。它将显示当前的瞬时数据,并在下一个页面加载时显示新的瞬时数据(无需长时间加载)。
这似乎是许多网站可以从中受益的东西。您是否联系过 WordPress 团队,看看是否可以将其集成到核心?
wordpress 使其易于操作。
使用瞬时数据并不能真正解决问题。我的意思是……您可以优化查询,而不是创建自定义元框。
我在这里看到几个真正的解决方案。
优化查询。
删除查询和选择框。
完全删除元框。无论如何谁需要它?
有关可能的优化查询,请参阅https://css-tricks.org.cn/swapping-a-wordpress-core-meta-box-to-speed-up-editing/#comment-1595719。
我同意默认情况下删除该框,因为大多数使用元数据的人都是通过其他插件(例如 ACF)来使用的。
如果您不需要“自定义字段”元框,只需将此代码粘贴到您的 functions 文件中即可将其隐藏……
此外,这里有一个票据,您可能需要关注……
https://core.trac.wordpress.org/ticket/24498
……因为一旦它得到解决,这些解决方法将不再需要。
TL