How to achieve the maximum speed of WordPress Gutenberg website?

In the last article, I wrote about how we managed to maximize the Google Insights score for a WordPress Gutenberg website. This is a step-by-step guide for developers on how exactly you can achieve that. How to achieve the maximum speed of WordPress Gutenberg website? Let's do it!

optimized desktop speed test
optimized mobile speed test

How did we do all that?
Now let’s go through each error one by one and see how we can go around the opportunities that Google suggests.

Removing unused CSS code

Usually, CSS code is loaded as a single, minified CSS file. This file contains all styles for all blocks, sections, and elements of a website. This means that e.g. a homepage loads blog post styles even though it doesn’t use them.

Wouldn’t it be nice to load styles only for blocks that actually exist on a given page? It sure would!

The code we write is modular. It means it is divided into separate, independent blocks. These blocks are used to build every page and can be placed anywhere on the website independently. Let’s have a look.

An exemplary block structure in the footer.

An exemplary block structure in the footer.

A sample core-embed block in the backend (WordPress admin):

Gutenberg Embed Block

Enter link to an embedded video

After providing the URL
Adding a block in the backend is simple and intuitive. Let’s go through particular files in the core-embed folder.

Adding a block in the backend is simple and intuitive. Let’s go through particular files in the core-embed folder.

  • index.js - JS code of our block that is minified to index.min.js
  • index.min.js - minified JS code that is executed to register.php
  • - JS code with additional polyfills for Internet Explorer support (it’s executed instead of index.min.js in IE)
  • register.php - In this file, functions are called in order to execute JS on the frontend and CSS files (style-editor.css) on the backend of the website. It also has a function that introduces in the block a special element <div data-styles-id=”core-embed”>. This element is replaced by inline CSS stiles of a given block; the one below, to be specific:
