ترنزینت وردپرس، ابزاری است که خیلی از توسعه‌دهندگان حتی اسمش را شنیده‌اند ولی کمتر کسی از پتانسیل واقعی‌اش استفاده می‌کند. من در بازبینی پروژه‌های مشتری زیاد دیده‌ام که یک کوئری سنگین دیتابیس، در هر بازدید کاربر دوباره اجرا می‌شود و توسعه‌دهنده به‌جای حل ریشه‌ای، یک افزونه کش نصب کرده و منتظر معجزه است. ترنزینت، آن معجزه کوچک است که خودِ وردپرس در قلبش گذاشته و فقط باید بدانید کجا و چگونه از آن استفاده کنید.

اگر تازه با معماری وردپرس آشنا می‌شوید، پیش از ادامه افزونه وردپرس چیست و چگونه افزونه مناسب انتخاب کنیم را بخوانید. این نوشته لایه درونی‌تر همان بحث است: نه با نصب افزونه، بلکه با کد نیتیو وردپرس، می‌توانید لایه کش اختصاصی خودتان را بسازید. برای تکمیل دانش پایه، بهترین افزونه‌های کش وردپرس برای افزایش سرعت را هم مرور کنید تا ببینید بعضی از این افزونه‌ها دقیقاً چطور از همان ترنزینت‌ها استفاده می‌کنند.

ترنزینت وردپرس دقیقاً چیست؟

Transient (گذرا) در وردپرس، یک مکانیزم کش داخلی با زمان انقضای مشخص است. شما یک کلید (Key) انتخاب می‌کنید، یک مقدار (Value) در آن ذخیره می‌کنید و یک زمان انقضا (Expiration) تعیین می‌کنید. وردپرس این مقدار را نگه می‌دارد و هر بار که درخواست خواندن می‌کنید، اگر مقدار هنوز منقضی نشده باشد آن را برمی‌گرداند و اگر منقضی شده باشد، به شما false برمی‌گرداند و شما می‌توانید مقدار تازه بسازید.

این تعریف ساده، چیز عجیبی به نظر نمی‌رسد؛ اما در عمل، ترنزینت به شما این امکان را می‌دهد که کوئری سنگین دیتابیس، فراخوانی API بیرونی، محاسبه پیچیده یا هر عملیات پرهزینه را یک بار اجرا کنید و نتیجه را برای مدت مشخصی در دسترس نگه دارید. این همان کاری است که افزونه‌های کش در لایه کلان انجام می‌دهند؛ ترنزینت به شما اجازه می‌دهد دقیقاً همان کار را در نقطه‌ای که خودتان می‌خواهید انجام دهید.

چرا ترنزینت در هسته وردپرس وجود دارد

وردپرس ترنزینت را برای پاسخ به یک نیاز واقعی طراحی کرد: افزونه‌هایی مثل کنش‌گرهای شبکه اجتماعی، پیش‌بینی آب‌وهوا، نرخ ارز و مشابه که داده‌شان از API بیرونی می‌آید، نباید در هر بازدید کاربر فراخوانی جدیدی انجام دهند. بدون ترنزینت، یک سایت با ۵۰۰۰ بازدید روزانه، ۵۰۰۰ فراخوانی به API بیرونی انجام می‌داد و با محدودیت نرخ (Rate Limit) آن API به سرعت به دیوار می‌خورد. ترنزینت این فراخوانی‌ها را به یک بار در هر بازه انقضا کاهش می‌دهد.

ترنزینت، کش در سطح کد است؛ کوچک، دقیق و در نقطه‌ای که خودتان تعیین می‌کنید. افزونه کش، همین کار را در سطح کل سایت انجام می‌دهد؛ ترنزینت، ابزار جراحی است نه چکش.

آناتومی یک ترنزینت: کلید، مقدار و انقضا

هر ترنزینت در وردپرس از سه جزء تشکیل می‌شود که درک دقیق هرکدام، تفاوت بین استفاده سطحی و استفاده حرفه‌ای است.

کلید ترنزینت و طول مجاز

کلید ترنزینت یک رشته است که به‌عنوان شناسه استفاده می‌شود. یک محدودیت مهم در وردپرس وجود دارد که در مستندات رسمی هم کم‌رنگ گفته شده: طول کلید نباید از ۱۷۲ کاراکتر بیشتر باشد. اگر از این طول بیشتر باشد، وردپرس ترنزینت را ذخیره نمی‌کند و در محیط‌های با Object Cache مثل Redis، این محدودیت رفتاری متفاوت پیدا می‌کند. بهترین روش این است که کلید را کوتاه و معنادار نگه دارید و اگر پارامترهای مختلفی دارید، آن‌ها را به یک هش (Hash) تبدیل کنید:

$args = [ 'category' => 12, 'limit' => 10, 'order' => 'DESC' ];
$key  = 'myplugin_top_posts_' . md5( wp_json_encode( $args ) );

این الگو تضمین می‌کند که کلید همیشه زیر ۱۷۲ کاراکتر باشد، هم قابل پیش‌بینی برای شما بماند و هم برای هر ترکیب پارامتر، یک ترنزینت جدا بسازد. اشتباه رایجی که در پروژه‌ها زیاد دیده‌ام، ساختن کلیدهای بلند با درج همه پارامترها به‌شکل خام است؛ نتیجه‌اش این است که در محیط‌هایی که Object Cache ندارند، ترنزینت ذخیره نمی‌شود و شما فکر می‌کنید کش کار می‌کند درحالی‌که هر بار کوئری از نو اجرا می‌شود.

مقدار ترنزینت و نوع داده مجاز

مقدار ترنزینت می‌تواند هر نوع داده PHP باشد: رشته، عدد، آرایه یا آبجکت. ولی وقتی در دیتابیس ذخیره می‌شود (بدون Object Cache)، وردپرس آن را با سریالایز کردن (Serialization) تبدیل به رشته می‌کند و هنگام خواندن، مجدداً آن را دیسریالایز می‌کند. این رفت‌وبرگشت، برای آرایه‌های بزرگ یا آبجکت‌های پیچیده می‌تواند هزینه‌بر باشد. اگر مقدار شما به‌طور مکرر بزرگ است (مثلاً آرایه‌ای با هزار عضو)، این هزینه سریالایز/دیسریالایز را در مصرف CPU لحاظ کنید.

یک قاعده عملی که در پروژه‌های پرترافیک رعایت می‌کنم: ترنزینت را برای مقادیری که در یک تراکنش دیتابیس قابل خواندن هستند استفاده کنید، نه برای مقادیری که حجمشان از یک مگابایت بیشتر می‌شود. برای داده‌های بزرگ، مکانیزم‌های دیگری مثل کش آبجکت اختصاصی یا فایل کش مناسب‌ترند. این تفکیک، تفاوت بین کشی که سرعت می‌دهد و کشی که سرعت می‌خورد است.

انقضا و منطق پاک‌سازی خودکار

انقضای ترنزینت به دو شکل مشخص می‌شود: یا به‌صورت مطلق (تعداد ثانیه از زمان ایجاد)، یا صفر به‌معنی «بدون انقضای خودکار». اگر انقضا صفر باشد، ترنزینت تا وقتی پاک نشود باقی می‌ماند. جالب اینجاست که رفتار پاک‌سازی خودکار ترنزینت‌های منقضی، بسته به اینکه Object Cache فعال باشد یا نه، متفاوت است:

  • بدون Object Cache: ترنزینت‌های منقضی به‌صورت خودکار حذف نمی‌شوند، بلکه به‌عنوان ردیف در جدول wp_options باقی می‌مانند تا یک فرایند پاک‌سازی دوره‌ای آن‌ها را حذف کند.
  • با Object Cache (Redis، Memcached): ترنزینت‌های منقضی معمولاً توسط خود سیستم کش حذف می‌شوند و ردیفی در دیتابیس نمی‌مانند.

این تفاوت رفتاری، دلیل اصلی است که در سایت‌های بزرگ با دیتابیس‌های حجیم، جدول wp_options را پر از ردیف‌های _transient_ می‌بینید. راهنمای چگونه دیتابیس وردپرس را پاک‌سازی کنیم روی همین جدول تمرکز دارد و یکی از موارد پرتکرار در پاک‌سازی‌ها، همین ترنزینت‌های یتیم است.

ترنزینت در برابر Options API و کش مرورگر

یکی از سوءتفاهم‌های رایج، اشتباه گرفتن ترنزینت با Options API یا کش مرورگر است. در حالی که هر سه مکانیزم کش هستند، جایگاهشان در معماری سایت کاملاً متفاوت است. تفاوت را در جدول زیر خلاصه کرده‌ام:

ویژگیTransientOptions APIکش مرورگر
محل ذخیرهwp_options یا Object Cachewp_optionsمرورگر کاربر
زمان انقضای خودکارداردندارددارد (HTTP)
هدف اصلیکش داده‌های پرهزینهذخیره تنظیمات دائمیکش asset و صفحات
مناسب برایکوئری سنگین، API بیرونیتنظیمات افزونهتصویر، CSS، JS
حذف خودکاربعد از انقضادستیبعد از انقضای هدر

تفاوت اصلی بین ترنزینت و Options API در هدف است، نه در مکانیزم. اگر داده‌ای را در Options ذخیره کنید، برای همیشه باقی می‌ماند تا وقتی صریحاً حذفش کنید. اگر در ترنزینت ذخیره کنید، خودِ وردپرس مدیریت انقضا را به‌عهده دارد. اگر روی مکانیزم دقیق Options API کنجکاو هستید، کار با Options API در کدنویسی وردپرس را ببینید. تفاوت کلیدی اینجاست: انتخاب بین این دو، تصمیم معماری است، نه سلیقه.

چرا کش مرورگر جای ترنزینت را نمی‌گیرد

کش مرورگر در سمت کاربر اجرا می‌شود و برای assetهای استاتیک (CSS، JS، تصویر) بی‌نظیر است. اما داده‌ای که در PHP تولید می‌شود — مثل نتیجه یک کوئری پیچیده — در سمت سرور باید هر بار بازتولید شود، حتی اگر مرورگر نسخه قبلی HTML را داشته باشد. ترنزینت دقیقاً همین شکاف را پر می‌کند: کوئری سنگین را یک بار در سرور اجرا می‌کند و نتیجه را در لایه‌ای ذخیره می‌کند که در درخواست بعدی، بدون اجرای مجدد، در دسترس باشد. برای درک کامل زنجیره کش، TTFB چیست و چگونه آن را کاهش دهیم دید خوبی می‌دهد که ترنزینت چطور روی زمان پاسخ سرور اثر می‌گذارد.

Transients API: توابع اصلی و رفتار دقیق

API ترنزینت در وردپرس فقط چهار تابع اصلی دارد و همین سادگی، نقطه قوت آن است:

set_transient و get_transient

// ذخیره مقدار به مدت یک ساعت
set_transient( 'myplugin_top_posts', $posts, HOUR_IN_SECONDS );

// خواندن مقدار
$posts = get_transient( 'myplugin_top_posts' );

if ( false === $posts ) {
    // کش منقضی یا هنوز ساخته نشده
    $posts = myplugin_fetch_top_posts();
    set_transient( 'myplugin_top_posts', $posts, HOUR_IN_SECONDS );
}

نکته کلیدی که خیلی از توسعه‌دهندگان را به دام می‌اندازد: get_transient در زمان موفقیت مقدار را برمی‌گرداند و در زمان عدم وجود یا انقضا false برمی‌گرداند. اگر مقدار ذخیره‌شده‌شده هم false باشد، تفکیک این دو حالت ممکن نیست؛ به همین دلیل توصیه من این است که هرگز مقدار false را به‌عنوان داده ذخیره نکنید. اگر می‌خواهید نتیجه‌ای «خالی» را کش کنید، از یک آرایه خالی یا رشته خالی استفاده کنید.

delete_transient و الگوی پاک‌سازی شرطی

// پاک‌سازی دستی یک ترنزینت
delete_transient( 'myplugin_top_posts' );

// پاک‌سازی خودکار هنگام انتشار نوشته جدید
add_action( 'save_post', function ( $post_id ) {
    if ( wp_is_post_revision( $post_id ) ) {
        return;
    }
    delete_transient( 'myplugin_top_posts' );
} );

الگوی دوم، نقطه اتصال ترنزینت با هوک‌های وردپرس است. اگر با مکانیزم هوک‌ها آشنا نیستید، نحوه استفاده صحیح از هوک‌های وردپرس را پیش از پیاده‌سازی این الگو بخوانید. الگوی پاک‌سازی شرطی یکی از آن نکاتی است که تفاوت بین کشی که کاربر را ناامید می‌کند و کشی که به‌موقع تازه‌سازی می‌شود را می‌سازد. ترنزینتی که هر ساعت تازه می‌شود ولی نوشته جدیدی منتشر شده، به کاربر نوشته قدیمی نشان می‌دهد؛ درحالی‌که با پاک‌سازی شرطی روی save_post، ترنزینت بلافاصله با محتوای تازه بازسازی می‌شود.

یک نکته ظریف که در بازبینی کدها زیاد دیده‌ام: در پاک‌سازی شرطی، نباید روی همه ترنزینت‌های مرتبط به‌صورت کورکورانه عمل کنید. مثلاً اگر ترنزینت شما به تفکیک دسته‌بندی ساخته می‌شود (یک ترنزینت برای هر دسته)، پاک‌سازی همه آن‌ها با هر انتشار نوشته، ضربه سنگین به کارایی است چون بلافاصله بعد از انتشار، همه ترنزینت‌ها باید از نو ساخته شوند. الگوی درست، پاک‌سازی هوشمندانه بر اساس زمینه است:

add_action( 'save_post', function ( $post_id, $post ) {
    if ( wp_is_post_revision( $post_id ) ) {
        return;
    }

    $categories = wp_get_post_categories( $post_id );
    foreach ( $categories as $cat_id ) {
        delete_transient( 'myplugin_top_posts_cat_' . $cat_id );
    }
}, 10, 2 );

