XF 2.3 Boosting performance in XenForo 2.3

googlechrome.github.io_lighthouse_viewer_ (4).png
In today's 'Have you seen...?' entry for XenForo 2.3 we're going to look at how we've zeroed in on enhancing performance, ensuring your community has a swift and seamless experience. We're going to take a deep dive into the advancements we've made and how they stack up against various performance metrics.

But, before we get into the individual changes, let's take a quick look at the baseline - this is our current Performance score as calculated by Lighthouse for the XenForo Community forum list:


Here's the performance scores for some other forum software:


It's crucial to note that while this score provides a performance benchmark, it isn't the only indicator of success. Indeed, results can fluctuate slightly with multiple test runs. A site can still enjoy popularity with a lower score, but a higher rating undeniably enhances both search engine rankings and overall user experience.

Stay tuned! We'll reveal XenForo 2.3's updated score shortly. But before that, let's dive into the changes that have brought us here, shall we?

If you would like to jump to a particular section, use the links below.
Alternatively, if you want to skip a whole lot of reading, check out the TL;DR below:

To view this content we will need your consent to set third party cookies.
For more detailed information, see our cookies page.

Oh and if you're concerned that all we've got to show you in XenForo 2.3 is performance improvements - fear not.

We will be showcasing a brand new feature next week!

Font Awesome improvements​

On almost every page we serve currently, we include as many as five different variants of the Font Awesome 5 Pro icon library. That's up to 8,000 icons split across their different variations, but on any given page we actually use... a lot less than that. The file size of each of these fonts is up to 200KB with an additional 40KB worth of CSS.

We have always wanted to ship a much smaller subset of icons but when admins, style designers and add-on developers have the full suite of icons available to them, this can be a challenge. In particular, working with fonts directly in order to create a subset can prove to be somewhat tricky.

Back in 2019, we prototyped something very similar to the excellent Font Awesome Manager add-on released by @Kirby, but we ultimately decided that this wasn't necessarily the approach we wanted to take.

In XenForo 2.3 we have developed a brand new way of subsetting Font Awesome icons so that only the icons which are actually used across your XenForo installation are ever served to your users and, even better, it's entirely automatic!

Icon usage analyzer​

XF 2.3 now includes an icon usage analyzer which can analyze precisely which icons are being used across various content types, including within Less templates, and HTML templates, template modifications, phrases and even within JavaScript.

The way that you use icons is not significantly changing from what it was in XF 2.2. For example, if you're familiar with using, e.g. <xf:fa icon="fa-comments" /> in templates or .m-faContent(@fa-var-comments) in Less templates then you're all set.

However, some approaches will no longer work. For example, using <i class="fas fa-comments"></i> will no longer work. These usages will not be analyzed and will not result in an icon being displayed. Because of this, there is a brand new syntax for use in phrases.

See the table below for examples:

Templates / template modifications
<xf:fa icon="fas fa-comments" />
Less templates
XF.Icon.getIcon('solid', 'fa-comments')

We are aiming to go into a little more detail surrounding the icon usage analyzer in an upcoming developer-focused 'Have you seen...?' but for now it is important to note that the usage analyzer can of course be extended in order to support analyzing in custom content types and other scenarios, including special casing specific icons.

Rendering icons​

So, what do we do with the analyzed icons? Well, rather than working with a somewhat clunky font file format, we use the information to produce an SVG sprite sheet. Actually, not just one, but one for each of the Font Awesome 5 Pro styles - light, solid, regular, and duotone and each of those contains only the icons we analyze in those styles.

For example, here's the full extent of the spritesheet we generate for the solid style, by default:

<?xml version="1.0" encoding="UTF-8"?>
Font Awesome Pro by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license (Commercial License)
<svg xmlns="http://www.w3.org/2000/svg">

    <symbol id="check-circle" viewBox="0 0 512 512">
        <path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
    <symbol id="exclamation-circle" viewBox="0 0 512 512">
        <path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
This is only loaded on the pages that actually use those icons but, what is even better, is that whereas previously we'd load a full 137 KB font file, this spritesheet weighs in at a mere 726 bytes.

Overall, the total file size we were loading for icons by default on the forum home page was around 400 KB now it will be less than 40 KB.

JavaScript performance​

Now we must turn our attention over to JavaScript and look at the potential performance savings we have incorporated there.

Native defer for JavaScript​

JavaScript standards have changed a significant amount over the years. It wasn't that long ago that the best practice was to move your <script> tags down to the bottom of your HTML in order to delay necessary client side code execution to be as late as possible, with a view to ensuring that as much of the page content can be parsed first, before ever increasingly complex JavaScript code loads and blocks the remaining parsing of the page.

Thankfully, JavaScript and most modern browsers support the new defer directive on <script> tags, allowing us to move them back up towards the top of the DOM.

This allows the browser to start downloading the scripts slightly earlier, while deferring their execution until the rest of the DOM is loaded.

Lazy loading JS handlers​

