Back

Reduce Material Design Icons Font to 7KB and automate with PyAutoGUI

January 5, 2022 11 minute read
Pounding hammer on hot iron forge
Source: Unsplash

This article will cover how I reduced the total size of loading Material Design Icons Font from 361KB to 7KB, and then automated that process using PyAutoGUI. We’ll go through a full end-to-end tutorial of the process. If you want to follow along you can download the distribution code before continuing.

Why optimise Material Design Icons Font?

Website performance is critical for delivering a solid user experience. It is especially important for serving web pages to mobile devices and/or locations with poor network connectivity. Not only that, lower page sizes saves bandwidth usage and in turn saves money. Every byte and millisecond counts. I try to identify any opportunity to improve performance and page speed I can. It’s a continuous process of improvement. I recently optimised this site and ticked off the following improvements:

  • Compressing images with tools like TinyPNG
  • Using smaller image formats like WebP
  • Lazy loading images and videos below the fold
  • Lazy hydration for SPA’s
  • Minifying JavaScript and CSS
  • Reducing payload sizes for data requests
  • Reducing webpack bundle size
  • Eliminating any redirects
  • Caching or precomputing results for expensive operations

There was something still bothering me though, I had a Lighthouse error indicating “ensure text remains visible during webfont load”.

This was because I was using a CDN to pull in the Material Design icons stylesheet from cdn.jsdelivr.net which then downloaded the woff2 font. From the CDN, the font file weighed in at 320KB and the stylesheet was 43.5KB.

The solution recommended was to add font-display swap to the font stylesheet selector. I know that seems silly for an icon font as there is no 'fallback' for icons really, a better suggestion for icon fonts might be to use font-display block instead. This was impossible to achieve using a CDN although I didn’t mind the idea of self-hosting icon web fonts. I knew there were trade offs between using a CDN opposed to self hosting, but in the interests of site reliability (who wants to use a site without icons if the CDN stops working, right?) I decided to self-host the icon font. This is where my optimisation experiment began!

Download the Material Design Icon pack

With the distribution code I’m starting with a simple HTML page, alongside empty style and font folders. We can see the page has 19 Material Design icons and they are coming from the CDN source to begin with.

