Giáo trình ôn tập WordPress bài bản + Vibe Coding

Chương 4 – Performance, AJAX & REST API

Cỡ chữ: 16px
Font:
Nền:
← Chương Trước Chương Tiếp →

PHẦN A: PERFORMANCE

1. Transients API — Cache có thời hạn

Vấn đề cần giải quyết: Có những query/tính toán nặng (VD: gọi API ngoài, tính toán thống kê phức tạp) mà kết quả không cần realtime tuyệt đối — cache lại trong 1 khoảng thời gian sẽ giảm tải rất nhiều.

function get_expensive_data() {
    // Bước 1: kiểm tra cache đã có chưa
    $cached = get_transient( 'my_expensive_data' );
    if ( false !== $cached ) {
        return $cached;   // có cache → trả về ngay, không tính toán lại
    }

    // Bước 2: không có cache → tính toán / gọi API
    $data = expensive_calculation_or_api_call();

    // Bước 3: lưu cache, hết hạn sau 12 tiếng
    set_transient( 'my_expensive_data', $data, 12 * HOUR_IN_SECONDS );

    return $data;
}

Lưu ý quan trọng: false !== $cached (dùng !== chứ không phải !=) — vì nếu data cache thực sự là giá trị false hoặc 0 (kiểu falsy hợp lệ), dùng != sẽ hiểu nhầm là “không có cache” và tính toán lại không cần thiết. Đây là chi tiết nhỏ nhưng hay bị hỏi để kiểm tra độ cẩn thận.

Transient lưu ở đâu: Mặc định lưu trong wp_options (với autoload = no). Nếu site có cài Object Cache (Redis/Memcached), WordPress tự động chuyển sang lưu ở đó thay vì DB — code bạn viết không cần đổi gì, transient API tự abstract việc này.

Hằng số thời gian có sẵn (khỏi tính giây thủ công):

MINUTE_IN_SECONDS   // 60
HOUR_IN_SECONDS     // 3600
DAY_IN_SECONDS      // 86400
WEEK_IN_SECONDS

2. Object Cache — khác Transient ở điểm nào

Câu hỏi hay gặp: “Transient và Object Cache khác nhau thế nào?”

TransientObject Cache
Đảm bảo tồn tạiCó — nếu chưa hết hạn, luôn lấy lại được (fallback về DB)Không đảm bảo — cache có thể bị xoá bất cứ lúc nào (memory-based)
Phạm viToàn site, có thời hạn cụ thểThường theo request/server session, dùng cho cache tạm trong lúc xử lý
Dùng khi nàoCache kết quả cần giữ qua nhiều request, nhiều giờ/ngàyCache tạm trong 1 request để tránh query trùng lặp cùng dữ liệu
// Object cache — cache trong phạm vi 1 request, tránh query lặp
wp_cache_set( 'my_key', $data, 'my_group' );
$data = wp_cache_get( 'my_key', 'my_group' );

3. Tối ưu Query — tránh N+1 problem

Sai lầm kinh điển (N+1 query):

// ❌ TỆ — mỗi post lại chạy thêm 1 query lấy meta riêng lẻ trong loop
$posts = get_posts([ 'post_type' => 'book', 'posts_per_page' => 50 ]);
foreach ( $posts as $post ) {
    $price = get_post_meta( $post->ID, '_book_price', true ); // 50 query riêng lẻ!
}

Thực ra WordPress có cơ chế meta caching tự động khi dùng WP_Query/get_posts() — nó “primes” cache toàn bộ meta của các post trong batch đó ngay từ query đầu, nên get_post_meta() sau đó thường KHÔNG query DB thêm (trừ khi tắt update_post_meta_cache). Đây là điểm hay bị hiểu nhầm — nhiều người nghĩ mỗi lần gọi get_post_meta() trong loop là 1 query mới, nhưng thực tế WP đã cache sẵn. Tuy nhiên nếu bạn dùng $wpdb viết tay trong loop thì KHÔNG có cơ chế này — đó mới là N+1 thực sự cần tránh.

Ví dụ N+1 thực sự (custom $wpdb trong loop — phải tránh):

// ❌ THỰC SỰ TỆ
foreach ( $posts as $post ) {
    $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}reviews WHERE book_id = %d", $post->ID
    ) ); // N query riêng biệt
}

// ✅ TỐT — 1 query duy nhất, gom hết
$ids = wp_list_pluck( $posts, 'ID' );
$placeholders = implode( ',', array_fill( 0, count($ids), '%d' ) );
$counts = $wpdb->get_results( $wpdb->prepare(
    "SELECT book_id, COUNT(*) as cnt FROM {$wpdb->prefix}reviews
     WHERE book_id IN ($placeholders) GROUP BY book_id", $ids
) );

4. Dùng đúng tham số WP_Query thay vì query rồi filter bằng PHP

// ❌ TỆ — lấy hết rồi lọc bằng tay
$all = get_posts([ 'post_type' => 'book', 'posts_per_page' => -1 ]);
$filtered = array_filter( $all, fn($p) => get_post_meta($p->ID, '_book_price', true) > 100000 );

