Generate Critical CSS (Above the fold) for Specific PimCore Documents
What is critical CSS
Critical CSS is a technique that extracts the CSS for above-the-fold content of the web page in order to render content to the user as fast as possible. Above-the-fold is all content that the user initially sees upon arriving on a web page (the origin of the phrase dates back to the beginning of the printing press - due to the way newspapers were printed on big sheets of paper, they were folded in half once they hit the newsstands, because of this - only the top half of the paper would be visible to anyone passing by). For example, this is a screenshot of what I initially see when I visit the https://factory.dev/blog url. This is what is considered above-the-fold for this page, so only the styling of this part is considered critical css.
So, why is using critical css beneficial? Usually, when the browser reaches the <link rel= "stylesheet" href="/styles.css"/> element where your css is loaded, it has to wait until the file has been downloaded from the server and parsed.Only once this is done, the browser can continue to render the rest of the page. That is called render-blocking.
Critical css allows you to load only the styles needed for above-the-fold content, render the page, and then load the rest of the css "in the background" asynchronously with <link rel= "preload" as= "style"> to avoid render blocking. This can be achieved either by appending the critical styles in the <head> directly as inline styles (this achieves the fastest loading time because no request to the server to fetch the file is needed), or by putting them in a separate file, much smaller than your main css file.
Critical CSS can be a very useful technique to optimize and minimize your initial page loading time, thus maximizing the user's satisfaction and overall metrics for your web page. There are a lot of different ways to implement it, which we'll go over, but first of all, let's analyze why it's important and how it can help you.
How Using Critical CSS Can Help Both You and Your Users
In these modern times of advanced technology and people spending more and more time online, whether it be for shopping, learning, or social interactions, it is no longer enough to make your web page pretty and functional — speed is just as important.
Not only does the page load time affect the user's satisfaction, it directly affects Google's search results rankings. Research by Kissmetrics revealed that 40% of people abandon a website that takes more than 3 seconds to load. So, for example, imagine that you're running an online business and your page load time is slow — this could mean losing existing customers, not gaining new customers, and ultimately leading to lost profit, which benefits nobody.
Also, as mentioned before, page speed is one of the top ranking factors for Google rankings and SEO, so it's easy to see how using critical css can be useful. While running your page through PageSpeed Insights or Lighthouse to diagnose performance issues, you may notice that one of the metrics used to calculate performance is "First Contentful Paint."
Also, in the “Opportunities” section, you may see the “Eliminate render-blocking resources” opportunity.
First Contentful Paint (FCP) is one of six metrics tracked in the Performance section of the Lighthouse report. FCP measures how long it takes the browser to render the first piece of DOM content after a user navigates to your page. A score of 1.8 seconds or less is considered a good FCP score. The “Eliminate render-blocking resources” opportunity is directly related to the FCP loading time — and, as mentioned in the intro section of the blog, this is where using critical css comes to save the day.
Also, in direct correlation with everything mentioned above, using critical css can increase your page’s conversion rate. Conversion rate is the percentage of users who take a desired action on your website. An example of conversion rate for an e-commerce website would be the percentage of users who visit your site and buy something. It’s an important measure, because you do not want users to just visit your site, you want them to engage with it, and complete actions related to its goal and purpose.
Of course, critical css is definitely not the only way to optimize the loading speed, and you should always use other best practices to achieve optimal performance and metrics, but if you find yourself with a lot of styling and large css files, critical css can be a lot of help.
Options for Generating Critical CSS
Before diving into a specific example and how-to for implementing critical css for your website, I want to present some of the various different options for developers to implement and use it.
Manually
The first way that you can implement critical css is of course, to manually extract the critical parts of your css without using any plugins or generators. For this, you would have to go through all the pages on your website, determine what elements and their relevant styles are the scope of your above-the-fold content, extract that css and include it either inline or in a separate smaller file. As you can already tell, this can be a tedious task if you have a lot of different pages on your site, more so, you have to account for different screen sizes - above-the-fold content will surely be different for the desktop and mobile versions of your site. Another downside to this approach is that it’s not change-proof. If you decide to add some new elements or change your DOM content for above-the-fold by some degree or completely, you would need to go over everything again and extract the new styles manually. But still, if your website doesn’t have a lot of different pages and you would prefer not to rely on any third party tools, this approach may be just the right one for you.
Using a Generator
The other way to generate your critical css could be by using an online generator. There are a number of online critical css generators where you can input your site url (for some, your full css code is needed along with the url) and the generator generates the needed css code for you. What’s left for you to do, is to include that code in your html. But be aware, this approach is also not change-proof like the first one, after any changes you make to the content or styling, you would have to go through the generator again.
Using a Plugin/Library
Lastly, using a third party plugin/library is the most efficient way to generate and implement critical css. There are a lot of different tools for you to choose from, whether you’re using npm, yarn or some other package manager. Some of these include Critical, CriticalCSS, Penthouse, crittr and so on. Researching the best option for you and setting up the plugin logic can take some time, but once you do that - you’re all set. You can automate the process and you don’t have to do everything all over again once you make some changes to the content or styling.
Example of Using Critical CSS with Pimcore Documents
In this section, you will see how to implement critical css for Pimcore Documents with Critical on a real project example. One note before we start — the process explained here is not exclusive to Pimcore and Pimcore Documents (of course, some parts of the code will have to be changed if you're not working with Pimcore Documents, but the general logic for using Critical is more or less the same. You will just have to adjust the options to your needs, so even if you're not using Pimcore currently, this can be a very useful guide.
Document Property
First of all, a custom Document property "generate_critical_css" is added to documents. This property will determine for which documents critical css will be generated (of course, this is not mandatory if you want to generate it for all of your documents).
Critical Plugin Basics
As we will be using critical, dotenv and axios in this guide, they need to be installed with npm -i critical dotenv axios.
After Critical is installed and imported with “import {generate} from 'critical';”, the configuration options must be defined. On the official Critical GitHub, you can see code examples with available options. Some of these include whether the critical css should be inline or not, the base directory, html source and source file, a path to css files, viewport width and height, target for output results, whether the css should be extracted and the option to ignore certain css rules.
Not all of these options are required and can be omitted if they are not needed. The list of all available options and explanations can be found here. Below, you can see an image with an example of usage.
The logic for Critical CSS Generating
Now we will go through our logic for generating critical css step by step. In our example, we won't be generating inline css. Instead, the critical css will be outputted in separate files and imported into our template.
All of the logic that will be shown here is contained inside a single "critical.mjs" (module js) file, but I will include smaller chunks of code from the file step by step along with the explanations so you can get a better understanding of what's going on in each part of the code. At the end of this section, I will include the entire file code in one place.
Imports
First of all, we import the things we need for this logic - “generate” from critical, axios to be used in the function for fetching the pages data and dotenv so we can set the base url from a .env variable.
Then, because there is a need for the base url in many parts of our code, we set the baseUrl variable from the environment.
Fetching Pages Configuration
The next step is fetching document data. As you already saw on the image with the example usage of Critical and Critical's GitHub page, two important options are the path to the html source file and the target path for the generated css file.
As we are generating critical css for all documents with the property "generate_critical_css" checked, we can't really hardcode those paths or the names of the files (the names, of course, need to be unique and have some document properties in the name like document key and/or id so we can import the right css file for each document). That's why we need to fetch the document data.
A bit of Backend logic is needed here to return the data for us. The PHP function getCriticalCssPages in the DeafultController.php gets the id, key, and path for all documents with the property "generate_critical_css" marked as true and returns an array of objects with all the data we need. And if it's needed, the logic for getting non-document (static) pages data can also be written here.
So now, in the fetchPagesConfiguration function in our critical.mjs file, we can send a request to the backend and retrieve that data. The data is then passed to the generateCSS function, which we’ll go through next.
Generate Critical CSS
The generateCss function takes the pagesData (data of our documents fetched from the backend) array as a parameter. The first thing we do here is set a config object with some options that will be the same for all of our documents - baseUrl we set from .env at the beginning, the suffix of the files that will be universal for all css files generated, criticalWidth and criticalHeight.
After that, we loop through the pagesData array (remember, each entry in this array represents one document). Before invoking “generate” we set two very important variables which will be used for “src” and “target” options - pageUrl and cssFile. pageUrl consists of the baseUrl and the path of the document.
Also, we are adding a “?generatecritical=’true’” param at the end of the pageUrl. As you will see in the next section, where we import the critical css file, the file is only imported if app.request.get('generatecritical') is NOT true. This is important to avoid a sort of an “infinite” loop that creates an error - the generate function first crawls through the page, and if it comes over a css file that doesn’t yet exist (because “generate” is going to create it) it gets “stuck” trying to create critical css from a non-existent file.
cssFile is the path of the target file in which the generated css will be placed; it consists of the path to the folder where all our critical css will be placed (‘./public/static/css/critical’) and the file name which we set by concatenating the page key and page id with the general suffix we set in the config object (‘_critical.min.css’). After that, all that is left to do here is invoke “generate” and pass the object with all of our options to it, and Critical takes care of the rest.
Here, you can see the full critical.mjs file, which we went through in this section.
Adding CriticalCss to Package.json
The execution of all the logic we added won't be done by itself, so we have to add it to the "scripts" part of our package.json file like this:
And then, run "npm run criticalcss" in the terminal so all of the critical css files can be actually generated.
Importing Critical CSS Files in the Template
The one thing that is left for us to do, is to include the critical css files in our template. We include them in our default.html.twig template (the default template that all other templates extend).
So first, we have to check if the document exists and if generatecritical is not true (explained why it's important in the section before). Next, we check if the document property "generate_critical_css" is true. And at last, we set the path to the critical css file path with the document key and document id, which are available to us in our twig template (remember that we set the file names like that in our "generate" function) and append the stylesheet!
To Conclude
I hope this guide helped you understand the concept of critical css, how it's beneficial, and how you can implement it on your own projects. This is not a definitive guide that covers all the possible usage and implementations of critical css. For example, we could've added the logic for generating critical css, not only for documents, but for other pages too.
Also, as mentioned before, there is an option to generate inline critical css and append it directly in the twig template, which would make loading time even faster. But even though those options are not covered in detail here, I am sure that by following this guide through, you will have gathered enough knowledge to try and implement those functionalities yourself and try different options to find out what exactly suits you and your project the best. If you have any questions, feel free to reach out.