Hello, fellow web perf enthusiast! Contribute to the 2024 edition happening this December. Click.

Web Performance Calendar

The speed geek's favorite time of year
2021 Edition
ABOUT THE AUTHOR
Erwin Hofman

Erwin Hofman (@blue2blond) started building his own CMS in 2004, which is now used by several Dutch web agencies. The CMS also became his playground to eventually help e-commerce agencies adopting and improving (perceived) performance, Core Web Vitals and accessibility.


A lot, maybe even too much has been written about icon loading strategies already. CSS-tricks.com already had a great (inline) SVG versus icon font comparison way back in 2014.

But here I am, in 2021, a frontend developer enthusiastic about performance and still using web fonts to show icons on my site. FontAwesome, to be precise. jQuery (or any other JS library) might have become technical debt over time and harder to move away from. But I’ll admit: that isn’t really the case with icons, right?

So here’s my side of the debate, including my migration plan.

Why I was still using icon fonts?

I had my reasons to still use icon fonts. First of all: convenience. Just embedding a CSS to show icons sure is convenient in multiple ways. I didn’t even want to spend a moment thinking about the time it would take to rename all icon elements across multiple templates.

But there were other reasons too:

  • Performance
    It’s not one file or strategy that will make or break your overall (perceived) performance. It’s often about how it is being used, instead of what you’re using.
  • Not render blocking
    A stylesheet embedding the font file(s) and containing the FontAwesome CSS classes can be render blocking. But sometimes I make sure to prevent it from being render blocking. Especially when fetched from public CDNs. Cache partition in browsers is another reason to be sure to not fetch them from public CDN’s.
  • Accessibility
    Unlike SVG’s, there is no way to add a title element to your icons. You could use aria-label though. But as I made sure that buttons were always accessible in other ways, this didn’t really felt like a big issue in my cases.
    I’m obviously forgetting about users overriding CSS for better readability. And I do felt guilty about this argument.
  • Browser caching
    The FontAwesome icon font always is the biggest individual font file on any of my sites. But we’ve got browser caching, right? I actually made sure to implement other pagespeed best practices to keep the amount of kilobytes low. That’s what I kept telling myself.
  • Slow internet
    But it’s still 76kb (FontAwesome 4.7.0 woff2 file), not ideal on a slow internet connection. So I came up with a solution in case someone enabled saveData/liteMode in their browser settings, or if anything below 4G was detected. Although this felt more ethical, it became a cumbersome solution over time, increasing maintainability.

Alternatives

First of all, I didn’t feel like upgrading FontAwesome, as it introduced additional files and thus requests. But always using and seeing the same icons started to become a bit boring. And despite sticking to my guns, I was aware of alternatives.

But the alternatives such as using JavaScript or inlined SVG’s didn’t feel much better. I questioned a setup of introducing additional JavaScript to do something trivial as showing icons on my site. I sometimes see websites where FontAwesome is moved over to JSON objects in JS files, instead of stylesheets. That isn’t better at all. Especially as nowadays the amount of main thread work often is more of a performance bottleneck then the amount of kilobytes served.

So, SVG to the rescue, right? Well, that depends on how many visitors are still using Internet Explorer. To prevent increased document size and maybe even chunked HTML, I did not feel like inlining SVG’s. But if you wanted to use external SVG’s, a browser compatibility fix was needed.

By the way, the FontAwesome v6 docs actually have a page describing different icon loading strategies and their impact on performance.

What changed my mind?

I wouldn’t be writing this post if I didn’t change my mind. I worked on a project where we used a different icon library. Still icon fonts, but seeing a different set of fonts instantly triggered a FontAwesome-fatigue. And you could even download the icons of the other icon library in basically any format.

1100+ pixel perfect icons also available as, .sketch, .fig, .studio, .xd, .iconjar, .ai, .svg, .png, .pdf, .woff, .ttf
‘The icon of’ is an icon library that comes in different file formats.

Combine this with a website that I was building with the same boilerplate as my own site, but expecting (more) 3G visitors. Time for a change.

Let’s migrate

The above were ingredients to trigger a feeling that we sometimes have as developers: it shouldn’t be too hard to come up with a straightforward solution without having to change too much within the stack and without using third-party dependencies. I wouldn’t dodge the customization bullet as I had some specific challenges, so I didn’t expect to save any time trying to implement third-party solutions.

Migration challenges

My (and most likely any) situation came with the following challenges:

  • As mentioned before: naming convention. Each library will use different names and naming conventions for icons.
  • The CMS that is used allows content managers to add icons via the FontAwesome plugin. So I would also have to consider user generated content.
  • I wanted to come up with an external SVG containing all possibly needed icons across different pages to reduce the amount of requests and to benefit from browser caching. It would already involve less unused fonts than embedding a whole icon font.
  • Something I found out after migrating: Instead of always using an element with FontAwesome classes, I sometimes used the :before pseudoclass in my custom CSS to display icons. You could say this was my icon font-related technical debt as I didn’t always use FontAwesome the way it was meant to be used.