این الگو، فقط ترنزینت‌های مربوط به دسته‌بندی‌هایی که نوشته در آن‌ها منتشر شده را پاک می‌کند و بقیه را دست‌نخورده نگه می‌دارد. تفاوت این رویکرد با پاک‌سازی کورکورانه، در سایت‌های پربازدید می‌تواند صرفه‌جویی چند ثانیه CPU در هر انتشار باشد.

الگوهای عملی کش با ترنزینت

حالا که API را می‌شناسید، برویم سراغ الگوهایی که در پروژه‌های واقعی به آن‌ها رسیده‌ام. هر الگو، برای یک سناریوی مشخص مناسب است و انتخاب اشتباه، اثر مورد انتظار را نمی‌دهد.

الگوی Fetch-Through: کش نتیجه API بیرونی

این الگو، پرکاربردترین سناریوی ترنزینت است. فرض کنید افزونه‌ای دارید که نرخ ارز را از یک API بیرونی دریافت می‌کند. فراخوانی این API در هر بازدید، کندی محسوس و ریسک Rate Limit دارد:

function myplugin_get_exchange_rates() {
    $rates = get_transient( 'myplugin_exchange_rates' );

    if ( false !== $rates ) {
        return $rates;
    }

    $response = wp_remote_get( 'https://api.example.com/rates', [
        'timeout' => 5,
    ] );

    if ( is_wp_error( $response ) ) {
        // در صورت خطا، ترنزینت را نساز و مقدار پیش‌فرض برگردان
        return [];
    }

    $body  = wp_remote_retrieve_body( $response );
    $rates = json_decode( $body, true );

    if ( ! is_array( $rates ) ) {
        return [];
    }

    set_transient( 'myplugin_exchange_rates', $rates, 6 * HOUR_IN_SECONDS );

    return $rates;
}

سه نکته مهم در این الگو وجود دارد. اول، در صورت خطای API، ترنزینت ساخته نمی‌شود و در درخواست بعدی، دوباره تلاش می‌شود. اگر به‌جای این، ترنزینت خالی ذخیره کنید، شش ساعت بعدی همه کاربران نرخ خالی می‌بینند. دوم، زمان انقضا بر اساس نرخ تغییر داده‌ها انتخاب می‌شود: نرخ ارز در طول روز چند بار تغییر می‌کند، پس شش ساعت منطقی است. سوم، در پاسخ خواندن از طریق wp_remote_get، از is_wp_error استفاده می‌کنیم نه چک کردن مستقیم کد وضعیت. اگر روی اصول نوشتن کد PHP امن در افزونه‌ها دقت بیشتری می‌خواهید، نوشتن کد PHP امن برای وردپرس راهنمای جامعی است.

الگوی Query Cache: کش کوئری سنگین

این الگو، جای دیگری است که ترنزینت ارزشش را نشان می‌دهد. فرض کنید می‌خواهید محبوب‌ترین نوشته‌ها را بر اساس تعداد بازدید نمایش دهید. یک کوئری با join و order روی جدول متادیتا، در سایت با هزاران نوشته، می‌تواند چند صد میلی‌ثانیه طول بکشد:

function myplugin_get_most_viewed( int $limit = 10 ): array {
    $key = 'myplugin_most_viewed_' . $limit;

    $ids = get_transient( $key );

    if ( false !== $ids ) {
        return $ids;
    }

    global $wpdb;

    $ids = $wpdb->get_col( $wpdb->prepare(
        "SELECT post_id
         FROM {$wpdb->postmeta}
         WHERE meta_key = '_view_count'
         ORDER BY CAST(meta_value AS UNSIGNED) DESC
         LIMIT %d",
        $limit
    ) );

    if ( empty( $ids ) ) {
        return [];
    }

    set_transient( $key, $ids, HOUR_IN_SECONDS );

    return $ids;
}

در این الگو، ترنزینت فقط شناسه‌های نوشته را نگه می‌دارد، نه آبجکت کامل. این تفکیک، حجم داده ذخیره‌شده را کم می‌کند و از سریالایز/دیسریالایز سنگین پرهیز می‌دهد. برای بهینه‌سازی بیشتر کوئری‌های دیتابیس، بهینه‌سازی کوئری‌های وردپرس با کدنویسی نکات مکملی دارد. الگوی «کش کردن شناسه‌ها و واکشی آبجکت با get_post» در پروژه‌های واقعی، معمولاً انتخاب اول من است چون امکان استفاده از کش آبجکت داخلی وردپرس را هم فراهم می‌کند.

الگوی Conditional Cache: کش شرطی بر اساس زمینه

گاهی کش کردن یک مقدار، به وضعیت کاربر یا صفحه بستگی دارد. مثلاً می‌خواهید مطالب مرتبط را کش کنید ولی برای کاربران لاگین‌کرده، این کش نباید استفاده شود چون ممکن است محتوای خصوصی در آن باشد:

