How to make better web vitals for wordpress theme or plugin [check list for developers]
I decided to make post for other developers and for users who want to optimize their sites for better web vitals. In this post I want to explain what we made in our products to get 100 grade in google speed test and how to fight with weak wordpress sides
To make perfect score of theme, you need to check next steps.
First step – Files
First of all, you need to check files which are loaded on your site by theme or plugins. For this, go to Google speed test and check site. Then, find this Treemap button
Click on it and you will find most slowest file. Well, usually, what I often see, it’s icon font. Many developers don’t want to make custom optimized icon font library and they use full FontAwesome library. And it’s huge. FA icons added more and more size in each version. So, we decided to make our own icon font bundle. The size was reduced from 1 Mb to 24kb. This was first victory in this war.
Fonts and typography
This is second most common reason of bad web vitals score. I see that many themes use minimum 3 custom fonts. And this is too much. In our case, we used Roboto font, but we found that we can use alternative with combination of system fonts.
Here is our example
body{font-family:Roboto,"Helvetica Neue",-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Oxygen-Sans,sans-serif;}
Using this fallback fonts instead of external Roboto font saves us near 100kb and one external request.
CSS framework
Most of themes use Bootstrap, Materialise, Foundary or other frameworks. Yes, it has a sense because frameworks have all useful styles and you don’t need to make new styles again and again for all new blocks. But this has own price – Bootstrap has minimum 200kb for basic styles. In some point we thought that we can use customised Bootstrap, but we decided to use another approach. Instead of adding all styles together, we decided that we will add only those styles which we need in current point.
Example – Bootstrap has a lot of styles to control margins on Mobiles. You can add, for example, 10px right margin only for mobiles or only for tablets. But we found, that such styles never used. In 99.99% cases, when you build mobile site, you want all elements to be stacked under each other and have only margins at bottom. So, we added styles only for bottom mobile margin.
Result – instead of 200 kb, our framework for common styles is 20kb. In the same time we are not limited of using flexbox or other popular systems and we can use also css grid and other experimental features without waiting for updates for bootstrap or other frameworks.
Also, i want to say that things are changing fast. On first sites we used tables, then floating blocks, then flexbox. Now, you can build basic responsive loop of items with just 3 lines of code, like this
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-gap: 10px;
}
This code will take all inner blocks of “grid” container and place them as columns with gaps, responsive style and auto fit it to width of block.
This is just example, here also you can also find most modern 1-line solutions in css
Conditional asset loading
I see that 99% of developers don’t care about this. Let me explain what is conditional asset loading.
Imagine, that you have some special block in theme or plugin, for example, simple box with some colors and formating. Most of developers create style and scripts for this block and load them directly when plugin is installed. This is big mistake. Developers must divide everything by logical blocks and make files for each this block.
5 years ago, before web vital requirements, google and gtmetrix reduced scores when you have many browser requests to files. All developers tried to add all scripts and files in one file. Today is different. Google wants to have less size of files instead of less requests. So, we divided all scripts and styles by logical parts and separate files. Even if it’s just few lines of code. We made separate styles and scripts for all blocks, widgets, modules, shortcodes, Gutenberg blocks and enabled them only if block is used on page. This saved another 120kb
Here we need to discuss one big problem. How to make conditional loading?
WordPress provides us few functions for loading and registering styles and scripts (wp_enqueue…), but they are good if you need to register something global on your site. For conditional loading, we have functions wp_add_inline_script and wp_add_inline_css. They are working great, but they must be assigned to other file. I mean, that you can’t use them as standalone render function.
Things become even worse when we use wp_enqueue_style in our template parts. Problem is in place where wordpress loads style files if you requesting them in templates. It loads them in footer. And this makes huge CLS (layout shifting error) in web vitals. So, we need somehow to load styles conditionally, non render blocking and before our content to prevent style glitches and shifting.
Unfortunately, wordpress doesn’t have anything in core for this. We made some own functions and use them properly in different places. Here are logic of these functions
Inline styles
Yes, many people wrote that you should not use them, but i don’t agree. Inline styles are very fast and best way if you have no too much code. Especially, it’s useful if you have some options for simple customization. There is no problem if you use something like this
<div style="background:red">Red bg</div>
Inline scope
Here we also use inline styles but as separate block. Example
<div class="red"><style scoped>.red{background:red}</style>My red block</div>
Yes, maybe it’s not best solution, but if i have few lines of code which I need to use just for special block, I use this option. It allows to add conditional styles and prevent CLS and incompatibilities with caching plugins. I try to use this option not too much, but I found it’s the best if I need to make some custom template part with custom styles which different from global part.
Using transients to store conditions
This is another more elegant solution for conditional loading. Imagine that we have some template part, for example, which renders post grid. But we don’t want to add styles for this block to common styles. Maybe user want to show list and not grid, so, we need grid styles as conditional.
So, create transient. Let’s call it “conditional_asset”.
Now, in our template part, we will get this conditional_asset transient and add name of our block to it.
Next, in your wp_head hook, we can check this transient and if we found name of block in it, we render styles in header. We can set transient cache as 1 day, so, if we remove this block from page, transient will be removed and not resaved, so, styles will be not loaded.
Checking by name for loading
Another great hack which i use quite often. If you have content blocks (like gutenberg, shortcode, page builder block) which is saved in content, you can simply check it by stripos php function
global $post; $postcontent = $post->post_content; if(stripos($post_content, 'boxnumber') === 0){ echo '<style>...</style>'; }
In this code we check if page has block with class, id or word “boxnumber”. If yes, we can load some conditional styles
Image loading
This is the most weak part of WordPress. It simply doesn’t have good resizer. So, as theme or plugin developer, you have few choices:
- You can use own custom resizer (but it will have problem with different kind of CDN, cache plugins
- You can register custom size of image add_image_size but this will generate separate copy of all uploaded images even if you don’t use it on page
- You can make custom lazy load for images.
We have long journey in our fight with images and finally, we decided to use hybrid method. For small images and for blocks which are not very often in use on sites of our clients we used own resizer. Even if it will have no compatibility with CDN or plugins, there is little chance that client uses this block on site with custom CDN, also he can use another widget or block if he has issues.
For most common blocks and product loops, we created few predefined sizes of images and added special wrapper around regular WordPress function which allows us to add custom lazy load for images, custom fallbacks for images, support for Ajaxed parts and in the same time this has compatibility with plugins, because we use regular WordPress functions.
On inner pages we decided to use regular WordPress images without any modifications, especially in fact that WordPress 5.5 version has lazy load for inner images.
This was fixed problem with different kind of google warnings about image sizes. But I need to mention one thing about images when you use them in first screen. Google executes all scripts before checking page in first screen. It means that if you have big full width image in top of page, you will have too big LCP and it’s almost not possible to fight with this.
So, we recommend to avoid using big images in first screen even if it has lazy load. For this purpose we made many post layouts which shows small featured image and we made special layouts which don’t have featured image at all and still look great.
and we see that many popular blogs made redesign and use small featured image or no image at all in first screen
Loading scripts on user interaction
We found that some blocks have more impact on speed than other. For example, if you use blocks with third party libraries, they can slow down site a lot. Especially, it’s problem if this block doesn’t have critical value on page. For example, it’s autoplayed video, Lottie file, 3d object. So, we found special option in javascript when you can load scripts only on user interactions.
How it’s working? Imagine that you have block in middle of page, now, when user open page, this block is hidden, any scripts are not loading for this block. But when user start to scroll, script is loading in background. When user reaches this block, it’s fully loaded and available
This is example of such function in Javascript
const body = document.body;
var loadedtdel = false;
const onInteraction = () => {
if (loadedtdel === true) {
return;
}
loadedtdel = true;
const modelViewerScript = document.createElement("script");
modelViewerScript.type = "module";
modelViewerScript.src = "https://site.com/js/js/model-viewer.min.js";
body.appendChild(modelViewerScript);
};
body.addEventListener("mouseover", onInteraction, {once:true});
body.addEventListener("touchmove", onInteraction, {once:true});
body.addEventListener("scroll", onInteraction, {once:true});
body.addEventListener("keydown", onInteraction, {once:true});
Here, we load script model-viewer.min.js only on scrolling, touching or key press.
Optimization for animations
Animations. Most of developers use css animation libraries or make own css animations. Yes, it’s good for developing as it’s easy to use. But not good for speed because you need to load additional styles (usually 50-150 kb). We used GSAP instead. It’s the most optimised animation javascript library. In some scenarios it’s even faster then css transformation and it’s very powerful. We added our WOW animation framework and you can see many powerful animations on our demos which are not render blocking and very smooth.
So, if you need just few animations – css is ok, but if you build complex theme with a lot of different micro animations, try to use GSAP, Anime.js or other javascript libraries.
This was our top methods which we used to improve Web vitals in our theme and plugins. But, Theme and plugins are just 30-50% of your speed score. Another 50% is your hosting and cache optimisation. What’s you need to check for hosting is TTFB. If it’s more than 100ms – maybe it’s better to change hosting or add Cloudflare. For example, If your server is in USA, users from Europe will get too big TTFB (Time To First Byte).Not a problem if your site is just for USA, but big problem if it’s for global wide users
Another thing which you MUST use is cache plugins. We added big step tutorial about free plugins which we use on our sites and continue to update it for last 2 years with all new improvements. Without reasonable and good configured cache system, it’s not possible to get 100 grade. I hope you understand this. Here you can find our article about free cache system for wordpress to improve web vitals