موقع WordPress البطيء لا يحتاج خادمًا أكبر في أغلب الحالات، بل يحتاج قياسًا يحدد أين يذهب الوقت، ثم ضبطًا لطبقات معروفة بترتيب واضح: إصدار PHP مدعوم، وعدد عمليات PHP-FPM يناسب الذاكرة، وOPcache (ذاكرة التخزين المؤقت لشيفرة PHP) بحجم كافٍ، وObject Cache (تخزين مؤقت لنتائج الاستعلامات) عبر Redis، وPage Cache (تخزين الصفحات الجاهزة)، وجدول wp_options نظيف، ووظائف Cron حقيقية بدل WP-Cron. يشرح هذا المقال كل خطوة مع إعداد تطبّقه وطريقة تتحقق بها من أثره.
لماذا تبدأ بالقياس قبل أي تعديل؟
لأن كل طبقة تعالج نوعًا مختلفًا من البطء. إذا كان الوقت يذهب في استعلامات قاعدة البيانات فلن يفيدك تكبير OPcache، وإذا كان يذهب في انتظار عملية PHP متاحة فلن يفيدك فهرس جديد. سجّل نتائج القياس قبل أي تعديل حتى تقارن بعده.
TTFB من سطر الأوامر
TTFB (الزمن حتى أول بايت) يقيس ما يستغرقه الخادم في توليد الصفحة قبل أن يبدأ المتصفح في استلامها:
for i in 1 2 3 4 5; do
curl -o /dev/null -s -w '%{time_starttransfer}\n' "https://example.com/?nocache=$i"
doneالمعامل المتغير يتجاوز Page Cache في أغلب الإعدادات، فترى زمن PHP وقاعدة البيانات الفعلي. قارنه بقياس الرابط دون المعامل؛ الفرق هو ما تقدمه طبقة التخزين المؤقت. وتذكّر أن زمن الاستجابة (Latency) بين الخادم والزائر جزء من النتيجة؛ راجع اختيار موقع الاستضافة لجمهور السعودية والخليج.
Query Monitor وسجل الاستعلامات البطيئة
ثبّت إضافة Query Monitor مؤقتًا، وافتح الصفحات البطيئة وأنت مسجل الدخول بحساب مدير. انظر إلى عدد الاستعلامات وزمنها، والمكرر منها، وأي الإضافات أو القوالب أطلقتها، وإلى طلبات HTTP الخارجية التي تنفذها الإضافات في كل تحميل.
Query Monitor يرى طلبًا واحدًا. سجل الاستعلامات البطيئة في MySQL يرى كل الطلبات على مدى أيام:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1بعد يوم أو يومين لخّص السجل بالأمر mysqldumpslow -s t /var/log/mysql/slow.log | head -40 لترى الاستعلامات الأكثر استهلاكًا للوقت إجمالًا. على الاستضافة المشتركة اطلب هذا الإعداد من مزود الخدمة.
أي إصدار PHP تختار؟
اختر فرعًا ما زال يتلقى تحديثات أمنية على الأقل، والأفضل فرع في مرحلة الدعم النشط. راجع صفحة الإصدارات المدعومة على php.net لتعرف تاريخ نهاية الدعم لكل فرع. الإصدار المنتهي دعمه لا يتلقى إصلاحات أمنية، وهذا سبب كافٍ للترقية قبل الحديث عن الأداء.
ما يتعطل عادة عند الترقية هو الإضافات والقوالب القديمة، لا WordPress نفسه. لذلك:
- حدّث WordPress والإضافات والقوالب.
- انسخ الموقع إلى بيئة تجريبية وغيّر إصدار PHP فيها وحدها.
- فعّل سجل الأخطاء وتصفح الصفحات الأساسية والنماذج وسلة الشراء.
- راجع
wp-content/debug.logبحثًا عن أخطاء Fatal وتحذيرات Deprecated، واستبدل الإضافات المتوقفة عن التطوير. - غيّر الإصدار على الموقع الحي في وقت قليل الزيارات وراقب السجلات.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );كم عملية PHP-FPM يحتاج موقعك؟
يشغّل PHP-FPM مجموعة عمليات تسمى pool (مجمع العمليات). كل طلب PHP يشغل عملية واحدة طوال مدته. إذا انشغلت كل العمليات انتظرت الطلبات الجديدة في الطابور، فيرتفع TTFB رغم أن استخدام CPU قد يبدو منخفضًا. وإذا سمحت بعمليات أكثر مما تتسع له الذاكرة بدأ الخادم في استخدام swap أو قتل العمليات.
الإعداد الأهم هو pm.max_children. احسبه من الذاكرة:
- قِس متوسط الذاكرة الفعلية (RSS) لكل عملية تحت حركة اعتيادية. اسم العملية يختلف حسب التوزيعة، مثل
php-fpmأوphp-fpm8.3. - حدد الذاكرة المتاحة لـ PHP: الإجمالي ناقص ما يحتاجه MySQL وRedis وخادم الويب والنظام، مع هامش أمان.
- اقسم الذاكرة المتاحة على متوسط العملية وقرّب الناتج إلى الأسفل.
ps --no-headers -o rss -C php-fpm8.3 \
| awk '{s+=$1; n++} END {printf "%d processes, avg %.0f MB\n", n, s/n/1024}'مثال للتوضيح فقط: إذا بقي لـ PHP نحو 2048 MB وكان متوسط العملية 80 MB فالناتج 25. ابدأ بأقل منه قليلًا ثم راقب.
; /etc/php/8.3/fpm/pool.d/example.conf
pm = dynamic
pm.max_children = 24
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500الوضع dynamic يناسب أغلب المواقع. ondemand يوفر الذاكرة لمواقع قليلة الزيارات لكنه يضيف زمن بدء عملية بعد فترات الهدوء. static يناسب خادمًا مخصصًا بحمل ثابت. وpm.max_requests يعيد تشغيل العملية دوريًا فيحد من أثر تسرب الذاكرة.
لتعرف أن الـ pool مشبع ابحث في سجل PHP-FPM:
grep -i "max_children" /var/log/php8.3-fpm.logظهور server reached pm.max_children setting يعني أن طلبات انتظرت. الحل ليس رفع الرقم دائمًا؛ إن لم تكفِ الذاكرة فقلّل زمن كل طلب بالتخزين المؤقت أولًا.
كيف تضبط OPcache؟
يحفظ OPcache ملفات PHP بعد ترجمتها إلى bytecode في ذاكرة مشتركة، فلا يعيد PHP ترجمتها مع كل طلب. هو مفعّل افتراضيًا غالبًا، لكن قيمه الافتراضية قد تضيق على خادم يستضيف عدة مواقع بإضافات كثيرة.
; /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60القيم أمثلة تعدّلها حسب خادمك:
opcache.memory_consumptionبالميجابايت. إذا امتلأت توقف OPcache عن إضافة ملفات أو أعاد تشغيل نفسه.opcache.max_accelerated_filesحد عدد الملفات. عدّ ملفات PHP بالأمرfind /home -name '*.php' | wc -lواختر رقمًا أكبر بهامش واضح.opcache.validate_timestamps = 1يتحقق من تعديل الملفات كلrevalidate_freqثانية. هذا الخيار الآمن لمواقع تُحدَّث إضافاتها من لوحة التحكم، وثمنه تأخر ظهور التعديل حتى انقضاء المدة.opcache.validate_timestamps = 0يلغي التحقق، فلا يظهر أي تعديل حتى تعيد تحميل PHP-FPM. استخدمه فقط حين ينتهي النشر بإعادة التحميل، كما في النشر من Git لتطبيقات Laravel وNode.js. مع WordPress الذي يحدّث نفسه من اللوحة يسبب ارتباكًا أكثر مما يوفر.
للتحقق اقرأ نتيجة opcache_get_status() من ملف PHP محمي يُطلب عبر خادم الويب، لا من سطر الأوامر، لأن CLI يملك ذاكرة OPcache منفصلة. إذا كان cache_full صحيحًا أو تكرر oom_restarts فزِد الذاكرة أو عدد الملفات.
متى يفيد Object Cache عبر Redis؟
يملك WordPress طبقة Object Cache (تخزين مؤقت للكائنات) تعيش داخل الطلب الواحد ثم تختفي. عند ربطها بـ Redis تبقى نتائج الاستعلامات والخيارات وtransients في الذاكرة بين الطلبات، فتقل الاستعلامات على قاعدة البيانات.
الإعداد الشائع: تثبيت Redis، ثم إضافة مثل Redis Object Cache تضع ملف drop-in باسم wp-content/object-cache.php، وهو ما يجعل WordPress يستخدم Redis. أضف الثوابت إلى wp-config.php:
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 );
define( 'WP_REDIS_PREFIX', 'example_com:' );
define( 'WP_CACHE_KEY_SALT', 'example_com:' );wp redis enable
wp redis status
redis-cli info stats | grep -E 'keyspace_hits|keyspace_misses'البادئة ضرورية إذا شارك أكثر من موقع نسخة Redis نفسها. وعلى خادم مشترك بين عملاء مختلفين لا تكفي البادئة للعزل؛ استخدم نسخة Redis لكل حساب عبر unix socket بصلاحياته، أو مستخدم ACL مستقلًا. واضبط حدًا للذاكرة:
# redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lruمتى لا يفيد؟ حين تخدم Page Cache أغلب الصفحات فلا يصل الطلب إلى PHP، وحين يكون عدد الاستعلامات قليلًا أصلًا، وحين يكون البطء في استعلام ثقيل واحد لا يُخزَّن مؤقتًا. وقد يضر إذا كتبت إضافة مفاتيح كثيرة بلا انتهاء صلاحية فطردت المفاتيح المفيدة. الدليل هو المقارنة في Query Monitor قبل التفعيل وبعده.
ما دور Page Cache؟
Page Cache (تخزين مؤقت للصفحات) يحفظ صفحة HTML كاملة ويعيدها للزائر التالي دون تشغيل PHP أو قاعدة البيانات، على مستوى خادم الويب مثل fastcgi_cache في Nginx أو عبر إضافة. القواعد الأساسية:
- لا تخزّن صفحات المستخدمين المسجلين ولا صفحات السلة والدفع والحساب.
- تجاوز التخزين عند وجود كوكيز تسجيل الدخول أو السلة.
- امسح الصفحات المتأثرة عند النشر، لا الموقع كله.
- تحقق من الترويسات بالأمر
curl -sI https://example.com/ | grep -i cache.
لوحة التحكم والصفحات الديناميكية لا تستفيد منها؛ هناك يظهر أثر OPcache وObject Cache وPHP-FPM.
لماذا تتضخم wp_options وكيف تنظفها؟
كل خيار في wp_options محدد للتحميل التلقائي (autoload) يُقرأ في كل طلب. الإضافات المحذوفة تترك خياراتها خلفها، وبعضها يخزن بيانات كبيرة في خيار واحد. احسب الحجم ثم اعرض أكبر الخيارات:
wp option list --autoload=on --format=total_bytesSELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY bytes DESC
LIMIT 20;راجع بادئة الجدول في wp-config.php إن لم تكن wp_. لكل خيار كبير اسأل: هل الإضافة التي أنشأته ما زالت مثبتة؟ إن لم تكن فاحذفه بعد أخذ نسخة من قاعدة البيانات، واحذف transients المنتهية:
wp db export before-cleanup.sql
wp option delete old_plugin_settings
wp transient delete --expiredوراجع صفحة Site Health، فالإصدارات الحديثة من WordPress تنبّه عند تضخم الخيارات المحملة تلقائيًا.
لماذا تستبدل WP-Cron بوظائف Cron حقيقية؟
يفحص WP-Cron المهام المستحقة مع زيارات الموقع، فيضيف عملًا إلى طلبات الزوار. في موقع قليل الزيارات تتأخر المهام مثل النشر المجدول، وفي موقع كثير الزيارات تتكرر الفحوص بلا حاجة. أوقفه وأنشئ وظيفة Cron على الخادم:
define( 'DISABLE_WP_CRON', true );*/5 * * * * /usr/local/bin/wp --path=/home/example/public_html cron event run --due-now --quietأضفها من صفحة وظائف Cron في لوحة الاستضافة، وتأكد من مسار WP-CLI وشغّل الأمر يدويًا قبل جدولته. ثم راجع wp cron event list لتكتشف مهامًا تركتها إضافات محذوفة.
ماذا عن الصور وCDN؟
الصور لا تؤثر في TTFB لكنها تؤثر في زمن تحميل الصفحة كاملة. ارفعها بأبعاد قريبة من حجم عرضها، واستخدم WebP أو AVIF حيث تُدعم، واترك التحميل الكسول المدمج في WordPress مفعّلًا. وCDN يقرّب الملفات الثابتة من الزائر، لكنه لا يصلح صفحة يتأخر PHP في توليدها.
قائمة التحقق
- قِس TTFB مع التخزين المؤقت وبدونه قبل أي تعديل.
- افحص الاستعلامات بـ Query Monitor وسجل الاستعلامات البطيئة.
- استخدم إصدار PHP مدعومًا بعد اختبار الإضافات والقوالب.
- احسب
pm.max_childrenمن الذاكرة وراقب رسائل التشبع. - تحقق من أن ذاكرة OPcache لا تمتلئ.
- فعّل Object Cache عبر Redis مع بادئة وحد ذاكرة وعزل لكل حساب.
- فعّل Page Cache للزوار مع استثناء السلة والحساب.
- نظّف الخيارات المحملة تلقائيًا، واستبدل WP-Cron بوظيفة Cron.
- غيّر شيئًا واحدًا في كل مرة وأعد القياس.
كيف يساعدك bPanel في هذه المشكلة؟
في bPanel تحقيق بالذكاء الاصطناعي يقرأ المقاييس والسجلات حين يبطؤ موقع، فيلاحظ مثلًا أن pool في PHP-FPM يصل إلى pm.max_children باستمرار، ويحسب حدًا مناسبًا من استخدام الذاكرة الفعلي، ثم يقترح التعديل لتراجعه وتوافق عليه قبل تطبيقه. يمكنك تجربته على خوادمك من ابدأ مع bPanel، وتشرح عمليات الذكاء الاصطناعي في لوحة استضافة هذا النهج.
الخلاصة
- القياس يحدد الطبقة التي تستحق الضبط.
- إصدار PHP المدعوم شرط أمني، واختبر التوافق قبل الترقية.
- عدد عمليات PHP-FPM يُحسب من الذاكرة، وOPcache يحتاج ذاكرة وعدد ملفات كافيين.
- Object Cache وPage Cache يخففان الحمل، وأثرهما يُثبت بالقياس.
- الخيارات المحملة تلقائيًا وWP-Cron مصدران شائعان للبطء يسهل علاجهما.