function myplugin_get_related_posts( int $post_id ): array {
    if ( is_user_logged_in() ) {
        // برای کاربران لاگین‌کرده، کش را دور بزن
        return myplugin_fetch_related( $post_id );
    }

    $key     = 'myplugin_related_' . $post_id;
    $related = get_transient( $key );

    if ( false !== $related ) {
        return $related;
    }

    $related = myplugin_fetch_related( $post_id );
    set_transient( $key, $related, DAY_IN_SECONDS );

    return $related;
}

این الگو از یک دام کلاسیک جلوگیری می‌کند: کش کردن داده‌ای که برای همه کاربران یکسان نیست. اگر این تفکیک را رعایت نکنید، کاربر A ممکن است محتوای کاربر B را ببیند که از نظر امنیتی و تجربه کاربری فاجعه است. این الگو با همان اصل جداسازی مسئولیت که در پاک‌سازی داده‌ها در کدنویسی وردپرس توضیح داده شده، هم‌راستا است.

ترنزینت در محیط Object Cache: Redis و Memcached

در دیتابیس MySQL، ترنزینت به‌عنوان یک ردیف در جدول wp_options ذخیره می‌شود. اما وقتی Object Cache فعال باشد، رفتار ترنزینت به‌طور بنیادی تغییر می‌کند.

چرا Object Cache ترنزینت را سریع‌تر می‌کند

در محیط Object Cache، ترنزینت در حافظه رم (Redis یا Memcached) ذخیره می‌شود، نه در دیسک. تفاوت سرعت بین خواندن از رم و خواندن از دیسک، در سطح میکروثانیه اندازه‌گیری می‌شود. این یعنی حتی اگر ترنزینت شما از نوع «خواندن یک ردیف از دیتابیس» باشد، در محیط Object Cache، این خواندن از رم اتفاق می‌افتد نه از دیسک.

نکته مهمی که در پروژه‌ها زیاد دیده‌ام: در محیط Object Cache، ترنزینت‌های منقضی به‌طور خودکار توسط سیستم کش حذف می‌شوند و ردیف‌های زامبی در wp_options شکل نمی‌گیرد. این تفاوت رفتاری، دلیل اصلی است که در سایت‌های بزرگ با Object Cache، جدول wp_options سبک‌تر می‌ماند. اگر با انتخاب هاست و نوع سرور آشنا نیستید، هاست چیست و چگونه انتخاب درستی داشته باشیم توضیح می‌دهد که Object Cache در کدام پلن‌ها در دسترس است.

محدودیت ۱۷۲ کاراکتری در محیط Redis

در Redis، کلیدها محدودیت طول مشخصی ندارند ولی وردپرس به‌عنوان لایه بالادستی، همان محدودیت ۱۷۲ کاراکتر را اعمال می‌کند. اگر کلید شما بلندتر باشد، در Redis به‌طور خودکار به یک هش تبدیل می‌شود، ولی این رفتار در نسخه‌های مختلف ثابت نیست. توصیه من: صرف‌نظر از محیط، همیشه کلیدها را کوتاه نگه دارید. این عادت، باعث می‌شود کد شما در همه محیط‌ها رفتاری یکسان داشته باشد. برای بهینه‌سازی سرعت سایت در سطح زیرساخت، تاثیر هاست بر سرعت سایت چقدر است بحث مفصلی دارد که ارزش خواندن دارد.

ترنزینت در MySQL، یک ردیف در دیسک است؛ در Redis، یک کلید در رم. کد شما نباید به این تفاوت وابسته باشد، ولی باید بداند که رفتار در هر دو محیط متفاوت است.

نرخ‌گذاری انقضا و مدیریت دام‌ها

انتخاب زمان انقضای ترنزینت، یک تصمیم مهندسی است که در نگاه اول ساده به نظر می‌رسد ولی دام‌های ظریفی دارد. سه اصل که در پروژه‌ها رعایت می‌کنم:

اصل اول: انقضا را بر اساس نرخ تغییر داده انتخاب کنید

اگر داده هر ساعت تغییر می‌کند، انقضای یک‌ساعته منطقی است. اگر هر روز تغییر می‌کند، انقضای روزانه. اگر داده تقریباً ثابت است ولی از API بیرونی می‌آید، انقضا را چند برابر میانگین فاصله به‌روزرسانی بگذارید. یک قاعده تجربی که زیاد به کارم آمده: انقضا را نصف زمانی که کاربر انتظار به‌روزرسانی دارد انتخاب کنید. مثلاً اگر کاربر انتظار دارد نرخ ارز حداکثر دو ساعت عقب باشد، انقضا را یک ساعت بگذارید.

اصل دوم: از انقضای صفر پرهیز کنید مگر دلایل مشخص