// ✅ TỐT — để DB lọc, chỉ query đúng cái cần
$query = new WP_Query([
    'post_type'  => 'book',
    'meta_query' => [[
        'key'     => '_book_price',
        'value'   => 100000,
        'compare' => '>',
        'type'    => 'NUMERIC',
    ]],
]);

'type' => 'NUMERIC' quan trọng — mặc định meta_value là string, so sánh > trên string sẽ sai (VD: "9" > "100" theo string là true vì so ký tự đầu).

5. Công cụ debug performance

Query Monitor (plugin miễn phí): hiện toàn bộ query chạy trong 1 request, hook nào chạy, thời gian mỗi hook, script/style đã enqueue, object cache hit/miss. Đây là công cụ “phải biết” của mọi WP dev — khi được hỏi “bạn debug site chậm thế nào”, câu trả lời tốt nhất luôn bắt đầu bằng: “Em cài Query Monitor để xem query nào chậm, chạy bao nhiêu lần, và hook nào tốn thời gian nhất.”


PHẦN B: AJAX TRONG WORDPRESS

1. Cơ chế admin-ajax.php

WordPress có sẵn 1 endpoint chung cho mọi request AJAX: wp-admin/admin-ajax.php. Bạn không tự viết endpoint riêng, mà đăng ký “action” và WP tự route tới đúng hàm xử lý.

// PHP — đăng ký 2 hook cho 2 nhóm user
add_action( 'wp_ajax_load_more_books', 'handle_load_more_books' );          // user đã đăng nhập
add_action( 'wp_ajax_nopriv_load_more_books', 'handle_load_more_books' );   // khách chưa đăng nhập

function handle_load_more_books() {
    // Check nonce — bắt buộc, AJAX cũng là request có thể bị CSRF
    check_ajax_referer( 'load_more_nonce', 'nonce' );

    $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;

    $query = new WP_Query([
        'post_type'      => 'book',
        'posts_per_page' => 10,
        'paged'          => $page,
    ]);

    $html = '';
    while ( $query->have_posts() ) {
        $query->the_post();
        $html .= '<div>' . esc_html( get_the_title() ) . '</div>';
    }
    wp_reset_postdata();

    // wp_send_json_success/error tự set header đúng, format JSON chuẩn, và tự gọi wp_die()
    wp_send_json_success([ 'html' => $html ]);
}

Điểm hay bị hỏi: vì sao cần đăng ký CẢ HAI wp_ajax_wp_ajax_nopriv_?

Nếu chỉ đăng ký wp_ajax_load_more_books, chỉ user đã login mới gọi được — khách vãng lai bấm nút sẽ nhận lỗi 400 “invalid action” (vì WP không tìm thấy hook wp_ajax_nopriv_... tương ứng). Ngược lại nếu tính năng chỉ dành cho user đăng nhập (VD: admin thao tác nội bộ), cố tình bỏ qua nopriv là đúng — như một lớp chặn tự nhiên.

// JavaScript phía frontend
fetch(ajaxObject.ajax_url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
        action: 'load_more_books',   // ⚠️ PHẢI khớp tên với add_action('wp_ajax_...')
        nonce: ajaxObject.nonce,
        page: 2,
    })
})
.then(res => res.json())
.then(data => {
    if (data.success) {
        document.querySelector('#book-list').innerHTML += data.data.html;
    }
});
// PHP — pass URL và nonce ra JS đúng cách (KHÔNG hard-code trong JS file)
add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script( 'my-ajax', get_template_directory_uri() . '/js/ajax.js', [], '1.0', true );
    wp_localize_script( 'my-ajax', 'ajaxObject', [
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'load_more_nonce' ),
    ]);
});

wp_localize_script() giải quyết vấn đề gì: JavaScript thuần không biết được URL admin-ajax.php hay giá trị nonce (những thứ chỉ PHP biết) — hàm này “bơm” 1 object JS chứa các giá trị đó vào trước khi script chạy, tránh phải hard-code URL (sẽ vỡ nếu site đổi domain hoặc cài trong subfolder).


PHẦN C: REST API

1. Vì sao REST API ngày càng quan trọng

  • Gutenberg (block editor) chạy hoàn toàn qua REST API phía sau.
  • Xu hướng “headless WordPress” — dùng WP làm backend/CMS thuần, frontend là React/Vue/Next.js riêng, giao tiếp qua REST (hoặc GraphQL qua plugin WPGraphQL).
  • App mobile lấy dữ liệu từ WP site qua REST thay vì render HTML.

2. REST endpoint có sẵn (tự động, không cần code)

Nếu CPT khai show_in_rest => true:

GET /wp-json/wp/v2/book              → danh sách
GET /wp-json/wp/v2/book/5            → chi tiết ID 5
POST /wp-json/wp/v2/book             → tạo mới (cần auth)

3. Đăng ký Custom REST Endpoint

add_action( 'rest_api_init', function() {
    register_rest_route( 'my-plugin/v1', '/books-by-genre/(?P<genre>[a-zA-Z0-9-]+)', [
        'methods'             => 'GET',
        'callback'            => 'get_books_by_genre',
        'permission_callback' => '__return_true',   // ⚠️ xem giải thích bên dưới
        'args' => [
            'genre' => [
                'validate_callback' => function( $param ) {
                    return is_string( $param );
                },
            ],
        ],
    ]);
});