The technical part

As the website already used some icons, I had to search for the equivalent SVG’s in my newly used icon library. I renamed them manually to match FontAwesome’s naming convention. The SVG itself could look like the following:

<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>add</title><path d="M19,11.5v1a.5.5,0,0,1-.5.5H13v5.5a.5.5,0,0,1-.5.5h-1a.5.5,0,0,1-.5-.5V13H5.5a.5.5,0,0,1-.5-.5v-1a.5.5,0,0,1,.5-.5H11V5.5a.5.5,0,0,1,.5-.5h1a.5.5,0,0,1,.5.5V11h5.5A.5.5,0,0,1,19,11.5Z"/></svg>

With this as a starting point, I had to remove the id attribute and transform it into a symbol-element to be able to embed it into a SVG sprite. I ended up using PHP’s DOMDocument.

And now that I had a sprite SVG, I could use the use element, like this:

<use xlink:href="/svg/sprite.svg#add"></use>

But I also wanted a server side rendered solution, instead of using JavaScript to swap FontAwesome placeholders into this HTML setup. So I used PHP to do this for me before a server side cache is created:

preg_match_all('|(<span[^>]*class="([^"]*fa-[^"]*)"[^>]*>)([^<]*)</span>|', $html, $icons, PREG_SET_ORDER);
$iconsLooped  = [];
foreach ( $icons as $icon ) 
{
  /**
   * 0 = matched HTML  that we want to replace
   * 1 = opening span element (ignore)
   * 2 = class list we'll use to filter the icon name
   * 3 = contents within the FA element (ignore as it should be empty anyway)
  **/

  // no need to try to transform already transformed icons
  if ( isset($iconsLooped[ $icon[0] ]) ) {
    continue;
  }

  // get rid of FA's fixed-width class first
  $iconClasses  = explode(' ', str_replace('fa-fw', null, $icon[2]) );
  $iconClass    = null;
  foreach ( array_map('trim', $iconClasses ) as $tmpClass ) {
    if ( strpos($tmpClass, 'fa-') === 0 ) {
      $iconClass = substr( $tmpClass, 3 );
      break 1;
    }
  }
  
  if ( $iconClass ) {
    $iconsLooped[ $icon[0] ] = 1;
    $iconHtml  = str_replace('</span>', '<svg><use xlink:href="/svg/sprite.svg#' . $iconClass . '"></use></svg></span>', $icon[0]);
    $html   = str_replace( $icon[0], $iconHtml, $html );
  }
}

This would result in the following HTML for an individual icon:

<span class="fa fa-fw fa-add"><svg><use xlink:href="/svg/sprite.svg#add"></use></svg></span>

The advantage of this solution is that it would also transform icons inserted by CMS users (user generated content). And keeping the parent FontAwesome wrapper element enabled me to do additional styling and aligning on top of the icon itself as well as JavaScript mutations (see below).

I know you would normally want to do this using DomDocument, but that’s another discussion and not faster by the way 😉 (yes, I tested this).

The JavaScript

I would still have to use JavaScript, because new elements could be introduced by CMS users. Although the HTML was transformed already, it’s possible that some icons aren’t part of our SVG sprite yet. So I came up with the following JavaScript (jQuery syntax) to then fetch the individual icons:

$.ajax({url:'/svg/sprite.svg', dataType: 'html', cache: true, success: function (data) 
{  
  let isIE = navigator.userAgent.indexOf("MSIE ") > -1 || navigator.userAgent.indexOf("Trident/") > -1,
    spriteSvg = $( data ),
    iconsFa = $('.fa'),
    iconsLooped = {};
    
  for ( let i = 0; i < iconsFa.length; i++ ) 
  {
    let iconName = ( iconsFa[i].getAttribute('class').replace('fa-fw','').match(/(^|\s)fa-\S+/g) || [])[0].trim().substr(3);
    if ( iconName in iconsLooped ) {
      continue;
    }
    
    iconsLooped[ iconName ] = 1;
    let spriteIcon = spriteSvg.find('symbol[id="' + iconName + '"]');
    if ( spriteIcon.length == 0 ) {
      $.ajax({url:'/svg/' + iconName + '.svg', dataType: 'html', cache: true, success: function(data) {
        $('.fa-' + iconName ).html( data );          
      }});
    }
    else if ( isIE ) {
      // fetch from sprite
      let faHtml = isIE ? '<svg role="img" viewBox="0 0 24 24">' + spriteIcon.html() + '</svg>': 
        '<svg><use xlink:href="sprite.svg#' + iconName + '"></use></svg>';
      $('.fa-' + iconName).html( faHtml );
    }
  }
}});