انقضای صفر یعنی ترنزینت هیچ‌وقت خودبه‌خود حذف نمی‌شود و باید دستی پاکش کنید. این حالت فقط برای داده‌هایی مناسب است که با رویداد مشخص تغییر می‌کنند و به هیچ‌وجه بر اساس زمان تغییر نمی‌کنند. در بیشتر پروژه‌ها، انقضای صفر منبع ترنزینت‌های زامبی است. اگر داده شما واقعاً بدون انقضا باید کش شود، از Options API استفاده کنید نه ترنزینت. این تفکیک، در کار با Options API در کدنویسی وردپرس دقیق توضیح داده شده است.

اصل سوم: مدیریت حجم ترنزینت‌ها در سایز بزرگ

در سایت‌هایی که ترنزینت‌های زیادی ساخته می‌شود — مثلاً یک ترنزینت برای هر نوشته — تعداد ردیف‌های جدول wp_options می‌تواند به چند صد هزار برسد. این حجم، روی کوئری‌های autoload اثر می‌گذارد چون وردپرس همه ردیف‌های autoload را در هر درخواست لود می‌کند. ترنزینت‌ها به‌طور پیش‌فرض در گروه autoload قرار نمی‌گیرند، ولی اگر افزونه‌ای غیراستاندارد آن‌ها را autoload کند، اثر آن در سرعت سایت محسوس است. تحلیل دقیق این اثر در تاثیر دیتابیس بر سرعت سایت چقدر است آمده است.

برای پاک‌سازی دوره‌ای، یک الگوی استاندارد دارم که در افزونه‌های حرفه‌ای زیاد استفاده می‌کنم. یک کرون هفتگی که ترنزینت‌های منقضی با پیشوند افزونه را پاک می‌کند. جزئیات راه‌اندازی این کرون در کرون وردپرس و زمان‌بندی خودکار کارها آمده و اگر با مشکلات زمان‌بندی مواجه شدید، رفع مشکلات کرون در وردپرس مسیر عیب‌یابی را نشان می‌دهد:

function myplugin_cleanup_expired_transients() {
    global $wpdb;

    $wpdb->query(
        "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
         WHERE a.option_name LIKE '_transient_myplugin_%'
         AND a.option_name NOT LIKE '_transient_timeout_%_myplugin_%'"
    );
}
add_action( 'myplugin_weekly_cleanup', 'myplugin_cleanup_expired_transients' );

if ( ! wp_next_scheduled( 'myplugin_weekly_cleanup' ) ) {
    wp_schedule_event( time(), 'weekly', 'myplugin_weekly_cleanup' );
}

این کوئری باید با احتیاط اجرا شود چون روی جدول wp_options عمل حذف انجام می‌دهد. در پروژه‌های پرترافیک، من آن را در ساعات کم‌ترافیک اجرا می‌کنم و قبل از هر اجرا، یک بکاپ سریع از جدول می‌گیرم.

امنیت و پاک‌سازی در استفاده از ترنزینت

ترنزینت به‌طور ذاتی جایگاه امنیتی نیست، ولی سه نکته امنیتی دارد که در بازبینی کدها زیاد به آن‌ها برمی‌خورم.

اعتبارسنجی مقدار بازگشتی از ترنزینت

هر مقداری که از ترنزینت برمی‌گردد، باید مثل داده‌ای از منبع خارجی در نظر گرفته شود. در محیط Object Cache، تئوریک امکان دارد ترنزینت بین سایت‌ها به اشتراک گذاشته شود (اگر کلید یکسان باشد و Redis مشترک استفاده شود) و آن‌وقت داده یک سایت ممکن است در سایت دیگر ظاهر شود. یک الگوی دفاعی که در افزونه‌های حرفه‌ای رعایت می‌کنم: کلید ترنزینت را با پیشوند دامنه سایت بسازید:

$prefix = 'transient_' . md5( home_url() ) . '_';
$key    = $prefix . 'top_posts';
set_transient( $key, $posts, HOUR_IN_SECONDS );

این الگو، در محیط‌هایی که Object Cache بین چند سایت به اشتراک گذاشته می‌شود، از تداخل جلوگیری می‌کند.

پاک‌سازی مقدار ترنزینت قبل از استفاده

هر مقدار ذخیره‌شده در ترنزینت، باید هنگام نمایش پاک‌سازی شود. این اصل همان چیزی است که در پاک‌سازی داده‌ها در کدنویسی وردپرس توضیح داده شده و در ترنزینت هم بی‌استثناست. حتی اگر خودتان مقدار را ذخیره کردید، باید در زمان خواندن فرض کنید که ممکن است تغییر کرده باشد. الگوی esc_html، esc_attr، wp_kses_post و مشابه، در زمان نمایش باید اعمال شوند.

پرهیز از ذخیره داده حساس در ترنزینت

