I’ll build this as a serious WordPress theme module for rx-theme/performance/font-optimization.php, focused on safe font loading, preload/preconnect, Google Fonts handling, local font support, CSS variables, admin-configurable filters, and performance fallbacks.
Yes friend — create this file:
rx-theme/performance/font-optimization.php
Then include it from functions.php like this:
require_once get_template_directory() . '/performance/font-optimization.php';
This module uses WordPress hooks such as wp_enqueue_scripts, wp_head, wp_resource_hints, and stylesheet tag filters. WordPress supports adding stylesheet metadata with wp_style_add_data(), and modern font performance guidance recommends limiting font variants, using font-display, preloading only critical fonts, and using preconnect with crossorigin for font file origins.
<?php
/**
* RX Theme - Advanced Font Optimization
*
* File: /performance/font-optimization.php
*
* Features:
* - Local font-face generation
* - Critical font preloading
* - Google Fonts optimized loading
* - Google Fonts display=swap enforcement
* - Preconnect and DNS-prefetch resource hints
* - Optional async Google Fonts loading
* - Optional remove WordPress emoji font/script overhead
* - Font CSS variables
* - Fallback font stack control
* - Font MIME/type detection
* - Admin-safe and frontend-safe loading
* - Developer filters for future extension
*
* @package RX_Theme
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'RX_Theme_Font_Optimization' ) ) :
final class RX_Theme_Font_Optimization {
/**
* Theme version fallback.
*
* @var string
*/
private static $version = '1.0.0';
/**
* Whether inline font CSS has been printed.
*
* @var bool
*/
private static $printed_inline_css = false;
/**
* Initialize hooks.
*
* @return void
*/
public static function init() {
self::$version = defined( 'RX_THEME_VERSION' ) ? RX_THEME_VERSION : wp_get_theme()->get( 'Version' );
add_action( 'after_setup_theme', array( __CLASS__, 'theme_supports' ), 5 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_fonts' ), 5 );
add_action( 'wp_head', array( __CLASS__, 'print_preload_links' ), 1 );
add_action( 'wp_head', array( __CLASS__, 'print_early_font_css' ), 2 );
add_action( 'wp_head', array( __CLASS__, 'print_noscript_google_fonts' ), 50 );
add_filter( 'wp_resource_hints', array( __CLASS__, 'resource_hints' ), 10, 2 );
add_filter( 'style_loader_tag', array( __CLASS__, 'optimize_style_loader_tag' ), 10, 4 );
add_filter( 'style_loader_src', array( __CLASS__, 'force_google_fonts_display_swap' ), 10, 2 );
add_action( 'init', array( __CLASS__, 'maybe_disable_emoji_assets' ), 20 );
}
/**
* Add theme supports related to editor typography.
*
* @return void
*/
public static function theme_supports() {
add_theme_support( 'editor-styles' );
}
/**
* Main configuration.
*
* Change values here or override with:
* add_filter( 'rx_font_optimization_config', function( $config ) { return $config; } );
*
* @return array
*/
public static function config() {
$config = array(
'enable_local_fonts' => true,
'enable_google_fonts' => true,
'enable_google_fonts_async' => false,
'enable_google_fonts_noscript' => true,
'enable_preload' => true,
'enable_preconnect' => true,
'enable_dns_prefetch' => true,
'enable_font_css_variables' => true,
'enable_emoji_cleanup' => true,
'enable_editor_fonts' => true,
'enable_admin_bar_debug_comment' => false,
/**
* Keep preload small.
* Only preload the most important above-the-fold fonts.
*/
'max_preload_fonts' => 4,
/**
* Font display option:
* swap = usually best for performance and readability.
* optional = more aggressive performance, may skip custom font on slow network.
* fallback = short invisible period, then fallback.
* block = not recommended for performance.
*/
'font_display' => 'swap',
/**
* Main font stacks.
*/
'font_stacks' => array(
'body' => '"RX Inter", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, "Noto Sans", "Helvetica Neue", sans-serif',
'heading' => '"RX Inter", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, "Noto Sans", sans-serif',
'mono' => '"RX Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',
'bangla' => '"Noto Sans Bengali", "SolaimanLipi", "Siyam Rupali", Vrinda, sans-serif',
),
/**
* Google font families.
* Keep weights limited for speed.
*/
'google_fonts' => array(
array(
'family' => 'Inter',
'weights' => array( '400', '500', '600', '700' ),
'display' => 'swap',
),
array(
'family' => 'Noto Sans Bengali',
'weights' => array( '400', '500', '600', '700' ),
'display' => 'swap',
),
),
/**
* Local fonts.
* Put files here:
* /rx-theme/assets/fonts/
*
* Example:
* /assets/fonts/inter/inter-regular.woff2
*/
'local_fonts' => array(
array(
'font_family' => 'RX Inter',
'font_style' => 'normal',
'font_weight' => '400',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'inter/inter-regular.woff2',
'format' => 'woff2',
),
),
'preload' => true,
'unicode_range'=> '',
),
array(
'font_family' => 'RX Inter',
'font_style' => 'normal',
'font_weight' => '600',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'inter/inter-semibold.woff2',
'format' => 'woff2',
),
),
'preload' => true,
'unicode_range'=> '',
),
array(
'font_family' => 'RX Inter',
'font_style' => 'normal',
'font_weight' => '700',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'inter/inter-bold.woff2',
'format' => 'woff2',
),
),
'preload' => false,
'unicode_range'=> '',
),
array(
'font_family' => 'RX Mono',
'font_style' => 'normal',
'font_weight' => '400',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'mono/rx-mono-regular.woff2',
'format' => 'woff2',
),
),
'preload' => false,
'unicode_range'=> '',
),
),
);
return apply_filters( 'rx_font_optimization_config', $config );
}
/**
* Enqueue Google Fonts and editor font CSS.
*
* @return void
*/
public static function enqueue_fonts() {
$config = self::config();
if ( ! empty( $config['enable_google_fonts'] ) ) {
$google_url = self::build_google_fonts_url();
if ( $google_url ) {
wp_register_style(
'rx-google-fonts',
$google_url,
array(),
null
);
wp_enqueue_style( 'rx-google-fonts' );
}
}
if ( ! empty( $config['enable_font_css_variables'] ) ) {
wp_register_style( 'rx-font-variables', false, array(), self::$version );
wp_enqueue_style( 'rx-font-variables' );
wp_add_inline_style( 'rx-font-variables', self::build_font_variable_css() );
}
}
/**
* Build Google Fonts URL.
*
* @return string
*/
private static function build_google_fonts_url() {
$config = self::config();
if ( empty( $config['google_fonts'] ) || ! is_array( $config['google_fonts'] ) ) {
return '';
}
$families = apply_filters( 'rx_font_optimization_google_families', $config['google_fonts'] );
if ( empty( $families ) || ! is_array( $families ) ) {
return '';
}
$query_families = array();
foreach ( $families as $font ) {
if ( empty( $font['family'] ) ) {
continue;
}
$family = str_replace( ' ', '+', sanitize_text_field( $font['family'] ) );
$weights = array();
if ( ! empty( $font['weights'] ) && is_array( $font['weights'] ) ) {
foreach ( $font['weights'] as $weight ) {
$weight = preg_replace( '/[^0-9]/', '', (string) $weight );
if ( $weight ) {
$weights[] = $weight;
}
}
}
$weights = array_unique( $weights );
if ( ! empty( $weights ) ) {
$query_families[] = $family . ':wght@' . implode( ';', $weights );
} else {
$query_families[] = $family;
}
}
if ( empty( $query_families ) ) {
return '';
}
$args = array(
'family' => implode( '&family=', $query_families ),
'display' => ! empty( $config['font_display'] ) ? sanitize_key( $config['font_display'] ) : 'swap',
);
$url = 'https://fonts.googleapis.com/css2?' . self::build_google_query_string( $args );
return esc_url_raw( apply_filters( 'rx_font_optimization_google_fonts_url', $url, $args ) );
}
/**
* Build Google Fonts query string while preserving repeated family params.
*
* @param array $args Query args.
* @return string
*/
private static function build_google_query_string( $args ) {
$family_part = '';
if ( ! empty( $args['family'] ) ) {
$families = explode( '&family=', $args['family'] );
$encoded = array();
foreach ( $families as $family ) {
$encoded[] = 'family=' . rawurlencode( $family );
}
$family_part = implode( '&', $encoded );
}
$display = ! empty( $args['display'] ) ? '&display=' . rawurlencode( $args['display'] ) : '';
return $family_part . $display;
}
/**
* Print preload links for critical local fonts.
*
* @return void
*/
public static function print_preload_links() {
$config = self::config();
if ( empty( $config['enable_preload'] ) ) {
return;
}
$fonts = self::get_local_fonts();
if ( empty( $fonts ) ) {
return;
}
$max = absint( $config['max_preload_fonts'] );
$printed = 0;
echo "\n" . '<!-- RX Font Optimization: preload critical fonts -->' . "\n";
foreach ( $fonts as $font ) {
if ( empty( $font['preload'] ) || empty( $font['src'] ) || ! is_array( $font['src'] ) ) {
continue;
}
foreach ( $font['src'] as $source ) {
if ( $printed >= $max ) {
break 2;
}
if ( empty( $source['file'] ) ) {
continue;
}
$url = self::font_file_url( $source['file'] );
if ( ! $url ) {
continue;
}
$type = self::font_mime_type( $source['file'] );
printf(
'<link rel="preload" href="%1$s" as="font" type="%2$s" crossorigin="anonymous">' . "\n",
esc_url( $url ),
esc_attr( $type )
);
$printed++;
}
}
if ( ! empty( $config['enable_admin_bar_debug_comment'] ) && current_user_can( 'manage_options' ) ) {
printf( "<!-- RX Font Optimization: %d font preload link(s) printed -->\n", absint( $printed ) );
}
}
/**
* Print local @font-face early in head.
*
* @return void
*/
public static function print_early_font_css() {
if ( self::$printed_inline_css ) {
return;
}
$config = self::config();
if ( empty( $config['enable_local_fonts'] ) ) {
return;
}
$css = self::build_local_font_face_css();
if ( empty( $css ) ) {
return;
}
self::$printed_inline_css = true;
echo "\n" . '<style id="rx-local-font-face-css">' . "\n";
echo wp_strip_all_tags( $css );
echo "\n" . '</style>' . "\n";
}
/**
* Build local font-face CSS.
*
* @return string
*/
private static function build_local_font_face_css() {
$fonts = self::get_local_fonts();
if ( empty( $fonts ) ) {
return '';
}
$css = '';
foreach ( $fonts as $font ) {
if ( empty( $font['font_family'] ) || empty( $font['src'] ) || ! is_array( $font['src'] ) ) {
continue;
}
$sources = array();
foreach ( $font['src'] as $source ) {
if ( empty( $source['file'] ) ) {
continue;
}
$url = self::font_file_url( $source['file'] );
if ( ! $url ) {
continue;
}
$format = ! empty( $source['format'] ) ? sanitize_key( $source['format'] ) : self::font_format_from_file( $source['file'] );
$sources[] = sprintf(
'url("%1$s") format("%2$s")',
esc_url_raw( $url ),
esc_attr( $format )
);
}
if ( empty( $sources ) ) {
continue;
}
$font_display = ! empty( $font['font_display'] ) ? sanitize_key( $font['font_display'] ) : 'swap';
$font_style = ! empty( $font['font_style'] ) ? sanitize_key( $font['font_style'] ) : 'normal';
$font_weight = ! empty( $font['font_weight'] ) ? sanitize_text_field( $font['font_weight'] ) : '400';
$css .= '@font-face{';
$css .= 'font-family:"' . esc_attr( $font['font_family'] ) . '";';
$css .= 'font-style:' . esc_attr( $font_style ) . ';';
$css .= 'font-weight:' . esc_attr( $font_weight ) . ';';
$css .= 'font-display:' . esc_attr( $font_display ) . ';';
$css .= 'src:' . implode( ',', $sources ) . ';';
if ( ! empty( $font['unicode_range'] ) ) {
$css .= 'unicode-range:' . esc_attr( $font['unicode_range'] ) . ';';
}
$css .= '}' . "\n";
}
return apply_filters( 'rx_font_optimization_local_font_face_css', $css, $fonts );
}
/**
* Build root font CSS variables.
*
* @return string
*/
private static function build_font_variable_css() {
$config = self::config();
if ( empty( $config['font_stacks'] ) || ! is_array( $config['font_stacks'] ) ) {
return '';
}
$body = ! empty( $config['font_stacks']['body'] ) ? $config['font_stacks']['body'] : 'system-ui, sans-serif';
$heading = ! empty( $config['font_stacks']['heading'] ) ? $config['font_stacks']['heading'] : $body;
$mono = ! empty( $config['font_stacks']['mono'] ) ? $config['font_stacks']['mono'] : 'monospace';
$bangla = ! empty( $config['font_stacks']['bangla'] ) ? $config['font_stacks']['bangla'] : $body;
$css = ':root{';
$css .= '--rx-font-body:' . esc_attr( $body ) . ';';
$css .= '--rx-font-heading:' . esc_attr( $heading ) . ';';
$css .= '--rx-font-mono:' . esc_attr( $mono ) . ';';
$css .= '--rx-font-bangla:' . esc_attr( $bangla ) . ';';
$css .= '}';
$css .= 'body{font-family:var(--rx-font-body);}';
$css .= 'h1,h2,h3,h4,h5,h6,.site-title,.entry-title{font-family:var(--rx-font-heading);}';
$css .= 'code,kbd,pre,samp{font-family:var(--rx-font-mono);}';
$css .= ':lang(bn),.rx-bangla{font-family:var(--rx-font-bangla);}';
return apply_filters( 'rx_font_optimization_font_variable_css', $css, $config );
}
/**
* Add preconnect and DNS-prefetch hints.
*
* @param array $urls URLs.
* @param string $relation_type Relation type.
* @return array
*/
public static function resource_hints( $urls, $relation_type ) {
$config = self::config();
if ( 'preconnect' === $relation_type && ! empty( $config['enable_preconnect'] ) ) {
$urls[] = array(
'href' => 'https://fonts.googleapis.com',
'crossorigin' => '',
);
$urls[] = array(
'href' => 'https://fonts.gstatic.com',
'crossorigin' => 'anonymous',
);
}
if ( 'dns-prefetch' === $relation_type && ! empty( $config['enable_dns_prefetch'] ) ) {
$urls[] = '//fonts.googleapis.com';
$urls[] = '//fonts.gstatic.com';
}
$urls = self::unique_resource_hints( $urls );
return apply_filters( 'rx_font_optimization_resource_hints', $urls, $relation_type, $config );
}
/**
* Make resource hints unique.
*
* @param array $urls URLs.
* @return array
*/
private static function unique_resource_hints( $urls ) {
$seen = array();
$output = array();
foreach ( $urls as $url ) {
$key = is_array( $url ) && ! empty( $url['href'] ) ? $url['href'] : (string) $url;
if ( isset( $seen[ $key ] ) ) {
continue;
}
$seen[ $key ] = true;
$output[] = $url;
}
return $output;
}
/**
* Optimize stylesheet link tags.
*
* @param string $html Link tag HTML.
* @param string $handle Style handle.
* @param string $href Style URL.
* @param string $media Media.
* @return string
*/
public static function optimize_style_loader_tag( $html, $handle, $href, $media ) {
$config = self::config();
if ( 'rx-google-fonts' === $handle && ! empty( $config['enable_google_fonts_async'] ) ) {
$html = sprintf(
'<link rel="preload" as="style" href="%1$s" crossorigin="anonymous" onload="this.onload=null;this.rel=\'stylesheet\'"><noscript><link rel="stylesheet" href="%1$s"></noscript>' . "\n",
esc_url( $href )
);
}
if ( false !== strpos( $href, 'fonts.googleapis.com' ) && false === strpos( $html, 'crossorigin' ) ) {
$html = str_replace( '<link ', '<link crossorigin="anonymous" ', $html );
}
return apply_filters( 'rx_font_optimization_style_loader_tag', $html, $handle, $href, $media, $config );
}
/**
* Force display=swap on Google Fonts URLs.
*
* @param string $src Stylesheet source.
* @param string $handle Style handle.
* @return string
*/
public static function force_google_fonts_display_swap( $src, $handle ) {
if ( false === strpos( $src, 'fonts.googleapis.com' ) ) {
return $src;
}
$config = self::config();
$display = ! empty( $config['font_display'] ) ? sanitize_key( $config['font_display'] ) : 'swap';
$src = remove_query_arg( 'display', $src );
$src = add_query_arg( 'display', $display, $src );
return esc_url_raw( $src );
}
/**
* Print noscript fallback for Google Fonts when async mode is enabled.
*
* @return void
*/
public static function print_noscript_google_fonts() {
$config = self::config();
if (
empty( $config['enable_google_fonts'] ) ||
empty( $config['enable_google_fonts_async'] ) ||
empty( $config['enable_google_fonts_noscript'] )
) {
return;
}
$url = self::build_google_fonts_url();
if ( ! $url ) {
return;
}
printf(
'<noscript><link rel="stylesheet" href="%s"></noscript>' . "\n",
esc_url( $url )
);
}
/**
* Optionally remove emoji assets.
*
* @return void
*/
public static function maybe_disable_emoji_assets() {
$config = self::config();
if ( empty( $config['enable_emoji_cleanup'] ) ) {
return;
}
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
add_filter( 'tiny_mce_plugins', array( __CLASS__, 'disable_tinymce_emoji_plugin' ) );
add_filter( 'emoji_svg_url', '__return_false' );
}
/**
* Remove TinyMCE emoji plugin.
*
* @param array $plugins TinyMCE plugins.
* @return array
*/
public static function disable_tinymce_emoji_plugin( $plugins ) {
if ( is_array( $plugins ) ) {
return array_diff( $plugins, array( 'wpemoji' ) );
}
return array();
}
/**
* Get local font list.
*
* @return array
*/
private static function get_local_fonts() {
$config = self::config();
$fonts = ! empty( $config['local_fonts'] ) && is_array( $config['local_fonts'] )
? $config['local_fonts']
: array();
return apply_filters( 'rx_font_optimization_local_fonts', $fonts, $config );
}
/**
* Get local font URL.
*
* @param string $relative_file Relative file path inside /assets/fonts/.
* @return string
*/
private static function font_file_url( $relative_file ) {
$relative_file = ltrim( sanitize_text_field( $relative_file ), '/' );
$relative_file = str_replace( array( '../', '..\\' ), '', $relative_file );
$child_path = trailingslashit( get_stylesheet_directory() ) . 'assets/fonts/' . $relative_file;
$child_url = trailingslashit( get_stylesheet_directory_uri() ) . 'assets/fonts/' . $relative_file;
$parent_path = trailingslashit( get_template_directory() ) . 'assets/fonts/' . $relative_file;
$parent_url = trailingslashit( get_template_directory_uri() ) . 'assets/fonts/' . $relative_file;
if ( file_exists( $child_path ) ) {
return esc_url_raw( $child_url );
}
if ( file_exists( $parent_path ) ) {
return esc_url_raw( $parent_url );
}
/**
* Return URL even if file does not exist locally.
* This helps when assets are deployed to CDN but not present in local filesystem.
*/
return esc_url_raw( apply_filters( 'rx_font_optimization_missing_font_url', $parent_url, $relative_file ) );
}
/**
* Detect font MIME type.
*
* @param string $file File path.
* @return string
*/
private static function font_mime_type( $file ) {
$extension = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
$types = array(
'woff2' => 'font/woff2',
'woff' => 'font/woff',
'ttf' => 'font/ttf',
'otf' => 'font/otf',
'eot' => 'application/vnd.ms-fontobject',
'svg' => 'image/svg+xml',
);
return isset( $types[ $extension ] ) ? $types[ $extension ] : 'font/woff2';
}
/**
* Detect CSS font format.
*
* @param string $file File path.
* @return string
*/
private static function font_format_from_file( $file ) {
$extension = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
$formats = array(
'woff2' => 'woff2',
'woff' => 'woff',
'ttf' => 'truetype',
'otf' => 'opentype',
'eot' => 'embedded-opentype',
'svg' => 'svg',
);
return isset( $formats[ $extension ] ) ? $formats[ $extension ] : 'woff2';
}
}
RX_Theme_Font_Optimization::init();
endif;
Optional override example in functions.php
Use this if your font file names are different:
add_filter( 'rx_font_optimization_config', function( $config ) {
$config['enable_google_fonts'] = true;
$config['enable_google_fonts_async'] = false;
$config['font_display'] = 'swap';
$config['local_fonts'] = array(
array(
'font_family' => 'RX Inter',
'font_style' => 'normal',
'font_weight' => '400',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'inter/inter-regular.woff2',
'format' => 'woff2',
),
),
'preload' => true,
),
array(
'font_family' => 'RX Inter',
'font_style' => 'normal',
'font_weight' => '700',
'font_display' => 'swap',
'src' => array(
array(
'file' => 'inter/inter-bold.woff2',
'format' => 'woff2',
),
),
'preload' => false,
),
);
return $config;
} );
Recommended folder
rx-theme/
├── performance/
│ └── font-optimization.php
└── assets/
└── fonts/
├── inter/
│ ├── inter-regular.woff2
│ ├── inter-semibold.woff2
│ └── inter-bold.woff2
└── mono/
└── rx-mono-regular.woff2
Only preload 1–4 most important fonts. Too many font preloads can make performance worse because the browser competes with CSS, JavaScript, and images for early network priority.