ทำไมต้อง Load Test WordPress Block Theme
WordPress Block Theme (Full Site Editing) มีสถาปัตยกรรมต่างจาก Classic Theme ตรงที่ render content ผ่าน block parser แทน PHP templates ใช้ theme.json สำหรับ styling ที่ generate CSS runtime และมี REST API calls เพิ่มเติมสำหรับ block patterns การเปลี่ยนแปลงเหล่านี้อาจส่งผลต่อ performance ที่ต่างจาก Classic Theme
Load Testing ช่วยให้รู้ว่า WordPress site รองรับ concurrent users ได้กี่คน, response time เป็นอย่างไรเมื่อ traffic สูง, จุดคอขวด (bottleneck) อยู่ที่ไหน (PHP, MySQL, disk I/O, memory) และ caching strategy ที่ใช้มีประสิทธิภาพเพียงพอหรือไม่
สำหรับ Block Theme ที่ใช้ Full Site Editing ต้อง test เพิ่มเติมในส่วนของ Block rendering performance, Global styles (theme.json) processing, Template parts loading, Block patterns REST API และ Site Editor (admin) performance
การทำ Load Test เป็นประจำ (ทุกครั้งที่ update theme, plugins หรือ WordPress core) ช่วยป้องกัน performance regression และมั่นใจว่า site พร้อมรับ traffic ได้ตลอดเวลา
เครื่องมือสำหรับ Load Testing WordPress
เปรียบเทียบเครื่องมือที่เหมาะกับ WordPress
# === เครื่องมือ Load Testing ===
#
# 1. k6 (Grafana)
# - เขียน test ด้วย JavaScript
# - Performance ดีมาก (Go-based)
# - Built-in metrics และ thresholds
# - Cloud service สำหรับ distributed testing
# - ติดตั้ง: brew install k6 / choco install k6
#
# 2. Locust (Python)
# - เขียน test ด้วย Python
# - Web UI สำหรับ real-time monitoring
# - Distributed testing ง่าย
# - เหมาะสำหรับ Python developers
# - ติดตั้ง: pip install locust
#
# 3. Apache JMeter
# - GUI-based, เหมาะสำหรับผู้เริ่มต้น
# - รองรับ protocols หลากหลาย
# - Plugins ecosystem ใหญ่
# - ใช้ resources มาก (Java-based)
#
# 4. Artillery
# - YAML-based configuration
# - Node.js based
# - Cloud integration
# - ติดตั้ง: npm install -g artillery
#
# 5. WP CLI + ab (Apache Bench)
# - Simple, built-in ใน most servers
# - เหมาะสำหรับ quick tests
# - ab -n 1000 -c 50 https://example.com/
#
# === WordPress-specific Test Scenarios ===
#
# 1. Homepage load (cached vs uncached)
# 2. Single post/page with blocks
# 3. Archive pages (category, tag, date)
# 4. Search functionality
# 5. WooCommerce product pages (if applicable)
# 6. REST API endpoints (/wp-json/wp/v2/posts)
# 7. Admin dashboard (wp-admin)
# 8. Login/authentication flow
# 9. Comment submission
# 10. Media uploads
#
# === Test Types ===
# Smoke test: 1-5 users, verify system works
# Load test: Expected traffic (e.g., 100 users)
# Stress test: Beyond expected (e.g., 500 users)
# Spike test: Sudden traffic burst (0 -> 1000 users)
# Soak test: Sustained load for hours (memory leaks)
# ติดตั้ง k6
# Windows: choco install k6
# macOS: brew install k6
# Linux: sudo apt install k6
# Docker: docker run --rm -i grafana/k6 run -
สร้าง Load Test Scripts ด้วย k6
k6 scripts สำหรับ WordPress Block Theme
// wordpress_load_test.js — k6 Load Test for WordPress Block Theme
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const pageLoadTime = new Trend('page_load_time');
const ttfb = new Trend('time_to_first_byte');
const cacheHitRate = new Rate('cache_hits');
// Test configuration
export const options = {
stages: [
{ duration: '1m', target: 10 }, // Ramp up
{ duration: '3m', target: 50 }, // Sustained load
{ duration: '2m', target: 100 }, // Peak load
{ duration: '1m', target: 50 }, // Scale down
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95% requests < 2s
http_req_failed: ['rate<0.05'], // Error rate < 5%
errors: ['rate<0.1'],
time_to_first_byte: ['p(95)<500'], // TTFB < 500ms
},
};
const BASE_URL = __ENV.BASE_URL || 'https://example.com';
export default function() {
// Scenario 1: Homepage
group('Homepage', function() {
const res = http.get(`/`, {
headers: { 'Accept-Encoding': 'gzip, deflate, br' },
});
check(res, {
'status is 200': (r) => r.status === 200,
'has block theme markup': (r) => r.body.includes('wp-block'),
'response time < 2s': (r) => r.timings.duration < 2000,
});
errorRate.add(res.status !== 200);
pageLoadTime.add(res.timings.duration);
ttfb.add(res.timings.waiting);
cacheHitRate.add(res.headers['X-Cache'] === 'HIT');
});
sleep(1);
// Scenario 2: Blog post with blocks
group('Blog Post', function() {
const res = http.get(`/sample-post/`);
check(res, {
'status is 200': (r) => r.status === 200,
'has content blocks': (r) => r.body.includes('wp-block-'),
'has schema markup': (r) => r.body.includes('application/ld+json'),
});
errorRate.add(res.status !== 200);
pageLoadTime.add(res.timings.duration);
});
sleep(1);
// Scenario 3: REST API
group('REST API', function() {
const res = http.get(`/wp-json/wp/v2/posts?per_page=10`, {
headers: { 'Accept': 'application/json' },
});
check(res, {
'api status 200': (r) => r.status === 200,
'returns JSON': (r) => r.headers['Content-Type'].includes('application/json'),
'has posts': (r) => JSON.parse(r.body).length > 0,
});
errorRate.add(res.status !== 200);
});
sleep(1);
// Scenario 4: Search
group('Search', function() {
const res = http.get(`/?s=wordpress+block+theme`);
check(res, {
'search returns 200': (r) => r.status === 200,
});
errorRate.add(res.status !== 200);
pageLoadTime.add(res.timings.duration);
});
sleep(Math.random() * 3);
}
// Run: k6 run --env BASE_URL=https://example.com wordpress_load_test.js
// With HTML report: k6 run --out json=results.json wordpress_load_test.js
Load Testing ด้วย Locust Python
Locust scripts สำหรับ advanced WordPress testing
#!/usr/bin/env python3
# locustfile.py — WordPress Block Theme Load Test with Locust
from locust import HttpUser, task, between, events
from locust.runners import MasterRunner
import random
import logging
import json
from datetime import datetime
logger = logging.getLogger("wp_loadtest")
class WordPressUser(HttpUser):
wait_time = between(1, 5)
def on_start(self):
self.posts = []
self.categories = []
self._fetch_content_list()
def _fetch_content_list(self):
try:
resp = self.client.get("/wp-json/wp/v2/posts?per_page=20", name="/api/posts")
if resp.status_code == 200:
self.posts = [p["link"] for p in resp.json()]
resp = self.client.get("/wp-json/wp/v2/categories?per_page=10", name="/api/categories")
if resp.status_code == 200:
self.categories = [c["link"] for c in resp.json()]
except Exception as e:
logger.warning(f"Failed to fetch content: {e}")
@task(30)
def view_homepage(self):
with self.client.get("/", name="Homepage", catch_response=True) as resp:
if resp.status_code == 200:
if "wp-block" not in resp.text:
resp.failure("Missing block theme markup")
else:
resp.success()
else:
resp.failure(f"Status {resp.status_code}")
@task(25)
def view_blog_post(self):
if not self.posts:
return
url = random.choice(self.posts)
path = url.replace(self.host, "")
with self.client.get(path, name="Blog Post", catch_response=True) as resp:
if resp.status_code == 200:
resp.success()
elif resp.status_code == 404:
resp.failure("Post not found")
@task(15)
def view_category(self):
if not self.categories:
return
url = random.choice(self.categories)
path = url.replace(self.host, "")
self.client.get(path, name="Category Archive")
@task(10)
def search(self):
queries = ["wordpress", "theme", "plugin", "security", "performance"]
query = random.choice(queries)
self.client.get(f"/?s={query}", name="Search")
@task(10)
def api_posts(self):
page = random.randint(1, 5)
self.client.get(
f"/wp-json/wp/v2/posts?per_page=10&page={page}",
name="/api/posts?page=N"
)
@task(5)
def view_sitemap(self):
self.client.get("/sitemap.xml", name="Sitemap")
@task(3)
def view_feed(self):
self.client.get("/feed/", name="RSS Feed")
@task(2)
def static_assets(self):
self.client.get("/wp-content/themes/theme/style.css", name="Theme CSS")
class AdminUser(HttpUser):
wait_time = between(3, 10)
weight = 1 # 1% of users
def on_start(self):
self.client.post("/wp-login.php", data={
"log": "admin",
"pwd": "admin_password",
"wp-submit": "Log In",
"redirect_to": "/wp-admin/",
}, name="Login")
@task
def view_dashboard(self):
self.client.get("/wp-admin/", name="Admin Dashboard")
@task
def edit_post(self):
self.client.get("/wp-admin/post.php?post=1&action=edit", name="Edit Post")
# Custom event handlers
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
logger.info(f"Load test started at {datetime.now()}")
@events.request.add_listener
def on_request(request_type, name, response_time, response_length, **kwargs):
if response_time > 5000:
logger.warning(f"Slow request: {name} took {response_time}ms")
# Run: locust -f locustfile.py --host=https://example.com
# Web UI: http://localhost:8089
# Headless: locust -f locustfile.py --host=https://example.com --headless -u 100 -r 10 --run-time 10m
วิเคราะห์ผลและ Optimize Performance
วิเคราะห์ผลการ test และ optimization
#!/usr/bin/env python3
# analyze_results.py — Load Test Results Analyzer
import json
import statistics
from pathlib import Path
from datetime import datetime
class LoadTestAnalyzer:
def __init__(self, results_file):
self.data = self._load_results(results_file)
def _load_results(self, filepath):
results = []
with open(filepath) as f:
for line in f:
try:
results.append(json.loads(line))
except json.JSONDecodeError:
continue
return results
def analyze(self):
http_reqs = [d for d in self.data if d.get("type") == "Point" and d.get("metric") == "http_req_duration"]
if not http_reqs:
print("No HTTP request data found")
return
durations = [d["data"]["value"] for d in http_reqs]
report = {
"total_requests": len(durations),
"avg_response_ms": round(statistics.mean(durations), 2),
"median_response_ms": round(statistics.median(durations), 2),
"p95_response_ms": round(sorted(durations)[int(len(durations) * 0.95)], 2),
"p99_response_ms": round(sorted(durations)[int(len(durations) * 0.99)], 2),
"min_response_ms": round(min(durations), 2),
"max_response_ms": round(max(durations), 2),
}
print(f"\n{'='*50}")
print(f"Load Test Analysis — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print(f"{'='*50}")
for key, value in report.items():
print(f" {key}: {value}")
# Performance recommendations
print(f"\n--- Recommendations ---")
if report["p95_response_ms"] > 2000:
print(" [CRITICAL] P95 > 2s: Enable page caching (WP Super Cache, W3 Total Cache)")
if report["avg_response_ms"] > 500:
print(" [WARNING] Avg > 500ms: Consider object caching (Redis/Memcached)")
if report["p99_response_ms"] > 5000:
print(" [CRITICAL] P99 > 5s: Check database queries, enable query caching")
return report
# WordPress Performance Optimization Checklist:
#
# 1. Page Caching:
# wp plugin install wp-super-cache --activate
# หรือใช้ Nginx FastCGI cache
#
# 2. Object Caching:
# wp plugin install redis-cache --activate
# wp redis enable
#
# 3. Database Optimization:
# wp db optimize
# wp transient delete --all
#
# 4. Image Optimization:
# wp plugin install imagify --activate
# Convert to WebP format
#
# 5. CDN:
# CloudFlare, BunnyCDN, AWS CloudFront
#
# 6. PHP Optimization:
# PHP 8.2+ with OPcache enabled
# opcache.memory_consumption=256
# opcache.max_accelerated_files=20000
#
# 7. Block Theme Specific:
# Minimize custom blocks
# Lazy load block assets
# Reduce template parts nesting
# Optimize theme.json (remove unused styles)
#
# 8. Server:
# Nginx > Apache for WordPress
# HTTP/2 or HTTP/3 enabled
# Gzip/Brotli compression
# Keep-alive connections
analyzer = LoadTestAnalyzer("results.json")
analyzer.analyze()
CI/CD Integration สำหรับ Performance Testing
รวม load testing เข้ากับ deployment pipeline
# .github/workflows/performance-test.yml
name: WordPress Performance Test
on:
push:
branches: [main]
paths:
- 'wp-content/themes/**'
- 'wp-content/plugins/**'
schedule:
- cron: '0 3 * * 1' # Weekly Monday 03:00
jobs:
load-test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: wordpress
ports: ["3306:3306"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- name: Setup WordPress
run: |
docker run -d --name wordpress \
--network host \
-e WORDPRESS_DB_HOST=127.0.0.1 \
-e WORDPRESS_DB_USER=root \
-e WORDPRESS_DB_PASSWORD=rootpass \
-e WORDPRESS_DB_NAME=wordpress \
-p 8080:80 \
wordpress:latest
sleep 30
# Install WP CLI and setup
docker exec wordpress bash -c "
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
php wp-cli.phar core install \
--url=http://localhost:8080 \
--title='Test Site' \
--admin_user=admin \
--admin_password=admin123 \
--admin_email=test@test.com \
--allow-root
"
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run Load Test
run: |
k6 run \
--env BASE_URL=http://localhost:8080 \
--out json=results.json \
--summary-export=summary.json \
tests/wordpress_load_test.js
- name: Check Thresholds
run: |
python3 -c "
import json
with open('summary.json') as f:
s = json.load(f)
p95 = s['metrics']['http_req_duration']['values']['p(95)']
err_rate = s['metrics']['http_req_failed']['values']['rate']
print(f'P95 Response Time: {p95:.0f}ms')
print(f'Error Rate: {err_rate:.2%}')
if p95 > 2000:
print('FAIL: P95 exceeds 2000ms')
exit(1)
if err_rate > 0.05:
print('FAIL: Error rate exceeds 5%')
exit(1)
print('PASS: All thresholds met')
"
- name: Upload Results
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test-results
path: |
results.json
summary.json
- name: Comment PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('summary.json'));
const p95 = summary.metrics.http_req_duration.values['p(95)'].toFixed(0);
const avg = summary.metrics.http_req_duration.values['avg'].toFixed(0);
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Load Test Results\n| Metric | Value |\n|--------|-------|\n| P95 Response | ms |\n| Avg Response | ms |`
});
FAQ คำถามที่พบบ่อย
Q: Block Theme ช้ากว่า Classic Theme จริงไหม?
A: Block Theme อาจช้ากว่าเล็กน้อยเพราะต้อง parse blocks, process theme.json และ generate CSS runtime แต่ด้วย proper caching (page cache + object cache) ความแตกต่างแทบจะไม่มี Block Theme มีข้อดีคือ frontend rendering เร็วกว่าเพราะ CSS ถูก optimize มากกว่า ลด unused CSS ได้ดีกว่า Classic Theme
Q: k6 กับ Locust เลือกอันไหน?
A: k6 เหมาะสำหรับ CI/CD integration มากกว่าเพราะ single binary, CLI-first, built-in thresholds และ performance ดีมาก (generate 10,000+ RPS จากเครื่องเดียว) Locust เหมาะสำหรับ Python developers ที่ต้องการ flexibility สูง มี Web UI ที่สะดวก สำหรับ WordPress แนะนำ k6 สำหรับ automated pipeline และ Locust สำหรับ exploratory testing
Q: WordPress รองรับ concurrent users ได้กี่คน?
A: ขึ้นอยู่กับ server specs และ optimization WordPress ไม่มี cache บน shared hosting รองรับ 10-50 concurrent users WordPress + page cache บน VPS 2 cores/4GB RAM รองรับ 200-500 concurrent users WordPress + full caching stack (Nginx + Redis + CDN) บน dedicated server รองรับ 1,000-10,000+ concurrent users
Q: ควร load test production หรือ staging?
A: ควร test staging ที่มี specs เหมือน production เสมอ การ test production โดยตรงมีความเสี่ยงทำให้ site ช้าหรือล่มสำหรับ users จริง ถ้าจำเป็นต้อง test production ให้ทำในช่วง low traffic (เช่น กลางคืน) ใช้ ramp-up ช้าๆ มี monitoring พร้อม และสามารถ stop test ได้ทันทีถ้ามีปัญหา