In previous versions of XenForo a huge chunk of our JavaScript code was considered to be "core" JavaScript, compiled into a single file which was loaded on every page. In XenForo 2.3 we have built a system for lazy loading commonly used handlers when they are needed. They are still grouped into form, action, structure, and tooltip handlers, but rather than all of these handlers being loaded on every page, they are now loaded and registered only if needed on the current page.

This represents a sizeable saving on most page loads, ensuring that pages are loading as quickly as possible.

Bye bye jQuery​

After being part of our product since the beginning, it is now the right time to say goodbye to jQuery.

jQuery is a JavaScript library which encapsulates a great deal of native JavaScript functionality into an alternative set of functions with a liberal sprinkling of syntactic sugar.

JavaScript and browser standards have evolved significantly over the history of XenForo. At one point it would have been unfathomable to use anything else. In the not too distant past, jQuery was practically essential for even being able to find a specific element with a specific class name, or being able to support the seemingly endless quirks in now-ancient versions of Internet Explorer and others.

It's a sizeable library in itself too weighing in at over 30 KB added to every page load, and how much of that library were we actually using, anyway?

Well, as the developer who personally went through and rewrote nearly 40,000 lines of code, a lot less than you'd think. And of the language features of jQuery we were using, many things are a simple straight swap to a native JavaScript function that, a long time ago, may either have not existed at all, or too new to have garnered enough browser support.

We acknowledge that there will be some pain points for existing developers who have existing code based on jQuery but, in truth, as long as you aren't maintaining anywhere near the 40,000 lines of code we are, it should be a relatively smooth transition. But, if you get completely stuck, you could always re-add jQuery if you wish but, we'd recommend avoiding that if you can. And removing jQuery as a dependency can start now if you're planning on making changes to existing code before XenForo 2.3 is released. We strongly advise against writing new code that directly uses jQuery functionality at this point.

If needed, we can go into a little bit more technical detail down the road about the changes we have made, but here are some highlights.

Note: The next section gets rather into the weeds in terms of development specifics so move on to the next section if this doesn't interest you.

Block scoped variables​

While not strictly related to jQuery, it's worth noting that we no longer use var to define variables in favour of using let and const. This makes the scoping of variables clearer and code less error prone and more predictable.

Selecting element(s)​

Selecting elements from the DOM is probably the most frequent operation you'll perform in JavaScript and therefore this is a significant change which, while slightly more verbose, makes code much clearer and less error prone.

jQuery / XF 2.2​

var $element = $('.someClassName')
if ($element.length)
   // you have an object containing one or more elements; you can call various methods which will interact with this element or elements

JavaScript / XF 2.3​

const element = document.querySelector('.someClassName')
if (element)
    // you have an instance of HTMLElement, if more elements exist with the same selector, you have the first element

// ... or

const elements = document.querySelectorAll('someClassName')
if (elements.length)
    // you have a NodeList object containing one or more HTMLElement objects

Arrow functions​

Again, while not jQuery related, you will now see arrow functions being utilised as much as possible. As well as being syntactically nicer to use than traditional anonymous functions, they do not create new bindings for keywords such as this.

jQuery / XF 2.2​

var self = this
var callback = function (foo)

JavaScript / XF 2.3​

const callback = (foo) =>

// ...or

const callback = (foo) => this.doSomething(foo)

Event handling​

Some functionality provided by jQuery was difficult to leave behind, and the majority of those really useful methods have been rewritten and ported to vanilla JavaScript as additional methods to our XF object. Not least of these is jQuery's event management which supports namespaced events and provides a more intuitive way of removing event listeners from an element that doesn't require a reference to the original event callback.

jQuery / XF 2.2​