This piece of JavaScript is doing the following:

  1. fetch the sprite.svg (by this time, it should already be in the browser cache due to the server side rendered svg use);
  2. loop all icons on the current page using the FontAwesome syntax;
  3. get the actual classname;
  4. check if the icon is in the sprite;
  5. if not, then fetch the individual SVG file and write the SVG icon contents into the FontAwesome wrapper;
  6. otherwise, if you still want IE support, then grab the SVG icon from the already fetched sprite, and insert it into the FontAwesome wrapper. This prevented me from having to use the mentioned browser compatibility library.

All icons that are in the sprite already, are already picked up by the browser.

You could choose to preload of prefetch this file to allow the browser to put it in the cache before this piece of JavaScript needs it. However, as I don’t consider icons to be as critical as my LCP candidate or the text itself, I choose to not do this.

The icon build proces

I wanted to come up with a solution that would automate this as much as possible, instead of baking this into my build proces. Especially as we still have to consider icons inserted by CMS users, there is no way of making this a task of the build process

So, I created the following .htaccess in my svg directory:

RewriteCond %{REQUEST_URI} !^/svg/sprite.svg$ [NC]
RewriteRule .+ index.php [L]

As a result, any request to an SVG file, expect for the sprite file itself, would result in being processed by the index.php in the same directory. I then made this index.php responsible for picking the right SVG file belonging to the request path:

$iconPath = '/upload/svg/'; // see 'Additional thoughts'
$uri 	= $_SERVER['REQUEST_URI'];
$path	= pathinfo( $uri );
$basename = preg_replace('/[^[:alnum:]-]/', '', $path['filename'] );
$filename = $iconPath . $basename . '.svg';

Next, the contents of the file would be returned and would also be added to the SVG sprite for the next visitor. Using PHP’s DOMDocument I remove any fill or other redundant attributes to be able to override them using CSS. For example on mouseover or focus states.

In this proces, I also remove the title element because accessibility is dealt with in a different way already (and using multiple title elements on a page can still result in weird title concatenation when sharing such pages using social media platforms).

I actually add the role="img" attribute and add an id attribute to be able to identify the icon in the JavaScript snippet.

When it comes to visitors, both the prior SVG sprite version as well as the individual SVG files are then cached in the browser. So there also is no real performance penalty here as this PHP step is only performed the first time.

Aligning the icons

Aligning SVG icons with your text is totally different challenge. I obviously did some homework here, but already experienced it will be different per site, depending on the body font being used.

I ended up using the following CSS:

.fa {
    box-sizing: content-box;
    min-width: 1.1em;
    display: inline-block;
}
.fa svg {
    width: 1.1em;
    height: 1.1em;
    vertical-align: middle;
    display: inline-block;
    position: relative;
    fill: currentColor;
}

But on my own site, I had to add top: -0.1em; to .fa svg to better align it with the text.

Migration outcome

Although most users wouldn’t even notice any performance win or real visual change, it felt way cleaner already. Instead of a 76kb woff2 file, it’s now a 23kb SVG file as the amount of unused icons got reduced massively. Moreover, I’m able to ditch the whole CSS file that was needed in combination with the icon font files.

It also became way easier to introduce new icons as its just another SVG icon file that has to be added. And when wanting to switch to inlined SVG’s in case the SVG sprite becomes to big, I only need to change the strategy to inlined SVG’s while keeping the styling and HTML markup.

Additional thoughts

There are some other things I considered (and implemented) as well. For example, the index.php file will actually grab the individual icons from a totally different directory. A directory that can be accessed via the CkEditor within the CMS. This way, if a specific icon is missing, CMS users are able to upload new icons themselves. Or even replace icons.

Obviously, completely new icons that aren’t part of the CkEditor FontAwesome plugin, can’t be selected by CMS users. They would actually have to edit the HTML itself within the editor.

There is also the chance of using icons on very obscure pages. When individual SVG’s weren’t uploaded, no icon will be shown at all. I came up with a file_exists check. If an individual SVG doesn’t exist, then an SVG cross icon is returned while also logging the icon name to a 404 log. This way, I know if I missed some icons.

You might also want to set the cache duration of icons. For example if you want to use a totally different icon library in the future. Or maybe changing the SVG structure. I’m using .htaccess to set the cache duration for the sprite file and PHP to set the caching duration for the individual files.

Proof of concept

Although already implemented in a few sites, including my own, I still consider this a proof of concept. So if anyone has additional thoughts to improve frontend or backend performance, don’t mind sharing them!