使用勾點 option_active_plugin
來程式化設定需要載入的外掛,藉此在某些特定情境下減少外掛的使用以提升頁面讀取速度,提供給第三方服務呼叫使用的 REST API 就非常適合使用。
自從改變接案模式後,跟每個案子相處的時間都已經超過一年半載,時間越長就會碰到以往短期專案遇不到的狀況,像是新需求的開發、外掛的相容性整合以及除蟲作業,其中最容易遇到的就是效能問題,尤其是在網站經營數年後這類問題會逐漸浮現。
WordPress REST API 回應速度遲緩
客戶的使用情境是採用了第三方服務來做客戶關係管理,該服務可以整合對話機器人以便根據顧客對話結果與屬性去做相對應的留言互動,由於要讓機器人互動有專屬的客製化訊息,因此需要從網站這邊取得資料後進行傳送。
流程是第三方服務在接收到顧客的訊息後,會以 Webhook 的方式呼叫站內 REST API 來取得機器人的回覆訊息,這內文是給機器人閱讀的格式以將其轉換成顧客看得懂的圖文按鈕,因此為了要讓顧客有對話的感覺,互動過程一定要很順暢。
但很可能是網站經營久了用了不少的外掛,或是資料庫肥大而造成第三方服務請求站內 REST API 的時候回應速度很慢,這就像你在跟人聊天時你講完一句話對方過了三秒才回覆你一樣,會讓整個互動體驗變得很差。
提升 WordPress REST API 請求速度的方法
為了解決這個問題,我依序嘗試了幾種方法。首先,最根本的解法就是透過網站效能分析工具來查看每一次 API 的請求經過哪些步驟與資料庫查詢,從中找尋效能瓶頸,我習慣用的是外掛 Query Monitor 以及網站分析服務 New Relic 的 APM。
追查結果矛頭都指向資料庫 wp_options
以及 wp_postmeta
,這兩張表格花了非常多的時間在進行查詢,因為所有外掛的設定功能都會記錄在 wp_options
,而文章、頁面的自訂欄位都寫在 wp_postmeta
,也因此長年經營的 WordPress 網站這兩個表格一定都會很肥。
一、資料庫表格索引
要把這兩張表瘦身除了移除掉不需要的紀錄以外,還可以利用資料庫的索引功能來增加讀取速度,資料索引的邏輯可以想像成是你到大賣場去找一件商品,你不用在每一個貨架裡面做地毯式搜索,而是先抬頭看看標誌找大類別,或是詢問賣場人員問你需要的東西可能會在哪一個區塊,而這些尋找商品的輔助就是一種索引。
因此當資料庫設定索引後就不用從表格中逐一搜尋,只要有索引表就能在擁有大量資料的表格中快速找到對應的資料,但索引也不是無敵的,當資料量沒有很多的時候反而會有反效果,一般來說會在我們發現資料庫讀取緩慢時使用。
設定 wp_options
表格索引可以參考這篇教學:
https://silicondales.com/tutorials/wordpress/big-wordpress-problem-slow-wp-admin-uncached-pageloads-slow/#Method_2_-_Add_an_Index_to_wp_options_database_on_autoload
害怕輸入 SQL 也有現成外掛可以使用:
https://wordpress.org/plugins/index-wp-mysql-for-speed/https://wordpress.org/plugins/index-wp-mysql-for-speed/
MySQL 索引原理介紹:
https://www.jyt0532.com/2021/01/30/index/
二、建立 REST API 快取
一般的頁面可以使用快取來增加讀取速度,那麼請求 REST API 也可以有快取嗎?當然可以,快取是我第二個考量的解決方案,因為比起慢慢整理資料庫使用快取很快就能讓客戶看到效果,在研究如何把 REST API 加入快取的同時也發現到有這樣的外掛:
WP REST Cache 這支外掛可以把 WordPress 內建的 API 都快取起來,同時也支援自定義文章與分類,還能手動清除快取以及查詢已快取的資料,如果只是單純要快取 Post 相關的 API 就十分方便,但實測之後發現它無法解決我的問題。
第一個問題在於它沒有支援自定義 API 請求路徑,也就是說它只吃 wp-json/wp/v2/posts
路徑下的回傳資料,由於整合第三方服務必須要自行設計 API 路徑與回傳內容,要讓這支外掛也能支援的話勢必要額外花時間處理了。
其次是該外掛只支援請求方法為 GET 的存取,如果今天你的 API 是使用 POST 或是 PUT 來進行呼叫的話會無法保存快取,通常除了 GET 以外其他的方法都不能使用快取,不然會造成資料無法更新的問題,綜合以上因素快取自定義 API 這條路對我來說行不通。
三、停用不需要的外掛
WordPress 最強大的部分無疑是外掛,但並非每個頁面都需要載入所有的外掛,如何讓 REST API 請求只載入必要外掛成為我另一個思考方向,想不到研究了一下還真的有人跟我想得一樣,這是由 Delicious Media 所提出的解決方案:
https://www.deliciousmedia.co.uk/journal/wordpress-rest-api-performance/
這方法很聰明,他們寫了一支外掛然後裝在 mu-plugins
資料夾裡面,之所以要放在這邊是因為這個資料夾中的外掛會最先啟用,這樣就能及早讀取到該外掛以修改外掛的啟用設定,非常適合 REST API 請求的使用場景。
這支外掛的程式碼短短 44 行就能搞定:
<?php
/**
* Plugin Name: QuickREST
* Plugin URI: https://www.deliciousmedia.co.uk/
* Description: Speed up REST API requests by selective loading of plugins.
* Version: 2.1.0
* Author: Delicious Media Limited
* Author URI: https://www.deliciousmedia.co.uk/
* Text Domain: dm-quickrest
* License: GPLv3 or later
*
* @package dm-quickrest
*/
/**
* Filters the active plugins for this request.
*
* @param array $plugins Activated WordPress plugins.
*
* @return array
*/
function quickrest_filter_plugins( $plugins ) {
if ( ! isset( $_SERVER['REQUEST_URI'] ) || false === strpos( stripcslashes( $_SERVER['REQUEST_URI'] ), rest_get_url_prefix() ) ) {
return $plugins;
}
$plugin_whitelist = apply_filters( 'quickrest_plugin_map', [ '_default' => [] ] );
$url_parts = explode( '/', trailingslashit( $_SERVER['REQUEST_URI'] ) );
if ( ! isset( $url_parts[2] ) ) {
return $plugins;
}
$plugins_allowed = isset( $plugin_whitelist[ $url_parts[2] ] ) ? $plugin_whitelist[ $url_parts[2] ] : $plugin_whitelist['_default'];
return array_intersect( $plugins, (array) $plugins_allowed );
}
add_filter( 'option_active_plugins', 'quickrest_filter_plugins', PHP_INT_MAX - 1, 1 );
該外掛使用了勾點 option_active_plugins
來修改啟用中的外掛,帶有一個陣列參數也就是啟用外掛的清單,只要修改這個陣列就能決定需要載入哪些外掛,修改的方式是透過這支外掛提供的勾點 quickrest_plugin_map
來處理。
假設我們自己設計的 API 路徑為 wp-json/myapi/v1/post
,只要在自己的外掛中加入以下程式碼來修改需要啟用的外掛即可:
function my_plugin_map_function( $map ) {
$new_map = array(
'myapi' => array(
'advanced-custom-fields-pro/acf.php',
'woocommerce/woocommerce.php',
),
);
return array_merge_recursive( $map, $new_map );
}
add_filter( 'quickrest_plugin_map', 'my_plugin_map_function', 15, 1 );
$new_map
這個陣列就是指定當請求 wp-json/myapi
的時候會啟用的外掛,陣列的 key 是 API 的命名空間,value 則是另外一個陣列,以「外掛資料夾/外掛主檔案」的形式來指定,所以範例中代表的是當請求 wp-json/myapi/v1/post
的時候,只會載入 ACF Pro 以及 WooCommerce 兩支外掛,其他沒指定的就不會載入。
但在我實測後發現如果是以勾點 quickrest_plugin_map
來指定啟用外掛並不會生效,請求 API 的結果都顯示無法找到路由,推測原因可能是我的外掛載入順序的問題,因此最後我還是直接把判斷寫在 QuickRest 外掛本體裡面,修改結果如下:
function quickrest_filter_plugins( $plugins ) {
// 1.檢查請求路徑是否帶有 wp-json.
if ( ! isset( $_SERVER['REQUEST_URI'] ) || false === strpos( stripcslashes( $_SERVER['REQUEST_URI'] ), rest_get_url_prefix() ) ) {
return $plugins;
}
// 2.取得 API 的命名空間.
$url_parts = explode( '/', trailingslashit( $_SERVER['REQUEST_URI'] ) );
// 3.如果不存在 API 的命名空間則直接回傳原本的外掛列表.
if ( ! isset( $url_parts[2] ) ) {
return $plugins;
}
// 4.如果 API 命名空間為 myapi,則回傳需要啟用的外掛.
if ( 'myapi' === $url_parts[2] ) {
return array_intersect( $plugins, array( 'advanced-custom-fields-pro/acf.php', 'woocommerce/woocommerce.php' ) );
}
return $plugins;
}
add_filter( 'option_active_plugins', 'quickrest_filter_plugins', PHP_INT_MAX - 1, 1 );
首先先判斷請求的網址是否帶有 wp-json
關鍵字,有的話就代表這次的請求是呼叫 API,並且取得 API 的命名空間,當命名空間為 myapi 時,則取得所有外掛與所需外掛的交集,之所以使用 array_intersect
取得陣列交集是確保指定的外掛已安裝在網站內,以避免啟用到沒有安裝的外掛。
當請求 wp-json/myapi/v1/post
就能明顯感受到速度的提升,像回到一開始剛裝完 WordPress 還沒有任何資料的順暢感,但這樣的作法還是有風險,你必須要 100% 掌控你的 API 有用到哪些外掛,當 API 整合新外掛的功能時也要記得把該外掛更新到啟用列表中,不然接手的人應該會除錯除到瘋掉…
結語
要提升 REST API 的請求速度有很多種作法,具體要採用哪種作法需視網站經營的實際情況來做判斷,看是要先治標撐過這次行銷活動,還是要規劃足夠的時間來徹底治本,這都需要與站長仔細評估後再做出因應的解決之道。
你曾經遇過這一類的問題嗎?你是如何解決的?歡迎在下方留言交流吧!