قبل از نوشتن این مقاله، یک خاطره‌ی تلخ را در ذهنم مرور کردم: پروژه‌ای که کارفرما می‌گفت «سایت کند است و پول داده‌ام برای این سرعت». سه روز روی کد کار کردم و هر مظنون اولیه‌ای را یکی‌یکی رد کردم — هاست ضعیف نبود، قالب سبک بود، افزونه‌ها هم چندان سنگین نبودند. مشکل جای دیگری بود: یک حلقه‌ی foreach که در هر ایتریشن یک کوئری MySQL می‌زد، و روی صفحه‌ای با ۲۰۰ محصول، عملاً ۲۰۰ کوئری جداگانه به دیتابیس می‌فرستاد. آن روز یاد گرفتم که بهینه سازی کدهای PHP بدون اندازه‌گیری، فقط تعویض حدس با حدس دیگر است. در این مقاله، همان مسیری را می‌روم که در پروژه‌های واقعی برای این نوع مشکل طی کرده‌ام: از پروفایلینگ و اندازه‌گیری تا الگوهای عملی که بارها در بازنویسی کد استفاده کرده‌ام.

چرا بهینه‌سازی بدون اندازه‌گیری بی‌معنی است؟

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

راه درست، پروفایلینگ است. سه ابزار که در پروژه‌هایم از آن‌ها استفاده می‌کنم:

  • Xdebug در حالت پروفایلر: با فعال‌سازی xdebug.mode=profile، یک فایل cachegrind تولید می‌شود که با KCacheGrind یا Webgrind قابل خواندن است و دقیقاً می‌گوید کدام تابع چند میلی‌ثانیه زمان برده. برای پروژه‌های محلی، بهترین ابزار است.
  • Blackfire یا Tideways: پروفایلرهای تجاری با رابط کاربری گرافیکی. برای پروژه‌های تیمی ارزش اشتراکشان را دارند چون نمودارهایشان قابل اشتراک با همکار است.
  • اندازه‌گیری دستی با microtime(true): ساده‌ترین ابزار، ولی وقتی هدف پیدا کردن یک گلوگاه مشخص باشد، سریع‌ترین راه است. یک قطعه کد مشکوک را بین دو نقطه‌ی زمانی می‌گیرید و اختلاف را لاگ می‌کنید.
$start = microtime(true);

// کد مشکوک اینجا

$elapsed = microtime(true) - $start;
error_log(sprintf("Section took %.2f ms", $elapsed * 1000));

نکته‌ی مهم این است که بعد از هر تغییر، دوباره اندازه بگیرید. بهینه‌سازی‌هایی که فکر می‌کنید بهتر شده‌اند، گاهی در عمل بدتر می‌شوند — چون رفتار PHP در مقیاس، غیرشهودی است.

هر بهینه‌سازی‌ای که با عدد قبل و بعد تأیید نشود، یک داستان خوب است که به کد تبدیل شده، نه یک بهبود واقعی.

سه سطحی که در پروژه‌های PHP باید جدا کنید

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

سطحنشانهابزار بهبود
کد PHPپروفایلینگ یک تابع را مقصر نشان می‌دهدبازنویسی حلقه، کاهش کپی آرایه
کوئری دیتابیستعداد یا زمان کوئری‌ها بالاستایندکس، JOIN، کاهش کوئری تکراری
زیرساختزمان پاسخ سرور بالاست و کد بی‌تقصیر استOpCache، APCu، کش لبه‌ای

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

عملیاتی که در پروفایلینگ بارها دیده‌ام

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

۱) کاهش کوئری در حلقه

همان مشکلی که در مقدمه به آن اشاره کردم. راه‌حل معمولاً جمع‌کردن شناسه‌ها و سپس یک کوئری با IN است. مثال ساده:

// بد: یک کوئری در هر ایتریشن
foreach ($product_ids as $id) {
    $rows[] = $db->query("SELECT * FROM products WHERE id = " . (int) $id);
}

// بهتر: یک کوئری برای همه
$ids = array_map("intval", $product_ids);
$in  = implode(",", $ids);
$rows = $db->query("SELECT * FROM products WHERE id IN ($in)")->fetchAll();

البته این نسخه‌ی ساده است و برای امنیت باید $ids را با prepared statement یا حداقل با intval امن کنید. اگر با PDO آشنا نیستید، آموزش PDO در PHP نمونه‌های درست و امن را دارد. روش دقیق‌تر با ساخت پلیس‌هولدر برای IN را در بهینه‌سازی کوئری‌های وردپرس با کدنویسی توضیح داده‌ام.

۲) جایگزینی حافظه‌محور با Generator

وقتی یک آرایه‌ی بزرگ را در حافظه می‌سازید، PHP کل آن را نگه می‌دارد. اگر نیازی به نگه‌داشتن کل داده ندارید (مثلاً پردازش خط‌به‌خط)، Generator راه‌حل است:

// بد: همه‌ی رکوردها در حافظه
function get_all_users(PDO $pdo): array {
    return $pdo->query("SELECT * FROM users")->fetchAll();
}