ترنزینت، جایگاه مناسبی برای داده‌های حساس نیست. توکن‌های API، اطلاعات هویتی کاربر، رمز عبور و مشابه، نباید در ترنزینت ذخیره شوند چون در دیتابیس wp_options به‌طور خام ذخیره می‌شوند و برای هر کسی با دسترسی به دیتابیس قابل خواندن هستند. در محیط Object Cache، این ریسک کمی کمتر است ولی همچنان جای توصیه نیست. برای داده‌های حساس، از مکانیزم‌های اختصاصی رمزنگاری استفاده کنید.

کجا ترنزینت انتخاب غلط است؟

برای کامل بودن تصویر، جاهایی را هم بگویم که ترنزینت انتخاب بهینه نیست. تجربه‌ام این است که این نکته کمتر گفته می‌شود و باعث می‌شود بعضی توسعه‌دهندگان همه چیز را ترنزینت کنند.

داده‌های حجم بزرگ

اگر داده‌ای که می‌خواهید کش کنید بزرگ‌تر از یک مگابایت است، ترنزینت در محیط بدون Object Cache می‌تواند به مشکل تبدیل شود چون سریالایز و دیسریالایز آن هزینه‌بر است. برای داده‌های بزرگ، کش فایل یا کش اختصاصی با ساختار متفاوت بهتر است. در پروژه‌های واقعی، من حجم آستانه را حدود ۵۰۰ کیلوبایت می‌گذارم و فراتر از آن، مکانیزم دیگری انتخاب می‌کنم.

داده‌ای که باید دقیقاً در لحظه تازه باشد

اگر داده شما کوچک‌ترین تأخیر را نمی‌پذیرد — مثل مانده حساب کاربر در یک سیستم مالی — ترنزینت جای مناسبی نیست. این نوع داده، باید هر بار از منبع اصلی خوانده شود، حتی اگر این خواندن هزینه‌بر باشد. برای داده‌های نیمه‌پویا، می‌توانید از الگوی «کش با پاک‌سازی رویدادمحور» استفاده کنید: هر بار که داده اصلی تغییر می‌کند، ترنزینت را پاک کنید و در زمان خواندن، اگر ترنزینت وجود داشت استفاده کنید، در غیر این صورت تازه بسازید.

داده‌ای که به کاربر وابسته است

ترنزینت برای کش داده‌ای که برای همه کاربران یکسان است طراحی شده. اگر داده شما به کاربر وابسته است — مثل سبد خرید یا پروفایل — باید یا از مکانیزم دیگری استفاده کنید یا کلید ترنزینت را با شناسه کاربر بسازید. الگوی دوم در عمل به مشکل مدیریت تعداد زیاد ترنزینت منجر می‌شود چون با هزاران کاربر، هزاران ترنزینت ساخته می‌شود و جدول wp_options متورم می‌شود. اینجاست که باید به جای ترنزینت، از مکانیزم‌های کش سشن (Session Cache) که در امن‌سازی نشست‌های کاربری توضیح داده شده استفاده کنید.

پرسش‌های متداول درباره ترنزینت وردپرس

آیا ترنزینت با کش صفحات قالب تداخل می‌کند؟ نه، این دو مکانیزم مستقل‌اند. ترنزینت داده را در لایه PHP کش می‌کند و کش صفحات، HTML نهایی را در مرورگر یا CDN. اگر کش صفحه فعال باشد، ترنزینت شما در بسیاری از مواقع اصلاً اجرا نمی‌شود چون صفحه از کش می‌آید. برای درک تعامل این دو، بهترین افزونه‌های کش وردپرس برای افزایش سرعت را ببینید.

ترنزینت در محیط چندسروری چه رفتاری دارد؟ در محیط چندسروری (Load-Balanced)، ترنزینت‌ها در دیتابیس مرکزی ذخیره می‌شوند ولی اگر Object Cache مشترک نباشد، هر سرور ممکن است نسخه خودش را داشته باشد. راه‌حل استاندارد، استفاده از Redis یا Memcached مشترک بین سرورها است. بدون Object Cache مشترک، ترنزینت در محیط چندسروری رفتار تصادفی دارد و نمی‌توان روی آن حساب کرد.

چرا ترنزینت من به‌طور خودکار پاک نمی‌شود؟ در دیتابیس MySQL، ترنزینت‌های منقضی به‌طور خودکار حذف نمی‌شوند. وردپرس فقط هنگام خواندن، مقدار را در دسترس قرار نمی‌دهد ولی ردیف در جدول wp_options باقی می‌ماند. پاک‌سازی واقعی توسط یک کرون دوره‌ای یا افزونه‌های بهینه‌سازی دیتابیس انجام می‌شود. برای رفع این مشکل، الگوی کوئری پاک‌سازی که در همین مقاله آورده‌ام را در یک کرون هفتگی قرار دهید.

