PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 All 78 releases
ablocks / includes / classes / critical-css.php

critical-css.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder trunk, at includes/classes/critical-css.php

141 lines 4.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Classes;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Split combined page CSS into a "critical" subset (inlined in <head>) and a
10 * deferred remainder (loaded non-blocking).
11 *
12 * The split is by CSS PROPERTY, not by selector. A rule is critical if it sets
13 * anything that affects layout/box size (dimensions, spacing, position, flex/
14 * grid, typography metrics, …); only rules that are purely cosmetic (color,
15 * background, shadow, radius, transition, filter, opacity) or interaction
16 * states (:hover/:focus/:active) and @keyframes are deferred. Because the
17 * deferred CSS changes nothing that occupies space, applying it after paint
18 * cannot cause a layout shift (CLS) — which the earlier selector-prefix split
19 * did, since it deferred per-block spacing/sizing.
20 *
21 * Parsing respects brace depth so nested at-rules (@media/@supports) stay whole.
22 * The heuristic is deliberately biased toward "critical": a rule we can't prove
23 * is cosmetic stays inlined (safe but slightly less deferral).
24 */
25 class CriticalCss {
26
27 /**
28 * Property-name substrings that affect layout/box size. Over-inclusive on
29 * purpose (e.g. matches background-position) — false positives only cost a
30 * little deferral; a miss would cost CLS.
31 */
32 const LAYOUT_TOKENS = [
33 'width', 'height', 'margin', 'padding', 'border', 'inset', 'top', 'bottom', 'left', 'right',
34 'display', 'position', 'float', 'clear', 'flex', 'grid', 'gap', 'align', 'justify', 'place', 'order',
35 'font', 'spacing', 'text-align', 'text-indent', 'text-transform', 'white-space', 'vertical-align',
36 'writing-mode', 'direction', 'box-sizing', 'column', 'overflow', 'list-style', 'aspect-ratio', 'tab-size',
37 ];
38
39 /**
40 * @param string $css Minified combined CSS.
41 * @return array [ critical_css, rest_css ]
42 */
43 public static function split( $css ) {
44 $css = (string) $css;
45 if ( '' === trim( $css ) ) {
46 return [ '', '' ];
47 }
48
49 $critical = '';
50 $rest = '';
51 $len = strlen( $css );
52 $i = 0;
53 $buffer = '';
54
55 while ( $i < $len ) {
56 $char = $css[ $i ];
57 $buffer .= $char;
58
59 if ( '{' === $char ) {
60 $prelude = trim( substr( $buffer, 0, -1 ) );
61 $depth = 1;
62 $i++;
63 while ( $i < $len && $depth > 0 ) {
64 $c = $css[ $i ];
65 $buffer .= $c;
66 if ( '{' === $c ) {
67 $depth++;
68 } elseif ( '}' === $c ) {
69 $depth--;
70 }
71 $i++;
72 }
73
74 if ( self::is_critical( $prelude, $buffer ) ) {
75 $critical .= $buffer;
76 } else {
77 $rest .= $buffer;
78 }
79 $buffer = '';
80 continue;
81 }
82 $i++;
83 }
84 $rest .= $buffer; // trailing text without a block (shouldn't happen)
85
86 return [ $critical, $rest ];
87 }
88
89 private static function is_critical( $prelude, $rule_text ) {
90 if ( '' === $prelude ) {
91 return false;
92 }
93
94 // At-rules.
95 if ( '@' === $prelude[0] ) {
96 if ( 0 === stripos( $prelude, '@keyframes' ) || 0 === stripos( $prelude, '@-webkit-keyframes' ) ) {
97 return false; // animations never affect initial layout
98 }
99 if ( 0 === stripos( $prelude, '@font-face' ) ) {
100 return true; // fonts affect text metrics
101 }
102 if ( 0 === stripos( $prelude, '@media' ) || 0 === stripos( $prelude, '@supports' ) ) {
103 return self::has_layout( $rule_text ); // critical only if it changes layout
104 }
105 return true; // @import/@charset/other — keep (safe)
106 }
107
108 // Interaction states can't shift the initial layout — defer them.
109 if ( preg_match( '/:(hover|focus|active|focus-within|focus-visible)\b/i', $prelude ) ) {
110 return false;
111 }
112
113 return self::has_layout( $rule_text );
114 }
115
116 /**
117 * True if the rule text declares any layout-affecting property.
118 */
119 private static function has_layout( $rule_text ) {
120 // Property names appear right after '{' or ';'. This avoids matching
121 // pseudo-selectors and colons inside values (url(), data:, http:).
122 if ( ! preg_match_all( '/[{;]\s*(-?[a-z][a-z-]*)\s*:/i', $rule_text, $m ) ) {
123 return false;
124 }
125 foreach ( $m[1] as $prop ) {
126 $prop = strtolower( $prop );
127 // Props that merely contain a layout token but are purely cosmetic:
128 // border-radius / *-color / *-style (incl. list-style, outline-*).
129 if ( preg_match( '/(-radius|-color|-style)$/', $prop ) ) {
130 continue;
131 }
132 foreach ( self::LAYOUT_TOKENS as $token ) {
133 if ( false !== strpos( $prop, $token ) ) {
134 return true;
135 }
136 }
137 }
138 return false;
139 }
140 }
141