var $element = $('.someClassName')
$element.on('namespace.click', function (e)

JavaScript / XF 2.3​

const element = document.querySelector('.someClassName')
if (element)
    XF.on(element, 'namespace.click', e =>
        XF.off(element, 'namespace.click')


This is mostly unchanged from XenForo 2.2 because we still have a XF.ajax() wrapper to use as a helper method but, behind the scenes, rather than using jQuery's $.ajax() method (which is a wrapper around XMLHttpRequest) we have migrated over to using the more modern, Fetch API.

The main thing to be aware of here is that the Promise methods available from the result of calling XF.ajax() are named slightly differently to what they were with jQuery.

jQuery / XF 2.2​

var t = this
XF.ajax('some-url', data, callback)
    .always(function ()
        t.loading = false

JavaScript / XF 2.3​

XF.ajax('some-url', data, callback)
    .finally(() =>
        this.loading = false

Storing arbitrary data for an element​

Some of jQuery's features, while powerful, can sometimes appear inconsistent or ambiguous. One such feature is the data method available on jQuery objects. Depending on its usage, it can manifest different behaviors. Consider the following example:

jQuery / XF 2.2​

var $element = $('.someClassName').first() // <span class="someClassName" data-foo="1"></span>

var foo = $element.data('foo') // reads the data-foo attribute from the element in the DOM

var bar = $element.data('bar') // attempts to read the data-bar attribute from the element which doesn't exist, but the internal data store may have a value set

$element.data('bar', [1, 2, 3]) // sets the bar key in the internal data store to an array

$element.data('foo', '100') // the foo entry in the internal data store for this element now returns 100, ignoring the data-foo attribute which remains unchanged in the actual DOM

In XenForo, there remains a necessity to store arbitrary data, especially data that isn't always a string. However, our current approaches are more predictable and consistent:

JavaScript / XF 2.3​

const element = document.querySelector('.someClassName') // <span class="someClassName" data-foo="1"></span>

const foo = element.dataset.foo // reads the data-foo attribute from the DOM

element.dataset.foo = '100' // sets the data-foo attribute in the DOM

XF.DataStore.set(element, 'bar', [1, 2, 3]) // a new XF.DataStore class is introduced for reading / storing arbitrary, non-string data

const bar = XF.DataStore.get(element, 'bar') // returns an array: [1, 2, 3]

Handler targets​

We have a special variable which we pass in to all element/event handlers which is this.$target currently. Note that this becomes this.target in XF 2.3 as conventionally the $ prefix on variable names typically is used to denote a jQuery object. In XF 2.3 this.target represents a HTMLElement object.

To find children of the target, this is a little more consistent with vanilla JavaScript than it was with jQuery:

jQuery / XF 2.2​

var $child = this.$target.find('.someChild').first() // returns the first child element which matches the someChild class

JavaScript / XF 2.3​

const child = this.target.querySelector('.someChild') // returns the first child element which matches the someChild class; this will be a HTMLElement; note it uses the same querySelector method rather than a separately named method

Migration support​

While understandably, some of the custom methods in XF 2.3 will be unavailable to you until release, we would encourage you to start migrating as much code as possible now to use vanilla JavaScript where practical.

If you feel you need support with converting code to vanilla JavaScript we have approximately 40,000 lines of experience between us and we will attempt to reply to queries in the XenForo development discussions forum where we can.

Improved CSS performance with HTTP/2+​

For webservers running HTTP/2 or higher, one of the key advantages is the support for multiplexing. Multiplexing allows for multiple resource requests (like JavaScript and CSS) to be handled concurrently, reducing overhead and boosting page load speeds.

In XenForo 2.3, we've optimized this functionality by refining how CSS requests are made.
  • The core CSS, which is consistent across all pages, continues to be bundled into a single request.
  • CSS specific to individual templates or pages will now be requested separately, rather than bundled together.
For a clearer representation of these changes, see below.


<link rel="stylesheet" href="css.php?css=public:normalize.css,public:fa.css,public:variations.less,public:core.less,public:app.less" />
<link rel="stylesheet" href="css.php?css=public:node_list.less,public:notices.less,public:share_controls.less,public:extra.less" />


<link rel="stylesheet" href="css.php?css=public:normalize.css,public:fa.css,public:variations.less,public:core.less,public:app.less" />
<link rel="stylesheet" href="css.php?css=public:node_list.less" />
<link rel="stylesheet" href="css.php?css=public:notices.less" />
<link rel="stylesheet" href="css.php?css=public:share_controls.less" />
<link rel="stylesheet" href="css.php?css=public:extra.less" />

By making these requests individually and unbundling the CSS from others, this allows the individual templates to be cached and reused more effectively across different pages.

Drum roll, please...​

We previously discussed several optimizations made to XenForo 2.3, each aiming to elevate our Lighthouse performance score:
  • Switching to SVG sprite sheets for Font Awesome, reducing the need to load complete web fonts.
  • Introducing native defer for <script> tags.
  • Implementing lazy loading for JS handlers.
  • Decreasing page size by removing jQuery as a dependency.
  • Harnessing the power of HTTP/2 multiplexing and unbundling CSS.
With these enhancements, XenForo 2.3 boasts a significant improvement over the Lighthouse score of 84 in XenForo 2.2. Each enhancement plays a critical role, from advanced JavaScript loading techniques to optimizing response size and harnessing the capabilities of modern web servers.

But the proof, as they say, is in the pudding:

googlechrome.github.io_lighthouse_viewer_ (1).png

This high performance is consistently seen throughout the software:

googlechrome.github.io_lighthouse_viewer_ (2).png
googlechrome.github.io_lighthouse_viewer_ (3).png

Although we're tantalizingly close to a perfect score of 100, anticipate even more refinements with the introduction of the new style in XenForo 3.0.
Wowsers! Love this stuff y'all! The JavaScript stuff is mostly gibberish to me but, as someone who is constantly worrying about page load speeds on my site, the rest of this is very much appreciated! Can't wait to see what else y'all have in store!

I may be in the minority here, but I'd already say we're well on the way to 2.3 being worth the wait!
Good update! Performance improvements are very welcome.

By making these requests individually and unbundling the CSS from others, this allows the individual templates to be cached and reused more effectively across different pages.

For a guest / unregistered user visiting for the first time, doesn't this slightly slow down (hopefully negligible) the loading time for the CSS, due to sending multiple requests?
Top Bottom