تفاوت ترنزینت و کش آبجکت چیست؟ کش آبجکت یک لایه سراسری است که برای همه آبجکت‌های وردپرس (post، term، user) استفاده می‌شود. ترنزینت در محیط Object Cache، در همان لایه ذخیره می‌شود ولی به‌عنوان یک مکانیزم مستقل با API اختصاصی. اگر می‌خواهید داده سفارشی خودتان را کش کنید، ترنزینت API راحت‌تر و استانداردتر است. اگر می‌خواهید کش آبجکت‌های وردپرس را بهبود دهید، افزونه Object Cache اضافه کنید.

آیا ترنزینت روی بودجه کارایی سایت اثر منفی دارد؟ نه، اگر درست استفاده شود. یک ترنزینت به‌طور معمول کمتر از یک ردیف در دیتابیس و چند خط کد است که اثرش در بودجه کارایی ناچیز است. مشکل وقتی ایجاد می‌شود که تعداد ترنزینت‌ها زیاد باشد و در ساعات شلوغ، دیتابیس را تحت فشار قرار دهد. برای درک بودجه کارایی و مدیریت آن، Core Web Vitals چیست و چرا گوگل بر آن تاکید دارد دید دقیقی می‌دهد.

آیا می‌توانم ترنزینت را از پنل مدیریت ببینم؟ به‌طور پیش‌فرض نه، ولی چند افزونه وجود دارد که ترنزینت‌ها را در پیشخوان نمایش می‌دهد. برای دیباگ، این ابزارها مفیدند ولی برای سایت‌های پرترافیک توصیه نمی‌کنم چون هر بازدید از پنل، باعث لود همه ترنزینت‌ها می‌شود. اگر می‌خواهید ترنزینت را دیباگ کنید، بهتر است از WP-CLI یا کوئری مستقیم دیتابیس استفاده کنید.

آیا ترنزینت با القاب اضافی در سایت‌های چندزبانه کار می‌کند؟ باید کلید ترنزینت را با زبان سایت پیشوند کنید. وگرنه ممکن است ترنزینت زبان انگلیسی، به زبان فارسی نمایش داده شود. الگو ساده است: پیشوند زبان را از get_locale() بگیرید و به کلید اضافه کنید. این رویکرد استانداردی است که در پروژه‌های چندزبانه به‌طور مرتب استفاده می‌کنم.

آیا باید ترنزینت‌ها را در زمان انتشار یک نوشته جدید پاک کنم؟ این بستگی به محتوای ترنزینت دارد. اگر ترنزینت شما محبوب‌ترین نوشته‌ها را کش می‌کند، انتشار یک نوشته جدید، آن را کهنه می‌کند و باید پاک شود. اگر ترنزینت شما نرخ ارز را کش می‌کند، انتشار نوشته هیچ ربطی به آن ندارد. الگوی پاک‌سازی زمینه‌ای که در همین مقاله توضیح داده‌ام، دقیقاً به همین دلیل اهمیت دارد.

ترنزینت به‌عنوان ابزار دقیق مهندسی

در پایان این مسیر، یک جمع‌بندی از تجربه واقعی‌ام دارم که در جلسه‌های مشاوره زیاد تکرارش کرده‌ام: ترنزینت، ابزار جراحی است نه چکش. اگر آن را برای کوئری سنگین، فراخوانی API بیرونی، یا محاسبه پرهزینه استفاده کنید، اثرش چشمگیر است. اگر برای هر داده کوچک و بزرگ از آن استفاده کنید، به‌سرعت به منبع مسئله تبدیل می‌شود. تفاوت این دو، همان تفاوت بین توسعه‌دهنده‌ای است که کش را می‌شناسد و توسعه‌دهنده‌ای که فقط اسمش را شنیده.

اگر در ابتدای مسیر یادگیری هستید، سه تمرین را پیشنهاد می‌کنم. اول، یک افزونه ساده بنویسید که نرخ ارز یا هر داده ثابت دیگری را از یک API بیرونی بگیرد و با ترنزینت، فراخوانی را به یک بار در ساعت کاهش دهد. دوم، در یک سایت تستی با هزار ردیف ترنزینت، رفتار جدول wp_options را با و بدون Object Cache مقایسه کنید. سوم، الگوی پاک‌سازی زمینه‌ای را روی یک افزونه با ترنزینت‌های متعدد پیاده کنید و اثرش را روی بودجه کارایی بسنجید. این سه تمرین، در چند ساعت، به شما درکی می‌دهد که هیچ مقاله‌ای جایگزینش نمی‌شود.

اگر تجربه‌ای از استفاده ترنزینت در پروژه‌ای پرترافیک دارید — چه موفق، چه پر از دام — در دیدگاه بنویسید. آن تجربه برای کسی که همین امروز تصمیم می‌گیرد از ترنزینت در افزونه‌اش استفاده کند یا نه، ارزشمندتر از هر مستند رسمی است. ⚙️