// بهتر: پیمایش تنبل
function stream_users(PDO $pdo): Generator {
    $stmt = $pdo->query("SELECT * FROM users");
    while ($row = $stmt->fetch()) {
        yield $row;
    }
}

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

۳) حذف کپی‌های ناخواسته‌ی آرایه

PHP آرایه‌ها را با copy-on-write مدیریت می‌کند؛ یعنی تا وقتی تغییر ندهید، کپی نمی‌شوند. ولی به‌محض یک تغییر کوچک، کل آرایه کپی می‌شود. در توابعی که آرایه‌ی بزرگ را می‌گیرند، اگر قصد تغییر دارید، پارامتر را با & به‌عنوان مرجع پاس بدهید — البته این کار خوانایی را کمی کم می‌کند، پس آگاهانه انتخاب کنید.

۴) استفاده از توابع بومی به‌جای حلقه‌ی دستی

توابع بومی PHP مثل array_map، array_filter، array_reduce، array_column و in_array در هسته‌ی C پیاده‌سازی شده‌اند و معمولاً سریع‌تر از حلقه‌ی معادل PHP هستند. در یکی از پروژه‌ها، جایگزینی حلقه‌ی دستیِ جستجو در آرایه‌ی چندهزار عضوی با array_flip + isset (تبدیل جستجوی خطی به هش‌لوکاپ) زمان اجرا را از چند صد میلی‌ثانیه به چند میلی‌ثانیه رساند.

در PHP مدرن، خوانایی کد و کارایی معمولاً دشمن هم نیستند؛ تابع بومی که مختصر است، بیشتر مواقع سریع‌تر هم هست.

بهینه‌سازی کوئری‌های دیتابیس در PHP

در اکثر پروژه‌های PHP، دیتابیس گلوگاه اصلی است. سه اصل که بارها به‌کارم آمده:

ایندکس، بزرگ‌ترین برد یک‌خطی

روی ستون‌هایی که در WHERE، ORDER BY یا JOIN استفاده می‌شوند، ایندکس بگذارید. یک کوئری بدون ایندکس روی جدول ۱۰۰هزار ردیفی می‌تواند ثانیه‌ها طول بکشد، درحالی‌که با ایندکس مناسب، در چند میلی‌ثانیه تمام شود. اگر با مفاهیم پایه‌ی دیتابیس آشنا نیستید، اتصال PHP به MySQL نقطه‌ی شروع خوبی است.

کاهش تعداد کوئری‌ها

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

انتخاب ستون‌های مورد نیاز، نه SELECT *

استفاده از SELECT * راحت است، ولی وقتی جدول ستون‌های سنگین مثل TEXT یا BLOB دارد، حجم داده‌ی منتقل‌شده چند برابر می‌شود. فقط ستون‌هایی را که واقعاً استفاده می‌کنید، انتخاب کنید.

کش در PHP: از OpCache تا APCu

کش در PHP چند لایه دارد و اشتباه‌گرفتن لایه‌ها، باعث می‌شود تلاش‌هایتان بی‌اثر باشد.

OpCache: کش سطح کامپایل

PHP هر فایل را در هر درخواست، از متن به بایت‌کد کامپایل می‌کند. OpCache این بایت‌کد را در حافظه نگه می‌دارد و در درخواست بعدی، کامپایل را رد می‌کند. روی هر سرور تولیدی، OpCache باید فعال باشد — این پایه‌ترین برد سرعت PHP است که نصبش یک خط تنظیم در php.ini می‌خواهد.

opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0

نکته‌ی مهم در validate_timestamps=0: در محیط تولید با آن، سرعت بالاتری می‌گیرید، ولی بعد از هر استقرار کد، باید OpCache را پاک کنید. اگر این را فراموش کنید، نسخه‌ی قدیمی کد را می‌بینید.

APCu: کش داده‌های سبک در حافظه

APCu یک کش user-land است که داده‌های سریالایزشده را در حافظه‌ی سرور نگه می‌دارد. برای کوئری‌های پرهزینه‌ای که نتیجه‌شان برای چند دقیقه تغییر نمی‌کند، مناسب است:

$stats = apcu_fetch("site_stats");
if ($stats === false) {
    $stats = expensive_stats_query();
    apcu_store("site_stats", $stats, 300); // ۵ دقیقه
}

در وردپرس، لایه‌ی Object Cache با APCu یا Redis همین نقش را ایفا می‌کند. انتخاب ابزار درست، به نوع پروژه بستگی دارد و همان تصمیمی است که در بهترین افزونه‌های کش وردپرس بررسی کرده‌ام.

اتولودینگ، Composer و بارگذاری هوشمند کد

در پروژه‌های قدیمی، فایل اصلی پر از require_once بود و هر فایل، حتی اگر در درخواست جاری استفاده نمی‌شد، بارگذاری می‌شد. Composer و اتولودینگ PSR-4 این مشکل را حل کرد: فایل فقط زمانی بارگذاری می‌شود که به کلاسش نیاز باشد. اگر با Composer آشنا نیستید، آموزش Composer در PHP کامل توضیح داده.

