PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
← All changes | includes/modules/Fonts/FontsModule.php +63 -5 1.3.4 → 1.4.1 View file →
@@ -33,9 +33,10 @@
33 33 public function ui_metadata(): array {
34 34 return array(
35 35 'label' => __( 'Fonts', 'xspeed' ),
36 36 'icon' => 'Type',
37 - 'description' => __( 'Stop web fonts from blocking text. Adds display=swap to Google Fonts and preloads the fonts you mark critical.', 'xspeed' ),
37 + 'description' => __( 'Shows text right away while web fonts load, and preloads key fonts.', 'xspeed' ),
38 + 'group' => 'performance',
38 39 );
39 40 }
40 41
41 42 public function settings_schema(): array {
@@ -42,17 +43,17 @@
42 43 return array(
43 44 'font_display_swap' => array(
44 45 'type' => 'bool',
45 46 'default' => true,
46 - 'label' => __( 'Add font-display: swap', 'xspeed' ),
47 - 'description' => __( 'Append display=swap to Google Fonts URLs so text renders immediately in a fallback face while the web font loads. Blocking values already on the URL (auto, block) are rewritten to swap; a deliberate non-blocking choice (fallback, optional) is left alone.', 'xspeed' ),
47 + 'label' => __( 'Show text while fonts load', 'xspeed' ),
48 + 'description' => __( 'Text in Google Fonts, and in fonts added to WordPress itself, shows at once in a standard font, then switches when the web font arrives.', 'xspeed' ),
48 49 ),
49 50 'preload_fonts' => array(
50 51 'type' => 'list',
51 52 'default' => array(),
52 53 'item_type' => 'url',
53 - 'label' => __( 'Preload Font URLs', 'xspeed' ),
54 - 'description' => __( 'One absolute font URL per line (woff2/woff/ttf/otf). Each becomes a <link rel="preload" as="font" crossorigin> in the head so the browser starts downloading before the CSS parses. Use only for fonts that render above the fold.', 'xspeed' ),
54 + 'label' => __( 'Fonts to preload', 'xspeed' ),
55 + 'description' => __( 'One full font file URL per line (woff2, woff, ttf or otf). The browser fetches these first, so list only fonts used at the top of the page.', 'xspeed' ),
55 56 ),
56 57 );
57 58 }
58 59
@@ -93,8 +94,27 @@
93 94 $opts = $this->get_settings();
94 95
95 96 if ( ! empty( $opts['font_display_swap'] ) ) {
96 97 add_filter( 'style_loader_tag', array( __CLASS__, 'inject_display_swap' ), 10, 2 );
98 +
99 + // Fonts added through the Font Library or theme.json are printed
100 + // by core with font-display: fallback, and core offers no filter
101 + // for it. Fallback hides the text for up to 100ms while the font
102 + // loads; when that text is the LCP, the hero paints after the
103 + // font instead of at first paint. Measured on a live text hero:
104 + // LCP landed ~85ms after FCP with fallback and on FCP with swap,
105 + // CLS unchanged, and PageSpeed mobile moved between 81 and 90
106 + // depending on which side of that window the font fell. So core's
107 + // own printer runs with swap as the default. Only an exact
108 + // priority-50 hook is replaced: anything that already moved or
109 + // removed it is left alone.
110 + if ( function_exists( 'wp_print_font_faces' )
111 + && class_exists( '\WP_Font_Face_Resolver' )
112 + && 50 === has_action( 'wp_head', 'wp_print_font_faces' )
113 + ) {
114 + remove_action( 'wp_head', 'wp_print_font_faces', 50 );
115 + add_action( 'wp_head', array( __CLASS__, 'print_font_faces_swap' ), 50 );
116 + }
97 117 }
98 118
99 119 if ( ! empty( $opts['preload_fonts'] ) ) {
100 120 add_action(
@@ -159,8 +179,46 @@
159 179 $separator = ( false === strpos( $href_decoded, '?' ) ) ? '?' : '&';
160 180 $new_href = $href . $separator . 'display=swap';
161 181
162 182 return str_replace( $href, $new_href, $tag );
183 + }
184 +
185 + /**
186 + * Print core's font faces as wp_print_font_faces() does, with swap as
187 + * the font-display default.
188 + */
189 + public static function print_font_faces_swap(): void {
190 + $fonts = \WP_Font_Face_Resolver::get_fonts_from_theme_json();
191 + if ( empty( $fonts ) ) {
192 + return;
193 + }
194 + // WordPress 6.4+. Only hooked when the function exists (see
195 + // boot_on_init()), called by name so a 6.0 floor stays compatible.
196 + call_user_func( 'wp_print_font_faces', self::default_display_swap( $fonts ) );
197 + }
198 +
199 + /**
200 + * Give every font face without its own font-display a swap one.
201 + *
202 + * The resolver only sets font-display when a theme.json fontFace
203 + * declares fontDisplay, so a face that has one was chosen on purpose
204 + * and keeps it. Public + static so the test suite can drive it.
205 + *
206 + * @param array<int|string,mixed> $fonts Font families, each a list of faces.
207 + * @return array<int|string,mixed>
208 + */
209 + public static function default_display_swap( array $fonts ): array {
210 + foreach ( $fonts as $family => $faces ) {
211 + if ( ! is_array( $faces ) ) {
212 + continue;
213 + }
214 + foreach ( $faces as $i => $face ) {
215 + if ( is_array( $face ) && ! isset( $face['font-display'] ) ) {
216 + $fonts[ $family ][ $i ]['font-display'] = 'swap';
217 + }
218 + }
219 + }
220 + return $fonts;
163 221 }
164 222
165 223 /**
166 224 * Render the preload <link> markup for a list of font URLs.