add_filter( 'render_block', 'wrap_embed_block', 10, 2 );
function wrap_embed_block( $block_content, $block ) {
  if ( 'core-embed/youtube' === $block['blockName'] || 'core-embed/wordpress' === $block['blockName'] ) {
    $block_content = '<div data-styles-id="core-embed"></div>'. $block_content;
  return $block_content;

function core_block_registration_embed() {
    if( is_admin() ) {
            get_template_directory_uri() . '/parts/blocks/core-embed/style-editor.css',
            array( 'wp-edit-blocks' )
add_action( 'enqueue_block_editor_assets', 'core_block_registration_embed' );

function register_block_embed_scripts() {
    if ( Block::if_block_exists( 'wp-block-embed' ) ) {
            get_template_directory_uri() . '/parts/blocks/core-embed/index.min.js#asyncload',
add_action( 'enqueue_block_assets', 'register_block_embed_scripts' );
  • style.scss - CSS styles of a given block
  • style.css - minigied CSS styles, added inline to the website frontend
  • style-editor.scss - CSS styles of a given block, required in the backend (admin panel)
  • style-editor.css - minified CSS styles of a given block that are executed with the core_block_registration_embed function in the register.php file.

But what about the unused CSS?

ACF Blocks

Because each block has its own CSS file, you can add styles inline to every block using a dedicated function. This function is added to the functions.php file and is used in blocks and components created with the ACF plugin.

function loadStyles($path, $name, $file_name = 'style', $echo = true){
    global $blocksLoaded;

    if (!in_array($name, $blocksLoaded)) {
        $html = '';
        $file = "$path/$file_name.css";

        if (file_exists($file)) {
            $style_content = file_get_contents($file);

            if ($style_content !== '') {
                $html = '<style>' . $style_content .'</style>';
                array_push($blocksLoaded, $name);

                if ($echo) {
                    echo $html;
                } else {
                    return $html;


Example of calling this function:

$name = "block-content-image";
<section class="block-content-images">
    <?php loadStyles( __DIR__, $name ); ?>

TThis function places <style> tags with the block content and also saves the name of the rendered block in the global array.

Imagine that a user wants to add two sections based on the same block, say a block with images and text. Normally, the page would have to load the exact same styles twice - after all the function is called for within the block. Thanks to the solution above, the styles will be loaded only once if a block with the same name appears more than once on a page, that is its name is already recorded in the global array. They will always load in a block that is higher in the DOM tree, that is in a block that is rendered first. You can see the effect below:

Third party styles and styles of reusable components that repeat in multiple blocks are added in a very similar way. If a given block uses additional styles, e.g. Slick styles (Slick is a useful plugin used to create sliders, e.g. image galleries), then an additional function loadStylesThird is executed. As in the previous example, this function sets style tags with the CSS file content. In this particular case this will be slick.css styles. Then it saves the name of the file in the global array to ensure the styles load only once.

<section class="block-slider">
 <?php loadStyles(__DIR__, $name); ?>
 <?php loadStylesThird('slick'); ?>


This way, you don’t have to load new styles as a separate file or do it on all subpages, even the ones that do not use them.

Custom Blocks

In the case of custom Gutenberg blocks, the situation is a little bit different. Custom Gutenberg blocks are created with JavaScript and it’s necessary to use another function which uses Regex to change a div added to the block into inline CSS.

function load_styles_custom_blocks( $content ) {
     global $blocksLoaded;

    preg_match_all('<div data-styles-id="(.+?)".+?\/div>', $content, $matches);
    $id_array = $matches[1];
    $id_array = array_unique($id_array);

    $path = get_template_directory() . '/parts/blocks';

    foreach ($id_array as $name) {

        $pattern = '/<div data-styles-id="' . $name . '".+?\/div>/';

        if (!in_array($name, $blocksLoaded) &&
            file_exists("$path/$name/style.css")) {
            $style_content = file_get_contents("$path/$name/style.css");

            if ($style_content !== '') {

                $style_tags = '<style>' . $style_content .'</style>';

                // Insert block styles only for the first match in the content
                $content = preg_replace ( $pattern, $style_tags, $content, 1 );

                array_push($blocksLoaded, $name);


        // Delete all remaining matches from the content
        $content = preg_replace ( $pattern, '', $content );


    return $content;
add_filter( 'the_content', 'load_styles_custom_blocks');

Then, add an additional div element with a specified attribute to the save method while registering the block.

 save: ( { attributes } ) => {
        const customClass = 'block-accordion';

        return (
            <div className={ `${customClass}` }>
                <div data-styles-id="custom-accordion" />
                <h2 className="section-heading is-style-underline">{ attributes.title }</h2>
                    <div className={ `${customClass}__body` }>
                    <InnerBlocks.Content />


Core Blocks

For core Gutenberg blocks, you can use the same approach as above using a filter hook. In this example, let’s add styles to the core/heading block.

function insert_heading_styles( $block_content, $block ) {
    if ( 'core/heading' === $block['blockName'] ) {
        $block_content = '<div data-styles-id="core-heading"></div>' . $block_content;
    return $block_content;
add_filter( 'render_block', 'insert_heading_styles', 10, 2 );


This type of solution is sufficient to completely eliminate unused CSS. Obviously, in this particular situation, we still need to load a CSS file with e.g. global body styles. But it is so small that it doesn’t affect the site loading speed.

Eliminating Render-blocking resources

Render-blocking resources are mostly CSS and JS files loaded in the page head. Here’s a typical list of such elements:

Let’s see what we can do about all that.

Firstly, minify CSS, JS, and HTML files.

Many free plugins allow minifying and merging all JS and CSS files. Here are some examples:

Using these plugins with proper settings will significantly decrease the downtime caused by render-blocking. In our case, it was enough for Google to stop showing these errors.

On the other hand, it can happen that minified and merged JS and CSS files will increase the page loading speed. Let’s check how we can completely get rid of these issues.
Using these plugins with proper settings will significantly decrease the downtime caused by render-blocking. In our case, it was enough for Google to stop showing these errors.

On the other hand, it can happen that minified and merged JS and CSS files will increase the page loading speed. Let’s check how we can completely get rid of these issues.

CSS Files

To get rid of the CSS render-blocking files, we can use a function that will preload CSS styles.

function add_rel_preload($html, $handle, $href, $media) {

    $html ="<link rel='preload' as='style' onload='this.onload=null;this.rel=\"stylesheet\"' id='$handle' href='$href' type='text/css' media='all' />";

    return $html;
add_filter( 'style_loader_tag', 'add_rel_preload', 10, 4 );

But then again, preloading styles has its flaws. First of all, it’s not supported by some browsers, including Mozilla Firefox and Internet Explorer. Secondly, using a preloader often causes small delay loading styles which results in flashing raw HTML elements before they load.

Hence, you need to decide for yourself whether you should use preloading or not based on the delay time and how much performance you can gain by getting rid of it. Sometimes it’s just not worth the hassle if the results are more than satisfying in the first place.

JavaScript files

Your site will load faster if the scripts are loaded asynchronously, i.e. execute independently from loading the entire website code. By default, a browser parses and executes scripts instantly in the line it’s been added to, which significantly delays loading the rest of the page code.

To do it, add the async attribute to scripts nested in the document from an external file - a file that is added to the document with an opening <script> tag and that has an src attribute with the right value set. This attribute makes the script load while other site resources are loaded without causing any delays.

<script type="text/javascript" src="//localhost/.../themes/themeName/js/bundle.min.js" async></script>

Remove unused JavaScript

Gutenberg makes it very easy to only load the scripts that are necessary on a given page.

In case of registering a block created with ACF, we achieve it with settings provided by the acf_register_block_type function:

function init_block_gallery_lightbox() {
    if ( ! function_exists( 'acf_register_block' ) ) {

    acf_register_block_type( array(
        'name'              => 'gallery-lightbox',
        'title'             => __( 'Gallery Lightbox', 'themeName' ),
        'description'       => __( 'Gallery Lightbox', 'themeName' ),
        'category'          => 'custom_blocks',
        'mode'              => 'edit',
        'keywords'          => array( 'gallery', 'slider','images','lightbox' ),
        'align'             => 'wide',
        'supports'          => array(
            'align' => array( 'wide', 'full' ),
        'render_template'   => get_template_directory() . '/parts/blocks/acf-gallery-lightbox/index.php',
        'enqueue_assets'  => function () {
                get_template_directory_uri() . '/parts/blocks/acf-gallery-lightbox/index.min.js,
                [ 'slick_script' ],

add_action( 'acf/init', 'init_block_gallery_lightbox' );

With custom or core blocks, you only have to add scripts with a function like this:

function register_block_scripts() {
            get_template_directory_uri() . '/parts/blocks/core-embed/index.min.js,
add_action( 'enqueue_block_assets', register_block_scripts);

This way you can keep track of what scripts are loaded on a page.

Using web browser friendly images formats

Web-friendly formats are jpeg 2000, jpeg, XR, and webp. To generate .webp images you can use plugins like:

To make sure that your browser loads the right image format, go to the Network tab on the page Inspector.

.webp images are much lighter and more browser-friendly, resulting in a much shorter loading time.

Unfortunately, Internet Explorer and Safari versions older than 14.0 don’t offer support for these formats. On the good side, most plugins handle these exceptions and load an appropriate format depending on the browser the site is viewed on.

Lazy loading images

You can use lazy loading to decrease the time before the page becomes available to a user. This feature makes the browser load the necessary assets only when they are needed instead of loading them all at once.

Again, you can achieve it with several plugins.

Alternatively, you can use premade lazy loading libraries. And from WordPress v. 5.5, you don’t have to worry about it at all because lazy loading is available out of the box.

Handling unoptimized and oversized images

Every image uploaded to the site should be as small as possible and its size should be adjusted to the requirements of a user’s device.

The following plugins let you handle lossless image optimization.

These plugins work in the background, so it’s a good practice to install them early on so that you don’t have to do a mass optimization later.

If you don’t want to use plugins, you can use other optimization tools like:

The size of images that load with the page depends mostly on properly set image thumbnails. It’s a common habit to upload camera or stock images straight into the media library. These images are often huge and can drastically decrease page loading speed. That’s why it’s so important to define thumbnails depending on your needs. This is a task for a web developer coding the website.

Server response time

This problem can be usually solved with a caching plugin:

  • Autoptimize
  • W3 Total Cache
  • WP Fastest Cache
  • Hummingbird

If none of these plugins help fix the server response time issue, here are a few more hints:

  • Try turning off plugins that aren’t key to the website functionality,
  • Update PHP to the newest version
  • Consider switching to a more efficient hosting provider.


This was a step-by-step guide on how to maximize your WordPress Gutenberg website performance. Remember, that once you load all external scripts, like Google Tag Manager, Google Optimize, or Analytics, the final result may end up being a little lower. It’s important to keep your theme clean and only install plugins and scripts that are actually important to your website’s functionality. But despite all that, you will notice a huge performance improvement after dealing with the issues above!

If you have any questions, just give us a shout. Our developers are here to assist you.

Check how to test website speed.

Subscribe to our Newsletter and get hand-picked articles

(Monthly updates. Good vibes only!)

The data you submit in the above form will be received and processed by Chop-Chop Sp. z o.o., located at Wloclawska 161, 87-100 Torun, Poland, EU. We will process the data in order to be able to provide you with marketing information. To read more on the way we handle data, please visit our Privacy Policy.