diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md b/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md index 5df98a568fbcd..c65d2ec46f8bc 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets.md @@ -130,6 +130,9 @@ function gutenberg_examples_02_register_block() { filemtime( plugin_dir_path( __FILE__ ) . 'style.css' ) ); + // Allow inlining small stylesheets on the frontend if possible. + wp_style_add_data( 'gutenberg-examples-02', 'path', dirname( __FILE__ ) . '/style.css' ); + register_block_type( 'gutenberg-examples/example-02-stylesheets', array( 'apiVersion' => 2, 'style' => 'gutenberg-examples-02', diff --git a/lib/blocks.php b/lib/blocks.php index 92b8bca5430b5..03109903da02b 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -180,6 +180,9 @@ function gutenberg_register_core_block_styles( $block_name ) { filemtime( gutenberg_dir_path() . $style_path ) ); wp_style_add_data( "wp-block-{$block_name}", 'rtl', 'replace' ); + + // Add a reference to the stylesheet's path to allow calculations for inlining styles in `wp_head`. + wp_style_add_data( "wp-block-{$block_name}", 'path', gutenberg_dir_path() . $style_path ); } if ( file_exists( gutenberg_dir_path() . $editor_style_path ) ) { @@ -193,6 +196,105 @@ function gutenberg_register_core_block_styles( $block_name ) { } } +/** + * Change the way styles get loaded depending on their size. + * + * Optimizes performance and sustainability of styles by inlining smaller stylesheets. + * + * @return void + */ +function gutenberg_maybe_inline_styles() { + + $total_inline_limit = 20000; + /** + * The maximum size of inlined styles in bytes. + * + * @param int $total_inline_limit The file-size threshold, in bytes. Defaults to 20000. + * @return int The file-size threshold, in bytes. + */ + $total_inline_limit = apply_filters( 'styles_inline_size_limit', $total_inline_limit ); + + global $wp_styles; + $styles = array(); + + // Build an array of styles that have a path defined. + foreach ( $wp_styles->queue as $handle ) { + if ( wp_styles()->get_data( $handle, 'path' ) && file_exists( $wp_styles->registered[ $handle ]->extra['path'] ) ) { + $block_styles = false; + $styles_size = filesize( $wp_styles->registered[ $handle ]->extra['path'] ); + + // Minify styles and get their minified size if SCRIPT_DEBUG is not enabled. + if ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) { + // Get the styles and minify them by removing comments & whitespace. + $block_styles = gutenberg_get_minified_styles( file_get_contents( $wp_styles->registered[ $handle ]->extra['path'] ) ); + // Get the styles size. + $styles_size = strlen( $block_styles ); + } + + $styles[] = array( + 'handle' => $handle, + 'path' => $wp_styles->registered[ $handle ]->extra['path'], + 'size' => $styles_size, + 'css' => $block_styles, + ); + } + } + + if ( ! empty( $styles ) ) { + // Reorder styles array based on size. + usort( + $styles, + function( $a, $b ) { + return ( $a['size'] <= $b['size'] ) ? -1 : 1; + } + ); + + /** + * The total inlined size. + * + * On each iteration of the loop, if a style gets added inline the value of this var increases + * to reflect the total size of inlined styles. + */ + $total_inline_size = 0; + + // Loop styles. + foreach ( $styles as $style ) { + + // Size check. Since styles are ordered by size, we can break the loop. + if ( $total_inline_size + $style['size'] > $total_inline_limit ) { + break; + } + + // Get the styles if we don't already have them. + $style['css'] = $style['css'] ? $style['css'] : file_get_contents( $style['path'] ); + + // Set `src` to `false` and add styles inline. + $wp_styles->registered[ $style['handle'] ]->src = false; + $wp_styles->registered[ $style['handle'] ]->extra['after'][] = $style['css']; + + // Add the styles size to the $total_inline_size var. + $total_inline_size += (int) $style['size']; + } + } +} +add_action( 'wp_head', 'gutenberg_maybe_inline_styles', 1 ); + +/** + * Minify styles. + * + * Removes inline comments and whitespace. + * + * @param string $styles The styles to be minified. + * @return string + */ +function gutenberg_get_minified_styles( $styles ) { + $re1 = '(?sx)("(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\')|/\\* (?> .*? \\*/ )'; + $re2 = '(?six)("(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\')|\\s*+ ; \\s*+ ( } ) \\s*+|\\s*+ ( [*$~^|]?+= | [{};,>~+-] | !important\\b ) \\s*+|( [[(:] ) \\s++|\\s++ ( [])] )|\\s++ ( : ) \\s*+(?!(?>[^{}"\']++|"(?:[^"\\\\]++|\\\\.)*+"|\'(?:[^\'\\\\]++|\\\\.)*+\')*+{)|^ \\s++ | \\s++ \\z|(\\s)\\s+'; + + $styles = preg_replace( "%$re1%", '$1', $styles ); + return preg_replace( "%$re2%", '$1$2$3$4$5$6$7', $styles ); +} + /** * Complements the implementation of block type `core/social-icon`, whether it * be provided by core or the plugin, with derived block types for each diff --git a/lib/client-assets.php b/lib/client-assets.php index 5115cb990e684..764928d949d3e 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -357,6 +357,7 @@ function gutenberg_register_packages_styles( $styles ) { filemtime( gutenberg_dir_path() . 'build/block-library/' . $block_library_filename . '.css' ) ); $styles->add_data( 'wp-block-library', 'rtl', 'replace' ); + $styles->add_data( 'wp-block-library', 'path', gutenberg_dir_path() . 'build/block-library/' . $block_library_filename . '.css' ); gutenberg_override_style( $styles, diff --git a/phpunit/class-gutenberg-utils-test.php b/phpunit/class-gutenberg-utils-test.php index bac88678ea826..ed7e5ebea9fd6 100644 --- a/phpunit/class-gutenberg-utils-test.php +++ b/phpunit/class-gutenberg-utils-test.php @@ -127,4 +127,35 @@ public function test_invalid_parameters_set() { array( 'a' => 2 ) ); } + + /** + * Test gutenberg_get_minified_styles(). + */ + public function test_gutenberg_get_minified_styles() { + $cases = array( + array( + 'in' => ' +/** + * Comment + */ + .foo { + bar: 1; + } + ', + 'out' => '.foo{bar:1}', + ), + array( + 'in' => '/* Comment */#foo{content:" "; bar: 0; + }', + 'out' => '#foo{content:" ";bar:0}', + ), + ); + + foreach ( $cases as $case ) { + $this->assertSame( + gutenberg_get_minified_styles( $case['in'] ), + $case['out'] + ); + } + } }