The Material Icons are being loaded via the stylesheet link tag in the head section of the document, which then loads the 320KB woff2 font file which you will see by hitting F12 and inspecting the Network tab in Chrome (or a different browser's) DevTools. To make viewing this information easier, you can filter the Network tab to just 'CSS' and 'Font' like in the image above.

index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Icons Site</title>
  <link rel="stylesheet preload" href="https://cdn.jsdelivr.net/npm/@mdi/font@5.8.55/css/materialdesignicons.min.css">
</head>

<body>
  <span class="icon"><i class="mdi mdi-48px mdi-language-cpp"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-cpp"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-csharp"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-python"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-javascript"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-ruby"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-html5"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-css3"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-fortran"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-go"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-java"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-kotlin"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-lua"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-markdown"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-php"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-r"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-language-web"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-cpu-64-bit"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-server"></i></span>
  <span class="icon"><i class="mdi mdi-48px mdi-access-point-network"></i></span>
</body>

</html>

We’re going to swap this out for a locally hosted icon font and stylesheet. Download the Material Design icon font and extract the contents.

Move all of the font files in the 'fonts' folder to our project's 'fonts' folder. Then move 'materialdesignicons.css' in the 'css' folder to our project's 'css' folder. At the end, we'll only be using the .woff2 file as it provides improved compression and is supported by major browsers.

With the stylesheet and the font files in the correct folders, let’s hook up the stylesheet and remove the CDN by updating the head section.

index.html
...
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Icons Site</title>
  <link rel="stylesheet preload" href="css/materialdesignicons.css">
</head>
...

To eliminate the pesky 'ensure text remains visible' Lighthouse error I also added 'font-display block' to 'materialdesignicons.css'.

materialdesignicons.css
/* MaterialDesignIcons.com */
@font-face {
  font-family: "Material Design Icons";
  src: url("../fonts/materialdesignicons-webfont.eot?v=6.5.95");
  src: url("../fonts/materialdesignicons-webfont.eot?#iefix&v=6.5.95") format("embedded-opentype"), url("../fonts/materialdesignicons-webfont.woff2?v=6.5.95") format("woff2"), url("../fonts/materialdesignicons-webfont.woff?v=6.5.95") format("woff"), url("../fonts/materialdesignicons-webfont.ttf?v=6.5.95") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

After hard refreshing the page (Ctrl + F5) you should see the icon font is still working as expected but with the CDN removed. Checking the Network tab again we can see the icons are now being loaded locally via 'materialdesignicons.css'. 👍

The major problem from the image above is that the 'materialdesignicons.css' file is over 26,000 lines of code for over 5,000 icons, and is 369KB, and the .woff2 file is 361KB, and yet we’re only using 19 icons! The page load time will be bloated, our bandwidth is being consumed and the visitor experience badly affected as a result. The average web page is around 2-3MB, but the recommended size is 1MB. This is 73% of that recommended 1MB in the icon stylesheet and font alone! We could minify 'materialdesignicons.css' to look similar to 'materialdesignicons.min.css' from the original download which is 298KB but that's still too large. Let’s embark on the next step in our efficiency quest to reduce both the stylesheet and font file sizes.

Identify which icons are actually being used throughout the site

I first searched the site to figure out which icons were actually being used throughout it. The one page site we’re using has 19 icons, this site had around 84, mostly in the tools (especially the System Capacity Calculator). I made a note of these by inspecting with DevTools, finding the CSS selector in the full stylesheet 'materialdesignicons.css', then copying them into a separate Notepad++ file. This can take a little time, but well worth it!

Remove unused selectors from the stylesheet

The result of my investigation to identify the icons actually used gave me a list of 19 CSS selectors. I made a backup of the full stylesheet in case I wanted to add any more icons in the future, but after replacing the body with the condensed list this is what it looked like:

materialdesignicons.css
/* MaterialDesignIcons.com */
@font-face {
  font-family: "Material Design Icons";
  src: url("../fonts/materialdesignicons-webfont.eot?v=6.5.95");
  src: url("../fonts/materialdesignicons-webfont.eot?#iefix&v=6.5.95") format("embedded-opentype"), 
       url("../fonts/materialdesignicons-webfont.woff2?v=6.5.95") format("woff2"), 
       url("../fonts/materialdesignicons-webfont.woff?v=6.5.95") format("woff"), 
       url("../fonts/materialdesignicons-webfont.ttf?v=6.5.95") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

.mdi:before,
.mdi-set {
  display: inline-block;
  font: normal normal normal 24px/1 "Material Design Icons";
  font-size: inherit;
  text-rendering: auto;
  line-height: inherit;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.mdi-language-c::before {
  content: "\F0671";
}

.mdi-language-cpp::before {
  content: "\F0672";
}

.mdi-language-csharp::before {
  content: "\F031B";
}

.mdi-language-css3::before {
  content: "\F031C";
}

.mdi-language-fortran::before {
  content: "\F121A";
}

.mdi-language-go::before {
  content: "\F07D3";
}

.mdi-language-html5::before {
  content: "\F031D";
}

.mdi-language-java::before {
  content: "\F0B37";
}

.mdi-language-javascript::before {
  content: "\F031E";
}

.mdi-language-kotlin::before {
  content: "\F1219";
}

.mdi-language-lua::before {
  content: "\F08B1";
}

.mdi-language-markdown::before {
  content: "\F0354";
}

.mdi-language-php::before {
  content: "\F031F";
}

.mdi-language-python::before {
  content: "\F0320";
}

.mdi-language-r::before {
  content: "\F07D4";
}

.mdi-language-ruby::before {
  content: "\F0D2D";
}

.mdi-cpu-64-bit::before {
  content: "\F0EE0";
}

.mdi-server::before {
  content: "\F048B";
}

.mdi-access-point-network::before {
  content: "\F0002";
}

.mdi-18px.mdi-set, .mdi-18px.mdi:before {
  font-size: 18px;
}

.mdi-24px.mdi-set, .mdi-24px.mdi:before {
  font-size: 24px;
}

.mdi-36px.mdi-set, .mdi-36px.mdi:before {
  font-size: 36px;
}

.mdi-48px.mdi-set, .mdi-48px.mdi:before {
  font-size: 48px;
}

You can use this to replace the entire contents of the stylesheet. 112 lines is much better than 26,000 lines. This file now weighs in a at 2.1KB and we’re feeling lighter already. If we hard refresh again, we can see the site is still working as expected 😆

Now onto the harder part, optimising the font file.

Remove unused selectors from the font file

So we’ve reduced the size of stylesheet to only the icons we’re using, how do we do the same for the .woff2 font file? To do this I used a free tool called FontForge. The process sounds difficult at first but this is what worked for me:

  1. Download FontForge
  2. Open the .ttf font file
  3. Select the icons you want to keep by searching for an icon with Ctrl + Shift + > then ticking 'Merge into selection'
  4. Invert the selection (selecting all the icons you want to get rid of)
  5. Remove the unused icons
  6. Condense
  7. Generate the font
  8. Save as .woff2

This process feels very repetitive and I certainly wasn’t doing this for 84 icons or even for our 19 icons. Of course, if you’re only using 5 you might not mind searching for them then removing the rest, but for any more it’s tedious. I automated this step using PyAutoGUI and have a video of a robot following the process outlined above in the next section.

Automate the minify font file process with PyAutoGUI

So we’ve decided selecting 19 or more icons is too repetitive, time consuming and prone to human error. Let’s automate the process. This video shows the IconFontMiniferRobot in action following the process we outlined earlier. I opened FontForge, loaded the .ttf file, ran the robot, switched back to FontForge and the robot takes over. I just love building robotic automation process solutions.

How to change video quality?
How to change video playback speed?

The robot reads the CSS stylesheet, extracts the icons selector names used in the stylesheet using a regular expression, selects those identified icons in FontForge, selects the inverse, condenses and generates the font then saves as a .woff2 file and it’s only 2.8KB! When I did the same thing for this site it was around 7KB for 84 icons. Now we can test it still works in our site. Before that though, you want to see the code for the robot right?

icon_font_minifier_robot.py
"""Automates removing unused material icons from a .tff font file.

Reads in a material icons scss or css file and parses applied css 
selectors such as '.mdi-close-box-multiple-outline::before'. Uses
PyAutoGUI to control FontForge in order to remove all unused icons 
from the .tff file then saves the output as a .woff2 file.

Ensure FontForge is opened and loaded with the .tff before running, 
then run the program, switch to FontForge and let the robot take over :)

  Typical usage example:

  robot = IconFontMinifierRobot()

  robot.removeUnusedIcons(
    css_filepath="css/materialdesignicons.css",
    woff2_output_path="C:\\Users\\shedloadofcode\\Documents\\icon-fonts-project\\fonts\\"
  )
"""
import pyautogui
import re
import time

class IconFontMinifierRobot():

    def removeUnusedIcons(self, css_filepath, woff2_output_folderpath):
        print(f"Opening stylesheet...")
        stylesheet = open(css_filepath, "r")
        
        print("Parsing stylesheet...")
        icons = self.get_icon_styles_from(stylesheet)

        print("Now switch active window to FontForge :)")
        time.sleep(15)

        print("Selecting icons in FontForge...")
        for icon in icons:
          self.select_icon_in_fontforge(icon)

        print("Removing icons not in CSS...")
        self.invert_selection()
        self.detach_and_remove_selected_glpyhs()
        self.make_compact()

        print("Generating .woff2 file...")
        self.generate_fonts(
            woff2_output_folderpath, 
        )
        self.confirm_generate()
        print("Font saved.")

    def get_icon_styles_from(self, stylesheet):
        pattern = re.compile(r"\.mdi-[a-z\-A-Z\-0-9]+::before")
        icon_styles = pattern.findall(stylesheet.read(), re.IGNORECASE)
        print(f"{len(icon_styles)} icons found.")
        
        return icon_styles

    def select_icon_in_fontforge(self, icon):
        pyautogui.hotkey("ctrl", "shift", ">")
        time.sleep(0.5)
        pyautogui.typewrite(
            icon.replace(".mdi-", "").replace("::before", "")
        )
        pyautogui.moveTo(927, 533)
        pyautogui.click()
        time.sleep(0.5)
        pyautogui.moveTo(915, 560)
        pyautogui.click()
        time.sleep(0.5)
        pyautogui.press('enter')
        time.sleep(2)

    def click_encoding_menu(self):
        pyautogui.moveTo(240, 35)
        pyautogui.click()
        time.sleep(2)

    def invert_selection(self):
        pyautogui.moveTo(53, 32)
        pyautogui.click()
        time.sleep(1)
        pyautogui.moveTo(112, 477)
        pyautogui.click()
        time.sleep(1)
        pyautogui.moveTo(444, 503)
        pyautogui.click()
        time.sleep(2)

    def detach_and_remove_selected_glpyhs(self):
        self.click_encoding_menu()
        time.sleep(1)
        pyautogui.moveTo(292, 182)
        pyautogui.click()
        time.sleep(2)
        pyautogui.press("enter")
        time.sleep(10)

    def make_compact(self):
        self.click_encoding_menu()
        pyautogui.moveTo(248, 80)
        pyautogui.click()
        time.sleep(2)

    def generate_fonts(self, woff2_output_folderpath):
        pyautogui.hotkey("ctrl", "shift", "g")
        time.sleep(2)
        pyautogui.hotkey("ctrl", "a")
        time.sleep(1)
        pyautogui.typewrite(
            woff2_output_folderpath + \
            "materialdesignicons-webfont-min.woff2"
        )
        pyautogui.press("enter")
        time.sleep(2)

    def confirm_generate(self):
        pyautogui.moveTo(1022, 609)
        pyautogui.click()
        time.sleep(3)


if __name__ == "__main__":
    robot = IconFontMinifierRobot()

    robot.removeUnusedIcons(
        css_filepath="css/materialdesignicons.css",
        woff2_output_folderpath="C:\\Users\\shedloadofcode\\Documents\\icon-fonts-project\\fonts\\"
    )

You’ll need to install PyAutoGUI to use this script with

pip install pyautogui 

You might need to update the screen coordinates in all of the moveTo methods too if you’re using a different resolution screen to adjust where the robot clicks to be the same as in the video. PyAutoGUI is a super useful tool but I’ve found it needs adjustments when using on different devices, so consider this your chance to practice and perfect your automation skills. You can check the screen coordinates of your current mouse position with the script below which is from the docs:

get_mouse_coordinates.py
import pyautogui, sys

print('Press Ctrl-C to quit.')
try:
    while True:
        x, y = pyautogui.position()
        positionStr = 'X: ' + str(x).rjust(4) + ' Y: ' + str(y).rjust(4)
        print(positionStr, end='')
        print('\b' * len(positionStr), end='', flush=True)
except KeyboardInterrupt:
    print('\n')

I also used PyAutoGUI in another interesting project Creating a screen and mouse jiggler with Python. It really is a great lightweight automation tool.

Replace font file with minified version

Now we have the minified font file generated and saved to the font folder as 'materialdesignicons-webfont-min.woff2', we can update our stylesheet to use the minified version instead of the bloated version as seen at the end of the video.

You can see I've removed all of the other font files leaving only the .woff2 font file, and only referenced that in the stylesheet.

materialdesignicons.css
@font-face {
  font-family: "Material Design Icons";
  src: url("../fonts/materialdesignicons-webfont-min.woff2?v=6.5.95") format("woff2");
  font-weight: normal;
  font-style: normal;
  font-display: block;
}
...

If we hard refresh we can see the icons still worked as expected! Checking the network tab shows the CSS file using only the woff2 at 1.8KB and the font file at 2.8KB! This is a 99.22% reduction to 2.8KB in font file size from our starting 361KB!

Performance improvements summary

I am very pleased with the performance improvements as a result of this project. It means that every user doesn't have to download a 361KB font file just to see icons display on the page. This has led to a better user experience, better page load times and has reduced bandwidth consumption. The stats for file size reductions from this project can be seen in the table below:

TypeStarting Size KBFinal Size KBReduction %
CSS369KB1.8KB99.51%
Woff2361KB2.8KB99.24%

If you have any questions about this tutorial please leave a comment in the comments section below or feel free to reach out via the contact button at the bottom of this page 👍 I hope this has given you an insight into how you can go about self-hosting and reducing icon fonts in your own projects.

If you enjoyed this article, be sure to check out other articles on the site.