21 minutes reading time (4174 words)

Joomla Performance Tuning IV: Site building calisthenics

March-Performance

In the third part of this series I described how to squeeze more performance out of your site by optimizing the static media files. This month, a month later than expected due to a minor family emergency, I'll talk about putting the finishing touches which make your site more polished and professional. They mostly have to do with how your site interacts with search engines and social networks but there's also a little bit of performance to be found in there. 

Site building calisthenics

If you've implemented all recommendations thus far you're about 90% of the way to a site that performs well, ranks well and is easy to maintain. 

We are not going to talk about techniques which are either obsolete (such as combining and minifying CSS and JavaScript — seriously, don't do that!) or cumbersome for most people (such as trying to remove whitespace and newlines from your HTML output or creating a Progressive Web Application). The former are pointless, the latter have a difference target audience than the people following these series.

Instead, we're going to talk about some things you can do to improve your site's performance and buff out some friction points with your users. It's usually not as easy as "install this, click that, bam, wham" but it's worth the very little extra effort. If you do this a few times it will become second nature and your clients will be impressed at the performance of the sites you deliver.

OpenGraph and Twitter Cards

If you've ever typed a URL into a social network such as Facebook (and its other web properties such as Instagram, Messenger and WhatsApp), Twitter or LInkedIn or even a messaging application like Apple Messages, Slack etc you might have seen something interesting happening. You get some sort of a card with an image and a short description of the content of the page. You may even get links to the content author or publisher on that social network. How does that magic work? The answer, my friend, is not blowing in the wind, it's meta tags in the HTML source code of the page. Namely, the OpenGraph and Twitter Card meta tags.

There are two ways to go about it: use a third party extension; or edit your template and make template overrides.

The first way is arguably much simpler. There are several extensions for Joomla to help you out with that. A simple but powerful one to use is Social Magick which generates title card images using a combination of a background / overlay / mask image, the article's full content image and title — or a combination thereof. Full disclosure: this plugin is owned by my wife's company and I'm its lead developer. Another plugin I have used is Phoca Open Graph which lets you set up your OpenGraph and Twitter Card tags for your content. This plugin will use your article's full content image as the card image. Both plugins will fill in the other OpenGraph metadata from your article's information.

If you are developing your own template or just your own template overrides you can go full custom code. This gives you absolute control but is, of course, much more involved.

The first change is obviously changing the root HTML element of your template to include the namespace for OpenGraph, e.g.:

<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>" prefix="og: http://ogp.me/ns#">

The astute reader may notice that we're using prefix instead of an XML namespace. This is the recommended way that works on newer browsers, i.e. Internet Explorer 10 and later.

Then you need to create template overrides for your categories and articles display which include the necessary OpenGraph and Twitter Card tags. For example, an article template override (templates/MY_TEMPLATE/html/com_content/article/default.php) might read something like:

$imagesRegistry = new \Joomla\Registry\Registry($this->item->images ?? '{}');
$imageIntro = $imagesRegistry->get('image_intro', null);
$imageFull = \Joomla\CMS\HTML\HTMLHelper::cleanImageURL($imagesRegistry->get('image_fulltext', $imageIntro))->url;
$canonicalURL = Route::_(ContentHelperRoute::getArticleRoute($this->item->id), true, Route::TLS_IGNORE, true);
$doc = $this->document;

$doc->setMetaData('og:type', 'blog');
$doc->setMetaData('og:title', $this->item->title);
$doc->setMetaData('og:description', $doc->getDescription());
$doc->setMetaData('og:site_name', $app->get('sitename'));
$doc->setMetaData('og:url', $canonicalURL);
$doc->setMetaData('og:image', $imageFull);
$doc->setMetaData('twitter:card', 'summary_large_image');
$doc->setMetaData('twitter:site', '@your_twitter_handle');
$doc->setMetaData('twitter:creator', '@your_twitter_handle');
$doc->setMetaData('twitter:description', $doc->getDescription());
$doc->setMetaData('twitter:title', $this->item->title);

A category blog template override would likewise need code similar to the following:

$canonicalURL = Route::_(ContentHelperRoute::getCategoryRoute($category->id), true, Route::TLS_IGNORE, true);

$doc->setMetaData('og:type', 'blog');
$doc->setMetaData('og:title', $this->params->get('page_title', $category->title));
$doc->setMetaData('og:description', $doc->getDescription());
$doc->setMetaData('og:site_name', $app->get('sitename'));
$doc->setMetaData('og:url', $canonicalURL);
$doc->setMetaData('og:image', \Joomla\CMS\HTML\HTMLHelper::cleanImageURL($category->params->get('image'))->url);
$doc->setMetaData('twitter:card', 'summary_large_image');
$doc->setMetaData('twitter:site', '@your_twitter_handle');
$doc->setMetaData('twitter:creator', '@your_twitter_handle');

Using template overrides obviously works best if you are already intent on making your own template, therefore creating copious amounts of template overrides anyway. If unsure, go with the third plugins method.

DNS prefetch and external resource preloading

Sometime earlier in the series I said that browsers don't magically know which files they need to request when they are loading the page, they start loading files as they figure out they need them or you pushed them to the browser with HTTP/2 Push. I lied somewhat. You can give some hints to the browser so it starts preparing things while it is still downloading the first few bytes of your HTML content using the DNS prefetch and preload link tags. But I think I'm getting ahead of myself.

DNS prefetch

When I described how a browser retrieves CSS, JavaScript and image files necessary to render the page I used a simplistic model where the browser sends a request to "your server". The unspoken assumption was that "your server" refers to the same server and domain name as the one it is loading the HTML content from. On most Joomla sites this is indeed the case. However, your site may be using an external CDN to host some media files, include CSS from Google Fonts or a similar resource, load JavaScript from an external service and so on and so forth. These are all resources hosted on a domain name different to the one hosting your site and delivering its HTML content. Your browser can't magically connect to these external domain names; domain names need to be resolved to an IP address first by means of a DNS query.

DNS queries can be slow, especially on high latency connections such as satellite, cellular, or just shoddy shared WiFi — when the pandemic is over and we can go back to working from cafés, hotels and airport WiFi connections. This adds a delay to loading that resource which can vary from annoying (large layout shifts) to downright problematic (a non-deferred, non-asynchronous JavaScript inclusion is causing the page to stall loading).

Using DNS prefetching you can give a heads up to the browser, essentially telling it that it will need to resolve these domain names in the course of rendering the page. The browser can do the DNS queries while it's twiddling its thumbs waiting for a blocking JavaScript or CSS file to finally load, saving the user some time in the later parts of rendering the page.

Another good reason to provide the DNS prefetch hint is when you know that a user interaction will cause a resource to be loaded from an external site. For example, if you know that clicking a Like button will cause a connection to a social network's domain name it makes sense to tell the browser to resolve the domain name ahead of time to make that button appear more responsive to the user's interaction.

The best way to do that is in your template's index.php or your template overrides.

For instance, let's say that your site is going to load an image from cdn.example.com whenever the user clicks a button on the page. Having the browser resolve the DNS before the user interaction will save some valuable time.

If you're doing this in your template's index.php you need to do this:

$this->getPreloadManager()->dnsPrefetch('https://cdn.example.com');

Do note that this code must be used early in your template's index.php file, before you fetch or output the document's head. The document's preload manager object  only applies changes to the document's head.

From inside a template override you need to instead do:

$this->getDocument()->getPreloadManager()->dnsPrefetch('https://cdn.example.com');
 

Pre-connect

Having the browser do the DNS query ahead of time is great. If you, however, know that the browser will most definitely need to connect to the external site to pull resource used to render the page – such as CSS, JavaScript, images or font files – it makes even more sense to tell the browser to open an HTTP connection to the external server and keep it at the ready. This goes further than the DNS prefetch. It does the DNS resolution, TCP handshake (open the connection to the server) and TLS negotiation (get ready to use HTTPS). It leaves a connection to that server open and ready to be used.

This works the same way as the DNS prefetch but you will instead be using the preconnect() method of the preload manager.

Let's say that you want to use Google Fonts. This means you are loading CSS from fonts.googleapis.com which, in turn, load the font files from fonts.gstatic.com. So, in your template's index.php page you need to do:

$this->getPreloadManager()->preconnect('https://fonts.googleapis.com', []);
$this->getPreloadManager()->preconnect('https://fonts.gstatic.com', []);

Likewise with the preconnect, if you are doing this from inside a template override you need to replace $this with $this->getDocument() since the execution context is different.

Resource preloading

In some cases the file(s) that need to be loaded are not immediately obvious to the browser by just parsing the HTML of your page. Some resources may be referenced from a CSS or JavaScript file. When that happens the browser is, like, "oh great, let me add yet another file to my download to-do list". This can have an adverse effect to your site's performance because that file may be important e.g. a font file or image that will cause a layout shift, or an externally hosted JSON or JavaScript file which will only become apparent it's necessary when the browser finishes parsing and executing another piece of JavaScript. The latter is very common when including JavaScript code from third party services.

Again, you can give a heads up to your browser by telling it that it will need to eventually retrieve a certain file. By giving it the hint very early in the HEAD section of the HTML document the browser can optimise the retrieval of that file i.e. start downloading it in the background while its main thread is busy waiting for and parsing any blocking CSS and JavaScript on the page. 

Just like DNS prefetching, resource preloading works by using Joomla's pre;pad manager object.

For example, if I want to preload a font file from the template's index.php page:

$this->preloadManager->preload('https://cdn.example.com/my_font.woff2', ['as' => 'font', 'crossorigin' => 'anonymous']);

It is important to note a few things.

  1. Unlike the DNS prefetch you are giving a full URL to a file.
  2. The as attribute is mandatory and tells the browser the usage intent of that file e.g. font, image, video etc.
  3. The crossorigin attribute is essentially mandatory for modern browsers and instructs them how to handle request authentiaction. Setting it to anonymous sends no cookies, HTTP authentication or client-side SSL certificates and is always allowed. Setting it to use-credentials will pass cookies, HTTP authentication or client-side SSL certificates but is subject to the cross origin resource policy of the site which may effectively prevent the request from going through, nullifying your effort at hinting the browser.
  4. Any resources preloaded must be used with a "short period of time" which in most browsers is between 5 and 10 seconds. If the resource is not used within that time it's evicted from the memory cache, meaning the browser won't use it, and the browser prints an angry message in the console — and you get penalised in your page performance metrics!

Swap fonts

This is an important feature when you are using custom fonts. By default, the browser will try to guess whether it should display any text at all while your custom font has not yet been loaded and parsed. If your CSS font-face does not include a fallback system font the default behaviour is "block" which means that your browser will not display any text at all before it retrieves the custom font you specified.

Ever visited a site which displayed sod all text forever and you were frustrated staring at a mostly white page with a few image elements here and there until poof! all text was suddenly there? Yeah, that's why.

Luckily, you can tell the browser to do something different using the font-display CSS attribute. Setting it to swap means that the browser will be impatient and fall back to the default font while it's waiting for the custom font to download. When the custom font downloads, even if it's after several minutes, it will re-render the page with the new font. If you set it to fallback the browser will be likewise impatient but if the custom font doesn't load within seconds it will stick with the fallback system font and ignore the custom font altogether.

If you are using Google Fonts it is enough to replace /css? with /css2? in the URL and appending &display=swap. For example, loading Noto Sans becomes:

<link href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" rel="stylesheet"> 

The best thing to do is to set up good fallback fonts and set the font-display to swap. For icon fonts you should use block so what renders is a blank space while the icon font is loading.

Speaking of font fallbacks, you should include the default system fonts as your fallbacks. For example, sans serif fonts can have a fallback of

BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", Helvetica, Arial, sans-serif

which covers newer and older Apple devices, Windows 7 to 11, Android and legacy systems, preferring the default user interface sans serif font on each platform. The idea is that the user will see a familiar font instead of the typically uglier default sans-serif fallback. On Apple devices, for example, this defaults to the San Francisco system font instead of the Helvetica font.

Site icons (favicons)

Back in the old days when the dinosaurs roamed the earth and 800x600 pixels was considered "high resolution" — that would be around 2001 — there were 16x16 pixel 256 color favicons, originally intended to present a site logo in the user's bookmarked favourite sites. Fast forward two decades and these icons are no longer 16 pixel square, 256 colors or limited to bookmarks.

Favicons are of course still used for bookmarks and in site tabs, just like they did 20 years ago. They are also used to display icons on smartphones' and tablets' application launchers / home screens when you create a shortcut for the site. Moreover, they are used by third party services to pull a logo for your site. If you didn't know about this use check out this third party CodePen.

To make matters more complicated, there are several sizes necessitated by different generations of smartphones, tablets, browsers and operating systems. You can find a great reference for all of these at https://github.com/audreyfeldroy/favicon-cheat-sheet

If we were to come up with a shortlist that supports every device, browser and operating system from 2013 to 2022inclusive we'd come up with the following:

  • An ICO file containing 16x16, 24x24, 32x32, 48x48 and 64x64 pixels.
  • Seven PNG files with the sizes 32x32, 128x128, 152x152, 167x167, 180x180, 192x192 and 196x196 pixels.
  • An 144x144 pixel PNG and a background colour for the Windows Modern UI site tiles which are deprecated but not removed yet (and very much used in Windows 7 which people still use for whatever reason).

Adding them to Joomla requires editing the index.php file of the template and putting this before outputting the HEAD of the HTML document. Assuming that your files are stored in images/logos/favicon we have the following code:

$doc = \Joomla\CMS\Factory::getApplication()->getDocument();
$baseUrl = \Joomla\CMS\Uri\Uri::base();

// Good old 16px square favicon
$doc->addFavicon($baseUrl . 'images/logos/favicon/favicon-16.ico');
// Default fallback: 152x152 pixels
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-152.png', 'apple-touch-icon-precomposed', 'rel');
// Windows Modern UI tile
$doc->setMetaData('msapplication-TileColor', '#d43431');
$doc->setMetaData('msapplication-TileImage', $baseUrl . 'images/logos/favicon/favicon-144.png');
// All other sizes
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-196.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "196x196"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-192.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "192x192"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-180.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "180x180"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-167.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "167x167"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-152.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "152x152"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-128.png', 'apple-touch-icon-precomposed', 'rel', ['sizes' => "128x128"]);
$doc->addHeadLink($baseUrl . 'images/logos/favicon/favicon-32.png', 'icon', 'rel', ['sizes' => "32x32"]);

The obvious problem is that you need to remember to make all these changes which can be impractical. You can automate this somewhat by uploading only two files in your images/logos/favicon folder:

  • favicon.ico which you still need to generate yourself but can get away by only including the legacy 16x16px icon
  • favicon.png a 512x512 pixel transparent PNG with the high quality version of your logo

You can then use this substantially more complicated code to automatically generate all necessary sizes:

$doc = \Joomla\CMS\Factory::getApplication()->getDocument();

/**
* Generate favicons.
*
* You need to upload two files to the $basePath folder, by default images/logos/favicon :
* - favicon.ico which includes 16x16, 24x24, 32x32, 48x48 and 64x64 pixel images.
* - favicon.png a 512x512 pixel transparent PNG
*
* @param string $basePath Path with your favicons relative to your site's root
* @param int[] $favIconSizes List of sizes to generate e.g. [32, 57, 152], see https://github.com/audreyr/favicon-cheat-sheet
* @param string|null $tileColor HTML hex color for the IE Modern UI tile icon, null to skip generating it
* @param int $defaultSize Fallback favicon size when the browser doesn't request a specific dimension
*/
$faviconGenerator = function (string $basePath, array $favIconSizes, ?string $tileColor, int $defaultSize = 152) use ($doc) {
$ensureSize = function (int $size) use ($basePath) {
$filePath = JPATH_SITE . '/' . $basePath . '/favicon-' . $size . '.png';

if (!file_exists($filePath))
{
try
{
(new \Joomla\CMS\Image\Image(dirname($filePath) . '/favicon.jpg'))
->resize($size, $size)
->toFile($filePath, IMAGETYPE_PNG, ['quality' => 9]);
}
catch (\Exception $e)
{
}
}

return basename($filePath);
};

$baseUrl = \Joomla\CMS\Uri\Uri::base(false) . $basePath . '/';

// Fallback .ICO file
$doc->addFavicon($baseUrl . 'favicon.ico?' . $doc->getMediaVersion());
// Default favicon
$doc->addHeadLink($baseUrl . $ensureSize($defaultSize) . '?' . $doc->getMediaVersion(), 'apple-touch-icon-precomposed', 'rel');
// Internet Explorer Modern UI (formerly Metro) tile icon, classic Edge
if (!is_null($tileColor))
{
$doc->setMetaData('msapplication-TileColor', $tileColor);
$doc->setMetaData('msapplication-TileImage', $baseUrl . $ensureSize(144) . '?' . $doc->getMediaVersion());
}
// All other favicons
foreach ($favIconSizes as $size)
{
$doc->addHeadLink($baseUrl . $ensureSize($size) . '?' . $doc->getMediaVersion(), 'apple-touch-icon-precomposed', 'rel', ['sizes' => "{$size}x{$size}"]);
}
};
$faviconGenerator('images/logos/favicon', [
// Favicons size reference: https://github.com/audreyr/favicon-cheat-sheet
32, // Default fallback for most desktop browsers.
// 57, // Deprecated: Standard iOS home screen (iPod Touch, iPhone first generation to 3G), old Android
// 72, // Deprecated: First- and second-generation iPad
// 76, // Deprecated: iPad home screen icon
// 96, // Deprecated: GoogleTV icon
// 114, // Deprecated: iPhone retina touch icon with iOS <= 6
// 120, // Deprecated: iPhone retina touch icon with iOS >= 7
128, // Chrome Web Store icon & Small Windows 8 Star Screen Icon
// 144, // Deprecated: IE10 Metro tile for pinned site, iPad Retina with iOS <=> 6
152, // iPad Retina with iOS >= 7
167, // iPad Retina with iOS >= 10 (in practice iOS will still use 152×152)
180, // iPhone Retina
192, // Google Developer Web App Manifest Recommendation
// 195, // Deprecated: Opera Speed Dial icon (Not working in Opera 15 and later)
196, // Chrome for Android home screen icon
// 228, // Deprecated: Opera Coast icon
], '#d43431');

This code may not yield the best results under all circumstances. Downsized logos may appear a bit blurry. Whenever possible generate the PNGs yourself from a vector source and manually tune the PNG files' size with ImageOptim or a similar tool.

Analytics and cookie notices

Since you're browsing the web like the rest of us you must be already deeply frustrated by the cookie banners that litter every corner of the web. In most cases the only applicable cookies which are not mandatory are those used by analytics services such as Google Analytics. If your site needs to show a cookie banner just because you are using analytics stop and think: do you really need the features provided by Google Analytics or similar services?

In most cases you do not need those advanced features. If all you need is basic analytics about how many people visited your site, whether they came from organic search or a referral link and which country they come from you may have to look no further than your hosting control panel which most likely already offers AWstats or a similar solution. These work by parsing the web server access logs and require no cookies, therefore no cookie banners on your site.

Another solution is to use a self hosted analytics service such as Matomo (formerly called Piwik). It offers most of the features you'd get with Google Analytics but since it's a first party solution that's GDPR and CCPA compliant you don't even need to show a cookie banner. It can even import data from Google Analytics so you have continuity of your data if that's important to your business use case.

By removing the need for a cookie banner you make your site easier to navigate and far less frustrating to your users. Knowing that you can do that without sacrificing the data you need to make business decisions and without forcing your users to share their data with a third party that makes money off selling personalised ads (Google is primarily an ad network, if you hadn't already noticed) is empowering.

Oh, yeah, not having to use cookie banners? That makes your page's rendering performance much better too!

Dark Mode

Most likely your site's design only comes in one colour scheme and chances are it is a "light" theme, i.e. a bright background with dark text. This has been the case for most web sites the past two decades and is consistent with what most operating systems used to only offer as an option.

For some users this colour scheme is inaccessible and using inverted screen colours is simply not a very good solution; all colours are inverted, including illustrations and images, making for a jarring experience. Also offering a dark colour scheme for your site will make it more accessible. It will also make your site more appealing to people who just prefer dark colour schemes over light ones – a good point to consider if your site's target audience is geeks and gamers.

If your template is using Bootstrap 5 you can add automatic Dark Mode support with my DarkMagic plugin.

In my onw blog I have written more about what is Dark Mode, why and how to use it and I have delivered a presentation on the subject of Dark Mode. I will simply link to these resource instead of repeating myself in what is already a really long article.

AMP

When I first started working on this article series I intended on recommending AMP (Accelerated Mobile Pages) for your site. I am not so sure anymore.

If you didn't already know, AMP is pretty much a subset of HTML with some quirks that make it faster to load on mobile devices — or, at least, appear to do so. It is much more restrictive than a regular responsive site. The appeal of the AMP standard is that Google will prioritise AMP content in search result carousels when a user makes a search using Google's application on a smartphone.

The downside is that AMP is very much Google-specific and unsupported outside the Google ecosystem. Moreover, people clicking on search results pointing to your site's AMP pages will see google.com in their browser address bar instead of your site. I feel that these are major disadvantages which isolate you from your audience.

The fact that a member of the AMP Advisory Committee recently quit because he felt that Google had pretty much hijacked the entire AMP project for its own gain and that the Texas Attorney General has filed an anti-trust lawsuit against Google which would end AMP's preferential treatment in the search carousel make me wonder if there is any point in introducing AMP to your site. From my experience, the integration is all but straightforward if you want to create your own custom template for your AMP pages.

Finally, based on the analytics I had collected on my own site, AMP doesn't really bring in as much traffic as it's supposed to. Most of my traffic comes from direct links  from other places, organic search leading to my regular responsive template and social media. AMP was barely a blip in the radar of traffic sources.

So, instead of telling you how to integrate AMP on your site I will tell you that you probably shouldn't. If you are, however, hell bent on implementing it there are solutions for Joomla. I have used wbAMP but I have to warn you that creating a custom template is quite complicated. Between writing the first draft of this series and today I ended up removing AMP from my site.

To be continued

There is one final part to this series left. It's about some more generic, non-Joomla-specific things you can do to make your site more appealing to humans instead of just search engines and keep it fast. Until then, happy site building!

0
The March Issue
Explore the Core - Accessibility Add-ons in Joomla...
 

Comments 2

Already Registered? Login Here
Marc Dechèvre on Saturday, 26 March 2022 11:33
DarkMagic : not only for Cassiopeia then?

Oh, so actually DarkMagic works not only for Cassiopeia in the front-end (as mentioned on https://github.com/nikosdion/DarkMagic) but for any template based on Bootstrap 5 ?
If so maybe the readme.md on Github should be adapted then.

0
Oh, so actually DarkMagic works not only for Cassiopeia in the front-end (as mentioned on https://github.com/nikosdion/DarkMagic) but for any template based on Bootstrap 5 ? If so maybe the readme.md on Github should be adapted then.
Nicholas K. Dionysopoulos on Sunday, 27 March 2022 03:58
Kind of

Right now there's a hardcoded check for the template name in both front- and backend. You can, however, use the CSS provided with the plugin for your own custom template. The SCSS source is included in the plugin as well. So, you can very well install the plugin, NOT enable it, include its SCSS files from your template's SCSS and rebuild your CSS. If there's a use case where removing the template name check would make sense file a GitHub issue and I'll add a control in the configuration page.

0
Right now there's a hardcoded check for the template name in both front- and backend. You can, however, use the CSS provided with the plugin for your own custom template. The SCSS source is included in the plugin as well. So, you can very well install the plugin, NOT enable it, include its SCSS files from your template's SCSS and rebuild your CSS. If there's a use case where removing the template name check would make sense file a GitHub issue and I'll add a control in the configuration page.

By accepting you will be accessing a service provided by a third-party external to https://magazine.joomla.org/