Frontend performance optimization for Drupal websites: Part 4
This is part 4 of a series of articles that defines our approach to frontend performance optimization. In this part we squeeze more performance out through delivery optimization.
In part 1 we covered cached optimization so that we’re only asking the server to produce a response when absolutely necessary. In part 2 we get into image optimization. In part 3 we defined our approach to slimming down the CSS we deliver to the browser. We suggest you start from the beginning.
Capellic maintains a suite of nonprofit Drupal websites that have been built over the past several years, using a very familiar but steadily evolving toolset. We adopted a component-driven approach to design and development years ago, and have steadily kept up with the popular tools used in the Drupal community to produce themes that adhere to the pattern.
Get the complicated stuff out of the way of the page load
Now that you’ve gone through the process of delivering as little as possible to your visitors, we have to tackle improving the performance of the front-end assets we are still sending down the pipe to land in their browsers.
This category of issues tends to have a big effect on a website's “Total Blocking Time” as well as its “Largest Contentful Paint” and “First Contentful Paint” metrics. We haven’t thus far directly covered “Web Vitals”, but these are the key metrics Google uses to judge the performance of a web page. We’ll cover these in a future post.
Fonts
Fonts are a key component of a website’s brand presence and make a huge impact on the reading experience of a page. The fonts shipped with our devices have only gotten better and better over the years, but most websites will still ship a dedicated brand font or two to provide a tailored reading experience for visitors. This is another area for optimization as fonts can be created and delivered in a variety of ways.
Serving your own fonts
There is some debate on whether performance will improve if you serve fonts from your server, or if instead performance will improve by using a CDN to serve your web fonts. At Capellic, we are hosting all of our clients on high-performance web hosting, almost exclusively with Pantheon, where their global CDN architecture ensures reliable and blazing fast delivery of all our assets, including web fonts. Because we aren’t concerned about the performance of our servers, we opt to serve our own font assets when possible. This ensures that we minimize dependency on another outside service provider to serve our sites as intended. This is better for overall security and reliability, as we have one less hole we have to punch in any content security policies, one less external provider we’re sending data to, and one less point of external failure for our sites. One tool that makes this more convenient which we’ve adopted in font source, which allows us to require most web fonts via NPM packages to self-serve these assets.
Prefer variable fonts when possible
Variable fonts allow for rendering glyphs at any/all possible weights and slants without having to transmit and load separate files for each variation.
Before the availability of variable font faces, each weight of a font (light, normal, semi-bold, bold, etc) would have to include a separate copy of the font file, as well as for italic variations. This requires downloading each of those files to render the varied weights and styles of typeface on a given webpage. Thankfully modern browsers won’t download font faces that are not referenced in your CSS, but use one spot of bold or italics on the page and get ready to load that entire font file.
Variable fonts allow you to render any combination of bold and italic presentations of text using a single font file. This reduces the weight of your final page delivery, which as we’ve learned previously, takes up less of a visitors bandwidth and less of your server’s resources. It’s another win-win for everyone.
Javascript
For most projects, we’re likely to still ship some first-party JavaScript, and it is the reality of business and marketing websites that we’ll also need to accommodate some third-party scripts that the business needs to track website performance and user behavior. The strategies for these two buckets of Javascript code will be similar in some ways, but they’ll also have some unique considerations.
Try to make your content render without Javascript first when you can
A lot can be achieved through server-side rendering and pure CSS that used to require significant JavaScript to accomplish. For example, when rendering a group of elements that has a default ‘open’ or ‘shown’ element (such as a tab group, a carousel/slider, or an accordion), it might be tempting to set the initial state of this component in Javascript (selecting the first element and showing it, and hiding the others), but this can often be accomplished in pure html/twig and css. Here is an example:
<div>
<div>
{% for tab in tabs %}
{% set attributes = create_attribute() %}
<button {{ attributes.setAttribute('data-tab-id', loop.index) }}>{{ tab.label }}</button>
{% endfor %}
</div>
{% for tab in tabs %}
{% set attributes = create_attribute() %}
{% set classes = [
'tab',
loop.first ? 'active'
] %}
<div{{ attributes.addClass(classes).setAttribute('data-tab-id', loop.index) }}>{{ tab.content }}</div>
{% endfor %}
</div>
<style>
.tab {
display: none;
}
.tab.active {
display: block;
}
</style>
<script>
(function (Drupal) {
Drupal.behaviors.myTabs = {
attach: function (context) {
context.querySelectorAll('button[data-tab-id]').forEach(function (button) {
button.addEventListener('click', function (element) {
const tabId = element.dataset.tabId;
const tabs = context.querySelectorAll('div.tab');
tabs.querySelector(`div[data-tab-id=${tabId}]`).classList.add('active');
tabs.querySelectorAll(`div[data-tab-id]:not([data-tab-id=${tabId}])`).classList.remove('active');
})
})
}
}
})(Drupal);
</script>
This example is somewhat simple and contrived, but it illustrates the purpose with minimal noise. All tabs are hidden by default, except for tabs with the active class, and the active class is added to the first element in the array on the page render. Any JavaScript is only responsible for toggling the class, and it only fires when the visitor takes an action. This is a general principle of how you should strive to write components that when possible work server first, and only rely on JavaScript to handle interactivity when the visitor decides to take an action, well after the page is loaded.
Defer scripts whenever possible
By default, when a webpage loads, it must download all of the assets attached to the page before it can render the page content. When it comes to scripts, it must also execute the script before rendering the rest of the page.
Using the defer attribute on your scripts tells the browser not to wait for the download and execution of your script before it can download and render the rest of the page assets. The script will load off the main thread and execute when the DOM is fully built.
Deferred scripts will also maintain their order, so you can guarantee the execution order by controlling the order they are attached to the page (The order of your libraries in your libraries.yml is one way Drupal helps you to control this). Here is what it looks like to defer a script using Drupal’s libraries system:
navbar:
version: 1.x
js:
js/navbar.js:
attributes:
defer: true
We recently applied this technique to a website that we've been brought in to help maintain. The results were fantastic! Across the ten pages we're tracking as KPIs (key performance indicators), we saw the "Total Blocking Time" and "Speed Index" get halved which caused our "Performance" score to go up by 10 to 15 points! All this for task that took longer to QA than it did to implement.
Use async when order doesn’t matter and there are no dependencies
Sometimes a script is completely independent of the others and does not need to wait for DOM content to be loaded. In this case, you can use async instead of defer to load the script in a non-blocking way, and have it finish as soon as it can. These types of scripts should not be looking for DOM content, because it may not be loaded when they execute. It should not be looking for data provided by other scripts, because we can’t guarantee that other scripts have already executed. Here is what that might look like in a Drupal libraries file.
initState:
version: 1.x
js:
js/initState.js:
attributes:
async: true
This is going to be a far less common occurrence in most Drupal projects. You will typically want to use defer for most things.
Consider dedicated tools to move third-party scripts off the main thread
Many of our clients at Capellic utilize Google Tag Manager to allow for great flexibility for their marketing departments to move fast and try new things. Clients can work directly with dedicated marketing consultants to track conversion data using a myriad of tools and try new experiments without having to wait for our team to run changes through the standard software development lifecycle.
This is, of course, a double-edged sword. Clients get low-cost iteration because they don’t have to involve development resources, which is a high-value service. On the other hand, the bypassing of development resources also means that it is common for website performance to not be considered. Web performance and even feature functionality can change without warning, and there are no deploy markers to help debug the problem and trace back to its source.
This is understandable, website performance is a nuanced and intricate topic, and it requires dedicated training to understand how to assess the impact of a change on that performance.
What this results in for many websites is a GTM container full of scripts from various services, with varying levels of impact on the site performance that are added (and rarely removed) at unexpected times. Website performance suffers because these scripts are blocking the main thread, causing layout thrashing, and downloading megabytes of extra data when visitors browse a site.
The default way we deal with this now is through regular communication with client marketing teams and general socialization of web performance knowledge. We regularly review performance and make recommendations about tools based on what we find. We help clients to understand as best we can the trade-off that certain scripts produce.
A lot can be achieved through a careful and trusting partnership between the development team and the marketing team. A culture of diligence around these scripts can often be maintained, but at Capellic we’re interested in exploring some technical options to mitigate the issue as much as possible.
Going to Partytown
Our research on third-party JavaScript optimization has led us to a tool called Partytown that works by moving scripts from the main thread to a service worker. Early experimentation with this tool has proven to be a bit complicated to implement on sites with large GTM containers. We’re still experimenting with this and hope in the future it opens some doors to reigning in the massive impact of overloaded GTM containers.
CSS
In addition to fonts and JavaScript, CSS can also have an impact outside of its mere presence and total size. The way you write CSS selectors can have a major impact on rendering performance as well.
Write the simplest CSS selectors you can
Another way you can control the complexity of the assets you deliver to production is by making your CSS as simple as possible. Outside of script execution, one of the most common contributors we’re seeing to high render-blocking time is “Style & Layout”. If we refer to Google’s documentation on what this means, it points to “scope and complexity of style calculations” and “large, complex layouts and layout thrashing”.
Some of the suggestions above about rendering your components “server first” without JavaScript intervention will have an impact here, but so will the complexity of CSS selectors. From Google’s documentation you see these examples:
// Nice and simple
.title {
/* styles */
}
// Complicated to compute
.box:nth-last-child(-n+1) .title {
/* styles */
}
// The above complicated example, moved to a class name
.final-box-title {
/* styles */
}
This may look familiar to the section above about rendering server-first. We want the template to take care of assigning these classes at render time, rather than relying on the CSS to calculate them in an opaque manner. By keeping the selector as simple as possible, the browser can calculate the styles faster. This has the additional benefit of making the CSS significantly easier to understand and more portable as well. Utility-first CSS advocates will be very familiar with this style of CSS. Here is an example of twig markup that would work with the class names above to effectively apply the same styles as the more complicated example.
<div class="boxes">
{% for box in boxes %}
{% set title_attribute = create_attribute() %}
{% set title_classes = [
'title',
loop.last ? 'final-box-title'
] %}
<div class="box">
<h3{{ title_attribute.addClass(title_classes) }}>
{{ box.title }}
</h3>
<div class="box-content">
{{ box.body }}
</div>
</div>
{% endfor %}
</div>
Look into Critical CSS if there is a real benefit for your website (but there might not be).
I’ve saved this section for last, because I find it to be the most unlikely thing most developers will use to improve site performance, despite it getting a decent amount of press and suggestions from Google themselves when reviewing page speed insights reports. In our experience, Critical CSS, when applied to sites built on Drupal, is an exercise in ‘the juice is not worth the squeeze”. In our experience, on sites we have tested it on, Critical CSS did not move the needle in terms of overall website performance and added significant complexity and time to our build/ci process. In addition, Critical CSS is treated in a “page-oriented” paradigm that we find antithetical to a “component-driven” design and development process.
Nevertheless, we believe it is possible that Critical CSS could be useful for some sites, just none that we have worked with yet. We may continue to experiment with it in the future. In Drupal, you can use the Critical CSS module to take care of loading the stylesheets once you’ve generated them, and the NPM package critical combined with a development environment running to generate them. Here is an example of a gulp task that we experimented with for generating critical CSS on Ohio Legal Help:
import { generate } from 'critical';
const PROXY_URL = process.env.PROXY_URL || 'http://localhost:8888';
function criticalCss(params, cb) {
return Promise.all(
[
{path: '/', file: 'default-critical'},
{path: '/blog-legally-informed-civil-criminal-case', file: 'blog'},
{path: '/detail/grounds-divorce', file: 'detail'},
{path: '/letters-forms/divorce-without-children', file: 'form_letter'},
{path: '/about-ohio-legal-help', file: 'generic_page'},
{path: '/resource/legal-aid-society-cleveland-0', file: 'legal_resource'},
{path: '/resource/women-helping-women', file: 'local_resource'},
{path: '/people/susan-choe', file: 'person'},
{path: '/topic/divorce_no_kids', file: 'topic'},
].map(
mapping => generate({
inline: false,
src:`${PROXY_URL}${mapping.path}`,
target: `css/critical/${mapping.file}.css`
}, cb))
);
}
This code loads up an example node of each content type on the site, along with the homepage and fires up a headless Chrome browser through critical/puppeteer to load the site and generate a critical CSS file automatically, and then it saves them to an output directory that we configured the Critical CSS module to look for them.
This had so little impact on the site performance metrics and was so complicated that we opted not to use it.
Conclusion
This series is the culmination of learning and experimentation that has spanned the last several years, and yet it is not exhaustive of all the ways one can optimize front-end performance on a Drupal site. These are the highlights of issues and remedies we’ve found to apply to a wide variety of sites, both ones that we’ve inherited from other developers, and those that our past selves produced. We are not done with our quest to produce the fastest websites possible, we’re still regularly experimenting with new techniques and technologies to up our game (see the sections about Partytown and Critical CSS above).
If you have any questions, you can find us in the #performance channel in Drupal Slack. Feel free to tag "dustinleblanc" or "Stephen Musgrave". We look forward to hearing from you.