یک نکته‌ی مهم در محیط تولید: دستور composer install --optimize-autoloader --classmap-authoritative یک نقشه‌ی کلاس‌ها می‌سازد که به PHP می‌گوید بدون جستجوی سیستم فایل، مستقیم فایل را بارگذاری کند. تفاوتش در درخواست‌های متعدد، محسوس است.

composer install --no-dev --optimize-autoloader --classmap-authoritative

بهینه‌سازی PHP در بستر وردپرس

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

۱) به‌جای حلقه‌ی get_posts در حلقه، از WP_Query هوشمند استفاده کنید

هر فراخوانی get_posts در یک حلقه، یک کوئری جدید می‌زند. اگر روی یک صفحه‌ی آرشیو ده دسته دارید و در هر دسته، ۱۰ نوشته می‌خواهید، جای یک حلقه با ۱۰ کوئری، یک WP_Query با پارامترهای tax_query بنویسید. روش کار در بهینه‌سازی کوئری‌های وردپرس با کدنویسی با مثال آمده است.

۲) از transient برای داده‌های پرهزینه استفاده کنید

خروجی APIهای خارجی، آمار سنگین، و محاسبات پیچیده را در transient ذخیره کنید. مثال:

$data = get_transient("remote_api_data");
if ($data === false) {
    $data = wp_remote_get("https://api.example.com/data");
    set_transient("remote_api_data", $data, HOUR_IN_SECONDS);
}

۳) کتابخانه‌های سنگین را فقط هنگام نیاز بارگذاری کنید

در توسعه‌ی افزونه، اگر کتابخانه‌ای مثل PHPMailer یا Twig دارید، آن را در هوک مناسب و فقط برای صفحه‌هایی که به آن نیاز دارند بارگذاری کنید، نه در همه‌ی درخواست‌ها. الگوی درست جداسازی، همان است که در ساختار فایل‌های یک افزونه استاندارد وردپرس توضیح داده‌ام.

در وردپرس، اولین جایی که برای بهینه‌سازی سراغش می‌روم، تعداد کوئری‌های همان صفحه است؛ اگر این عدد بالا باشد، هر بهینه‌سازی دیگری، تزئینی است.

اشتباهات رایج در بهینه‌سازی PHP

در بازبینی کد پروژه‌های دیگران، این خطاها را بارها دیده‌ام و هر کدام، یک درس عملی است:

  • بهینه‌سازی زودهنگام: قبل از این‌که پروفایلینگ نشان دهد گلوگاه کجاست، شروع به بازنویسی می‌کنند. جمله‌ی معروف دونالد کِنوث را جدی بگیرید: بهینه‌سازی زودهنگام، ریشه‌ی همه‌ی شرارت‌هاست.
  • جایگزینی خوانایی با سرعت‌های نامحسوس: یک تابع را از دو خط به یک خط کوتاه می‌کنند و دو میلی‌ثانیه صرفه‌جویی می‌کنند، ولی ماه بعد کسی نمی‌فهمد چه می‌گوید. در نود درصد موارد، خوانایی برنده است.
  • نادیده‌گرفتن هزینه‌ی حافظه: تمرکز روی سرعت و فراموش‌کردن حافظه. یک کد سریع که حافظه‌ی سرور را پر می‌کند، در ترافیک بالا بدتر از یک کد کند است.
  • استفاده از @ برای خفه کردن خطاها: خطاها را با @ سرکوب می‌کنند و بعداً نمی‌فهمند چرا رفتار سیستم عجیب است. مدیریت درست خطا، پایه‌ی بهینه‌سازی است؛ اصولش را در مدیریت خطا در PHP و بهترین روش‌های PHP مدرن آورده‌ام.
  • بی‌توجهی به امنیت در حین بهینه‌سازی: در جریان سرعت‌بخشی، اعتبارسنجی یا پاک‌سازی را حذف می‌کنند. این کار در نهایت هزینه‌ی امنیتی سنگینی می‌سازد — همان قواعدی که در امنیت در PHP روی آن تأکید کرده‌ام، در حین بهینه‌سازی هم باید حفظ شوند.
  • فعال کردن همه‌ی OpCacheها در محیط توسعه: در توسعه، باید تغییرات فوری دیده شوند. OpCache و کش‌های مشابه را در محیط توسعه غیرفعال و در تولید فعال نگه دارید.

سخن آخر

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

اگر امروز فقط یک کار بکنید، پیشنهاد می‌کنم پروفایلر Xdebug را روی محیط محلی فعال کنید و صفحه‌ی کند سایت‌تان را یک بار پروفایل بگیرید. همان گزارش اول، در بیشتر موارد، شما را با گلوگاهی روبرو می‌کند که تا دیروز نمی‌دیدید. تجربه‌ی خودتان از بهینه‌سازی PHP — مخصوصاً اگر با یک گلوگاه غیرمنتظره روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. ⚡