function get_books_by_genre( WP_REST_Request $request ) {
    $genre = sanitize_text_field( $request['genre'] );

    $query = new WP_Query([
        'post_type' => 'book',
        'tax_query' => [[ 'taxonomy' => 'genre', 'field' => 'slug', 'terms' => $genre ]],
    ]);

    $books = [];
    foreach ( $query->posts as $post ) {
        $books[] = [
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'price' => get_post_meta( $post->ID, '_book_price', true ),
        ];
    }

    return new WP_REST_Response( $books, 200 );
}

4. permission_callback — LỖI BẢO MẬT NGHIÊM TRỌNG NHẤT khi làm REST API

Đây là câu hỏi/bài test cực kỳ hay gặp.

'permission_callback' => '__return_true',

Bất kỳ ai (kể cả không đăng nhập) đều gọi được endpoint này. Chỉ chấp nhận nếu endpoint thực sự là public data (VD: danh sách sách công khai).

// Nếu cần user đăng nhập
'permission_callback' => function() {
    return is_user_logged_in();
},

// Nếu cần quyền cụ thể (VD: endpoint tạo/sửa dữ liệu)
'permission_callback' => function() {
    return current_user_can( 'edit_posts' );
},

// Nếu cần verify nonce cho request từ chính site mình (JS nội bộ gọi)
'permission_callback' => function( WP_REST_Request $request ) {
    return wp_verify_nonce( $request->get_header('X-WP-Nonce'), 'wp_rest' );
},

Sự thật quan trọng cần biết: Trong các bản WP cũ hoặc code ẩu, có tình trạng bỏ hẳn permission_callback — khi đó WP sẽ hiện cảnh báo trong log, và mặc định coi như public (rất nguy hiểm cho endpoint có thao tác ghi/sửa dữ liệu). Từ WP 5.5+, thiếu tham số này sẽ bị flag cảnh báo rõ ràng, nhưng vẫn có nhiều code cũ mắc lỗi này.

Câu trả lời mẫu khi bị hỏi “review đoạn code REST API này”:

“Điều đầu tiên em kiểm tra luôn là permission_callback — endpoint có ghi/sửa dữ liệu mà để __return_true hoặc bỏ trống là lỗ hổng nghiêm trọng, ai cũng gọi được. Tiếp theo em kiểm tra input có được sanitize/validate đúng trước khi dùng trong query không, vì tham số route ($request['genre']) vẫn là dữ liệu người dùng, phải xử lý như mọi input khác.”


PHẦN D: THỰC HÀNH TAY

  1. Viết chức năng “Load more” bằng AJAX cho CPT book (dùng lại từ Ngày 2) — đủ: nonce, wp_localize_script, wp_send_json_success.
  2. Viết custom REST endpoint GET /my-plugin/v1/books-by-genre/{genre} — thử gọi bằng trình duyệt hoặc Postman.
  3. Test bảo mật: thử gọi AJAX action bằng Postman không kèm nonce → phải bị từ chối. Nếu không bị chặn, quay lại kiểm tra check_ajax_referer().
  4. Cache thử 1 hàm nặng (VD: đếm tổng số sách) bằng Transient, dùng Query Monitor xem query có giảm không sau khi cache.

PHẦN E: TỰ KIỂM TRA CUỐI NGÀY

  1. Transient khác Object Cache ở điểm nào? (Transient đảm bảo tồn tại tới khi hết hạn/fallback DB; Object cache không đảm bảo, mang tính tạm thời hơn)
  2. Vì sao false !== $cached chứ không phải != khi check transient? (Tránh hiểu nhầm giá trị cache hợp lệ là false/0 thành “chưa có cache”)
  3. Gọi get_post_meta() nhiều lần trong loop WP_Query có gây N+1 query thật không? Vì sao? (Không, vì WP đã prime meta cache sẵn khi query; N+1 thực sự xảy ra khi tự viết $wpdb query trong loop)
  4. Vì sao cần đăng ký cả wp_ajax_ lẫn wp_ajax_nopriv_? (wp_ajax chỉ chạy cho user đã login, nopriv cho khách — thiếu 1 trong 2 sẽ có nhóm user không gọi được)_
  5. Lỗi bảo mật nghiêm trọng nhất hay gặp khi làm REST API là gì? (permission_callback để __return_true hoặc bỏ trống cho endpoint có ghi/sửa dữ liệu)
📝 2,504 từ 📅 14/07/2026 👁 5 lượt đọc
← Chương Trước Chương Tiếp →
Giáo trình ôn tập WordPress bài bản + Vibe Coding

Giáo trình ôn tập WordPress bài bản + Vibe Coding

Tác giả: Giang Thanh Nhon

Trạng thái: Đang tiến hành

Số chương: 7

Đánh giá: ⭐ 10/10

💬 Bình luận (0)

Hãy là người đầu tiên bình luận về chương này!