Skip to content

Instantly share code, notes, and snippets.

@vapvarun
Created February 26, 2026 05:31
Show Gist options
  • Select an option

  • Save vapvarun/16993b156ae5aaec120b0a25e735763c to your computer and use it in GitHub Desktop.

Select an option

Save vapvarun/16993b156ae5aaec120b0a25e735763c to your computer and use it in GitHub Desktop.
Performance Optimization for High-Traffic BuddyPress Communities (bpcustomdev.com)
// BAD: N+1 pattern — fires a query per member
$members = bp_core_get_users( array( 'per_page' => 20 ) );
foreach ( $members['users'] as $member ) {
// Each call fires a SELECT against bp_xprofile_data
$location = xprofile_get_field_data( 'Location', $member->ID );
$job_title = xprofile_get_field_data( 'Job Title', $member->ID );
$company = xprofile_get_field_data( 'Company', $member->ID );
echo "<div class='member-card'>";
echo "<h3>" . bp_core_get_user_displayname( $member->ID ) . "</h3>";
echo "<p>{$job_title} at {$company} — {$location}</p>";
echo "</div>";
}
// Total: 1 + (20 × 3) = 61 queries just for the directory listing
// GOOD: Bulk prefetch — 2 queries total regardless of member count
$members = bp_core_get_users( array( 'per_page' => 20 ) );
$user_ids = wp_list_pluck( $members['users'], 'ID' );
// Prefetch all xprofile data for these users in one query
BP_XProfile_ProfileData::get_all_for_user_ids( $user_ids );
// Now these calls hit the object cache, not the database
foreach ( $members['users'] as $member ) {
$location = xprofile_get_field_data( 'Location', $member->ID );
$job_title = xprofile_get_field_data( 'Job Title', $member->ID );
$company = xprofile_get_field_data( 'Company', $member->ID );
// ... render member card
}
// Total: 2 queries — one for members, one for all their profile data
/**
* Bulk-fetch xprofile data for multiple users.
*
* @param array $user_ids Array of user IDs.
* @param array $field_ids Array of xprofile field IDs to fetch.
* @return array Keyed by user_id => field_id => value.
*/
function bpcd_bulk_get_xprofile_data( $user_ids, $field_ids ) {
global $wpdb;
$bp = buddypress();
$user_ids_sql = implode( ',', array_map( 'absint', $user_ids ) );
$field_ids_sql = implode( ',', array_map( 'absint', $field_ids ) );
$results = $wpdb->get_results(
"SELECT user_id, field_id, value
FROM {$bp->profile->table_name_data}
WHERE user_id IN ({$user_ids_sql})
AND field_id IN ({$field_ids_sql})",
OBJECT
);
$data = array();
foreach ( $results as $row ) {
$data[ $row->user_id ][ $row->field_id ] = $row->value;
}
return $data;
}
SELECT a.id, a.user_id, a.component, a.type, a.action,
a.content, a.primary_link, a.item_id, a.secondary_item_id,
a.date_recorded, a.hide_sitewide, a.is_spam
FROM wp_bp_activity a
WHERE a.hide_sitewide = 0
AND a.is_spam = 0
AND a.type != 'activity_comment'
ORDER BY a.date_recorded DESC
LIMIT 0, 20;
-- Composite index for the primary activity stream query pattern
ALTER TABLE wp_bp_activity
ADD INDEX idx_activity_stream (
hide_sitewide,
is_spam,
type,
date_recorded DESC
);
-- Index for user-specific activity feeds
ALTER TABLE wp_bp_activity
ADD INDEX idx_user_activity (
user_id,
hide_sitewide,
is_spam,
date_recorded DESC
);
-- Index for component-specific queries (group feeds, etc.)
ALTER TABLE wp_bp_activity
ADD INDEX idx_component_activity (
component,
item_id,
hide_sitewide,
is_spam,
date_recorded DESC
);
-- Index for looking up a user's profile data (member profiles)
ALTER TABLE wp_bp_xprofile_data
ADD INDEX idx_user_field_value (user_id, field_id, value(191));
-- Index for searching members by field value (member directory filters)
ALTER TABLE wp_bp_xprofile_data
ADD INDEX idx_field_value_user (field_id, value(191), user_id);
-- Index for the last_updated column (recently updated profiles)
ALTER TABLE wp_bp_xprofile_data
ADD INDEX idx_last_updated (last_updated);
# Ubuntu/Debian
sudo apt install redis-server php-redis
# Verify Redis is running
redis-cli ping
# Expected: PONG
# Check memory usage
redis-cli info memory | grep used_memory_human
# Allocate at least 256MB for a 50K+ member community
/* Redis Object Cache Configuration */
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_REDIS_DATABASE', 0 ); // Use database 0 for this site
define( 'WP_REDIS_TIMEOUT', 1 ); // 1 second connection timeout
define( 'WP_REDIS_READ_TIMEOUT', 1 ); // 1 second read timeout
define( 'WP_REDIS_MAXTTL', 86400 ); // Max 24-hour TTL for all keys
// CRITICAL: Set a unique prefix if you host multiple WP sites on the same Redis
define( 'WP_REDIS_PREFIX', 'bpsite1:' );
// Enable igbinary serialization for ~30% less memory usage
define( 'WP_REDIS_SERIALIZER', 'igbinary' );
// Disable banners in admin
define( 'WP_REDIS_DISABLE_BANNERS', true );
# Check Redis key count after loading a BuddyPress page
redis-cli dbsize
# Should show hundreds of keys after a single page load
# Monitor cache hits in real-time
redis-cli monitor | grep -i "bp_"
# You should see keys like bp_activity, bp_xprofile, bp_groups
/**
* Warm the object cache for commonly accessed BuddyPress data.
* Run this on a cron schedule (every 5-15 minutes).
*/
function bpcd_warm_cache() {
// Pre-cache the 100 most recently active members
$active_users = BP_Core_User::get_users(
'active',
100,
1,
0,
false,
false,
true // populate_extras
);
// Pre-cache popular groups
$groups = groups_get_groups( array(
'per_page' => 50,
'orderby' => 'popular',
'show_hidden' => false,
) );
// Pre-cache active activity items
bp_activity_get( array(
'per_page' => 50,
'fields' => 'all',
) );
}
add_action( 'bpcd_cache_warm_event', 'bpcd_warm_cache' );
// Schedule the cache warming event
if ( ! wp_next_scheduled( 'bpcd_cache_warm_event' ) ) {
wp_schedule_event( time(), 'every_ten_minutes', 'bpcd_cache_warm_event' );
}
/**
* Fragment cache helper for BuddyPress template parts.
*/
class BPCD_Fragment_Cache {
/**
* Get or generate a cached fragment.
*
* @param string $key Cache key.
* @param callable $callback Function that generates the HTML.
* @param int $ttl Time to live in seconds.
* @param string $group Cache group.
* @return string The HTML fragment.
*/
public static function get( $key, $callback, $ttl = 300, $group = 'bpcd_fragments' ) {
$html = wp_cache_get( $key, $group );
if ( false === $html ) {
ob_start();
call_user_func( $callback );
$html = ob_get_clean();
wp_cache_set( $key, $html, $group, $ttl );
}
return $html;
}
/**
* Invalidate a fragment.
*/
public static function invalidate( $key, $group = 'bpcd_fragments' ) {
wp_cache_delete( $key, $group );
}
}
// Usage: Cache the member directory header stats for 5 minutes
echo BPCD_Fragment_Cache::get(
'member_directory_stats',
function() {
$total = bp_get_total_member_count();
$online = bp_get_online_member_count();
echo "<div class='member-stats'>";
echo "<span>{$total} members</span> | <span>{$online} online</span>";
echo "</div>";
},
300 // 5-minute TTL
);
/**
* Cache the activity stream output per user per page.
*/
function bpcd_cached_activity_stream() {
$user_id = bp_loggedin_user_id();
$page = bp_current_action() ? bp_current_action() : 1;
$scope = isset( $_COOKIE['bp-activity-scope'] )
? sanitize_key( $_COOKIE['bp-activity-scope'] )
: 'all';
$cache_key = "activity_stream_{$scope}_{$page}_{$user_id}";
echo BPCD_Fragment_Cache::get(
$cache_key,
function() {
if ( bp_has_activities( bp_ajax_querystring( 'activity' ) ) ) {
while ( bp_activities() ) {
bp_the_activity();
bp_get_template_part( 'activity/entry' );
}
}
},
120 // 2-minute TTL — balance freshness vs performance
);
}
// Invalidate when new activity is posted
add_action( 'bp_activity_add', function( $args ) {
// Flush all activity fragments — simple but effective
if ( function_exists( 'wp_cache_flush_group' ) ) {
wp_cache_flush_group( 'bpcd_fragments' );
}
} );
/**
* Replace the activity stream with a skeleton loader on initial load.
* The actual content loads via AJAX after DOMContentLoaded.
*/
function bpcd_lazy_activity_stream() {
// Only lazy-load the main activity page, not AJAX requests
if ( wp_doing_ajax() || ! bp_is_activity_component() ) {
return;
}
// Enqueue the lazy loader script
wp_enqueue_script(
'bpcd-lazy-activity',
get_stylesheet_directory_uri() . '/js/lazy-activity.js',
array( 'jquery' ),
'1.0.0',
true
);
wp_localize_script( 'bpcd-lazy-activity', 'bpcdLazy', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'bpcd_lazy_activity' ),
) );
}
add_action( 'bp_enqueue_scripts', 'bpcd_lazy_activity_stream' );
// lazy-activity.js — Load activity stream after page shell renders
(function($) {
'use strict';
$(document).ready(function() {
var $container = $('#activity-stream');
if (!$container.length) return;
// Show skeleton placeholders
var skeleton = '';
for (var i = 0; i < 5; i++) {
skeleton += '<div class="activity-skeleton">' +
'<div class="skeleton-avatar"></div>' +
'<div class="skeleton-content">' +
'<div class="skeleton-line w-60"></div>' +
'<div class="skeleton-line w-80"></div>' +
'<div class="skeleton-line w-40"></div>' +
'</div></div>';
}
$container.html(skeleton);
// Fetch actual activity via AJAX
$.ajax({
url: bpcdLazy.ajaxurl,
type: 'POST',
data: {
action: 'bpcd_load_activity',
nonce: bpcdLazy.nonce,
scope: 'all',
page: 1
},
success: function(response) {
if (response.success) {
$container.html(response.data.html);
}
}
});
});
})(jQuery);
// Modern infinite scroll using Intersection Observer
(function() {
'use strict';
var page = 1;
var loading = false;
var sentinel = document.getElementById('activity-sentinel');
if (!sentinel) return;
var observer = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting && !loading) {
loading = true;
page++;
fetch(bpcdLazy.ajaxurl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'action=bpcd_load_activity&nonce=' + bpcdLazy.nonce + '&page=' + page
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success && data.data.html) {
var stream = document.getElementById('activity-stream');
stream.insertAdjacentHTML('beforeend', data.data.html);
loading = false;
} else {
// No more items — disconnect observer
observer.disconnect();
}
});
}
}, { rootMargin: '200px' }); // Start loading 200px before sentinel is visible
observer.observe(sentinel);
})();
/**
* Maintain a fast lookup table for member last-active times.
* This avoids querying bp_activity for sorting member directories.
*/
function bpcd_update_last_active_index( $user_id ) {
global $wpdb;
$wpdb->replace(
$wpdb->prefix . 'bp_member_last_active',
array(
'user_id' => $user_id,
'last_active' => current_time( 'mysql', true ),
),
array( '%d', '%s' )
);
}
add_action( 'bp_activity_add', function( $args ) {
if ( ! empty( $args['user_id'] ) ) {
bpcd_update_last_active_index( $args['user_id'] );
}
});
add_action( 'wp_login', function( $user_login, $user ) {
bpcd_update_last_active_index( $user->ID );
}, 10, 2 );
/**
* Create the fast lookup table.
*/
function bpcd_create_last_active_table() {
global $wpdb;
$table = $wpdb->prefix . 'bp_member_last_active';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
user_id BIGINT UNSIGNED NOT NULL,
last_active DATETIME NOT NULL,
PRIMARY KEY (user_id),
INDEX idx_last_active (last_active DESC)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
register_activation_hook( __FILE__, 'bpcd_create_last_active_table' );
/**
* Pre-filter member directory queries to reduce JOIN overhead.
* Hooks into bp_pre_user_query to add efficient conditions.
*/
function bpcd_optimize_member_query( $query ) {
// Only optimize when xprofile search is active
if ( empty( $query->query_vars['xprofile_query'] ) ) {
return;
}
global $wpdb;
$bp = buddypress();
// Get the field ID and value from the search
$xprofile_query = $query->query_vars['xprofile_query'];
foreach ( $xprofile_query as $clause ) {
$field_id = absint( $clause['field'] );
$value = sanitize_text_field( $clause['value'] );
// Use a subquery with our optimized index
$user_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT user_id FROM {$bp->profile->table_name_data}
WHERE field_id = %d AND value LIKE %s
LIMIT 1000",
$field_id,
'%' . $wpdb->esc_like( $value ) . '%'
) );
if ( ! empty( $user_ids ) ) {
$query->query_vars['include'] = $user_ids;
} else {
// No matches — short-circuit the query
$query->query_vars['include'] = array( 0 );
}
}
}
add_action( 'bp_pre_user_query', 'bpcd_optimize_member_query' );
/**
* Rewrite BuddyPress avatar URLs to use CDN.
*/
function bpcd_cdn_avatar_url( $avatar_url ) {
$site_url = site_url();
$cdn_url = 'https://cdn.yourdomain.com';
return str_replace( $site_url, $cdn_url, $avatar_url );
}
add_filter( 'bp_core_fetch_avatar_url', 'bpcd_cdn_avatar_url' );
add_filter( 'bp_core_fetch_avatar', 'bpcd_cdn_avatar_url' );
/**
* Rewrite cover image URLs for CDN.
*/
function bpcd_cdn_cover_image_url( $cover_url ) {
$upload_dir = wp_upload_dir();
$cdn_url = 'https://cdn.yourdomain.com/wp-content/uploads';
return str_replace( $upload_dir['baseurl'], $cdn_url, $cover_url );
}
add_filter( 'bp_attachments_pre_get_attachment', 'bpcd_cdn_cover_image_url' );
# .htaccess — Cache BuddyPress static assets aggressively
<IfModule mod_expires.c>
# Avatars and cover images — 30 days
<FilesMatch "\.(jpg|jpeg|png|gif|webp)$">
ExpiresDefault "access plus 30 days"
Header set Cache-Control "public, max-age=2592000, immutable"
</FilesMatch>
# CSS and JS — 7 days (with versioned query strings for cache busting)
<FilesMatch "\.(css|js)$">
ExpiresDefault "access plus 7 days"
Header set Cache-Control "public, max-age=604800"
</FilesMatch>
</IfModule>
/* Query Monitor — enable full profiling */
define( 'QM_ENABLE_CAPS_PANEL', true ); // Show capability checks
define( 'SAVEQUERIES', true ); // Store query caller info
/* Enable slow query highlighting (queries taking > 0.05s) */
define( 'QM_DB_EXPENSIVE', 0.05 );
/* Show queries from all plugins, not just the current request */
define( 'QM_DARK_MODE', true ); // Easier on the eyes during long sessions
/**
* Log BuddyPress slow queries to a dedicated log file.
* Enable in production for ongoing performance monitoring.
*/
function bpcd_log_slow_queries() {
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) {
return;
}
global $wpdb;
$slow_threshold = 0.05; // 50ms
foreach ( $wpdb->queries as $query ) {
list( $sql, $time, $caller ) = $query;
if ( $time > $slow_threshold && strpos( $caller, 'bp_' ) !== false ) {
$log_entry = sprintf(
"[%s] %.4fs | %s | %s\n",
current_time( 'mysql' ),
$time,
trim( preg_replace( '/\s+/', ' ', $sql ) ),
$caller
);
error_log( $log_entry, 3, WP_CONTENT_DIR . '/bp-slow-queries.log' );
}
}
}
add_action( 'shutdown', 'bpcd_log_slow_queries' );
/* ============================================
Performance Configuration for BuddyPress
============================================ */
/* Memory — BuddyPress is memory-hungry, especially with large member lists */
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // Admin memory limit
/* Cron — Disable wp-cron on page loads, use system cron instead */
define( 'DISABLE_WP_CRON', true );
// Add to crontab: */2 * * * * wget -q -O - https://yourdomain.com/wp-cron.php
/* Post revisions — Reduce DB bloat */
define( 'WP_POST_REVISIONS', 5 );
/* Autosave interval — Reduce ajax requests */
define( 'AUTOSAVE_INTERVAL', 120 ); // 2 minutes instead of 60s
/* Trash — Auto-delete trashed items after 14 days */
define( 'EMPTY_TRASH_DAYS', 14 );
/* Concatenate admin scripts — Fewer HTTP requests in admin */
define( 'CONCATENATE_SCRIPTS', true );
/* Disable file editing — Security + slight performance gain */
define( 'DISALLOW_FILE_EDIT', true );
/* Table repair optimization */
define( 'WP_ALLOW_REPAIR', false ); // Disable in production
# System cron for WordPress — runs every 2 minutes
# Edit with: crontab -e
*/2 * * * * /usr/bin/php /var/www/yourdomain.com/wp-cron.php > /dev/null 2>&1
# Or using WP-CLI for more reliable execution:
*/2 * * * * cd /var/www/yourdomain.com && /usr/local/bin/wp cron event run --due-now > /dev/null 2>&1
/**
* Configure HyperDB for read/write splitting.
* Place this in db-config.php (not wp-config.php).
*/
$wpdb->add_database( array(
'host' => 'primary-db.yourserver.com',
'user' => 'wp_user',
'password' => 'secure_password',
'name' => 'wp_buddypress',
'write' => 1, // Primary handles all writes
'read' => 1, // Primary also handles some reads
'dataset' => 'global',
'timeout' => 0.5,
) );
$wpdb->add_database( array(
'host' => 'replica-db.yourserver.com',
'user' => 'wp_reader',
'password' => 'secure_password',
'name' => 'wp_buddypress',
'write' => 0, // Replica never writes
'read' => 2, // Higher priority for reads
'dataset' => 'global',
'timeout' => 0.5,
) );
-- Partition bp_activity by month for fast range scans
ALTER TABLE wp_bp_activity
PARTITION BY RANGE (YEAR(date_recorded) * 100 + MONTH(date_recorded)) (
PARTITION p202501 VALUES LESS THAN (202502),
PARTITION p202502 VALUES LESS THAN (202503),
PARTITION p202503 VALUES LESS THAN (202504),
PARTITION p202504 VALUES LESS THAN (202505),
PARTITION p202505 VALUES LESS THAN (202506),
PARTITION p202506 VALUES LESS THAN (202507),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Activity stream queries now only scan recent partitions
-- A query for "last 20 activities" hits 1-2 partitions instead of the entire table
# Nginx FastCGI Cache configuration for BuddyPress
fastcgi_cache_path /var/cache/nginx/bp levels=1:2
keys_zone=bp_cache:100m
inactive=60m
max_size=2g;
server {
# ... your server block ...
# Cache key — includes scheme, host, and request URI
set $skip_cache 0;
# Skip cache for logged-in users (BuddyPress personalizes content)
if ($http_cookie ~* "wordpress_logged_in_") {
set $skip_cache 1;
}
# Skip cache for BuddyPress AJAX requests
if ($request_uri ~* "/wp-admin/admin-ajax.php") {
set $skip_cache 1;
}
# Skip cache for BuddyPress activity post submissions
if ($request_method = POST) {
set $skip_cache 1;
}
location ~ \.php$ {
fastcgi_cache bp_cache;
fastcgi_cache_valid 200 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
# ... fastcgi_pass and other params ...
}
}
/**
* Disable BuddyPress components you are not using.
* Each disabled component eliminates its database queries entirely.
* Add to your theme's functions.php or a custom plugin.
*/
function bpcd_disable_unused_components( $components ) {
// Remove components you do not need
$disable = array(
'blogs', // Blog tracking (rarely needed on single-site)
'settings', // If you use a custom settings page
);
foreach ( $disable as $component ) {
unset( $components[ $component ] );
}
return $components;
}
add_filter( 'bp_active_components', 'bpcd_disable_unused_components' );
/**
* Limit activity queries to the last 90 days by default.
* Prevents full-table scans on large activity tables.
*/
function bpcd_limit_activity_date_range( $args ) {
if ( empty( $args['date_query'] ) ) {
$args['date_query'] = array(
array(
'after' => '90 days ago',
'inclusive' => true,
),
);
}
return $args;
}
add_filter( 'bp_after_has_activities_parse_args', 'bpcd_limit_activity_date_range' );
/**
* Add native lazy loading to BuddyPress avatars.
* Reduces initial page weight significantly for member directories.
*/
function bpcd_lazy_load_avatars( $avatar_html ) {
// Add loading="lazy" to avatar img tags
if ( strpos( $avatar_html, 'loading=' ) === false ) {
$avatar_html = str_replace( '<img ', '<img loading="lazy" decoding="async" ', $avatar_html );
}
return $avatar_html;
}
add_filter( 'bp_core_fetch_avatar', 'bpcd_lazy_load_avatars' );
/**
* Serve WebP avatars when the browser supports them.
*/
function bpcd_webp_avatars( $avatar_url ) {
$webp_path = preg_replace( '/\.(jpg|jpeg|png)$/i', '.webp', $avatar_url );
// Check if the WebP version exists
$upload_dir = wp_upload_dir();
$local_path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $webp_path );
if ( file_exists( $local_path ) ) {
return $webp_path;
}
return $avatar_url;
}
add_filter( 'bp_core_fetch_avatar_url', 'bpcd_webp_avatars' );
/**
* Defer non-critical BuddyPress JavaScript.
* Improves First Contentful Paint by ~200-400ms.
*/
function bpcd_defer_bp_scripts( $tag, $handle ) {
$defer_handles = array(
'bp-confirm',
'bp-widget-members',
'bp-jquery-query',
'bp-mentions',
);
if ( in_array( $handle, $defer_handles, true ) ) {
return str_replace( ' src', ' defer src', $tag );
}
return $tag;
}
add_filter( 'script_loader_tag', 'bpcd_defer_bp_scripts', 10, 2 );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment