Tree Shaking is the process of stripping out parts of libraries that you’re not actually using in your applications in order to optimize the file size of your build. In this short tutorial I’m going to apply it to CSS and a WOFF2 file, but you can use the same technique for other types of files as well; e.g.: JS files.
Standardization
There’s no standard way of implementing tree shaking, however, there are a few tools that can be setup to handle this for you automatically, like WebPack, which I’m sure that you’ve at least heard of, if not actually used it to compile ECMA Script 2015 and above in your projects.
Current Situation
I was at work, trying to optimize the main application to fix some problems identified with the help if web.dev/measure (Lighthouse) tool, made available by Google itself.
One of the bigger problems, that was causing us to get a low score on Performance, was the fact that we’re loading the entire CSS file throughout the website, even though we’re using small bits of it on certain pages. In order to solve this, I’m in the process of modularizing the entire SCSS codebase in order to be able to compile a specific CSS file for each main page of the site. So for the search page, I’m compiling a separate main CSS file, for the profile area another one, and so on.
However, while I was doing this, I noticed that removing the FontAwesome file was saving us 20KB of bandwidth per request, so it looked like something I could work on. So, I decided to try to remove the icons that we weren’t using, because this would save us a bit of bandwidth, and, when you’re in the process of optimizing for performance in Google, every KB counts.
Tree Shaking CSS
Gathering the Icons that are Actually Used
Unfortunately, one can’t simply create something that will work for every project, so I wasn’t expecting to find something already implemented that would fix this for me, so I had to get creative.
I started working on a GREP command that would return all of the usages from the main files of the project. We’re using Phalcon, so this basically means scanning the app/
dir. In our case, we have some Vue components inside, public/js/
, hence, we’re going to scan that location too.
The final command looks like this:
grep -Ro 'fal fa-[a-zA-Z0-9_-]\+' app/ public/js/ | uniq
I’m scanning for the fal fa-*
format because I’m obsessive enough to know that every single use case is going to look like that, as I’m always spending time to correct white space in my code. And I’m doing that specifically because I want to be able to search for things later.
If you’re not used to correcting your white space and fal fa-*
is the same thing as fal fa-*
for you, you might need to come up with a more complex regex. Don’t know regex? I’ve got you covered.
That command is going to return a list of files and the match inside that file, and then it’s going to be run through the uniq
*nix tool to make the results unique, just in case the same icon is found in the same file multiple times.
This is an example of the output:
app//modules/frontend/views/updates/index.volt:fal fa-moon app//modules/frontend/views/updates/index.volt:fal fa-times-circle public/js//components/finder.vue:fal fa-location-arrow public/js//components/finder.vue:fal fa-3x fa-map-marker-alt public/js//components/finder.vue:fal fa-exclamation-circle
Gathering the Used Part of the Source File
Next, I’m taking the output of that command, run to PHP, create a new CLI task and handle it. Handling it means extracting all of the icon names and generating a hash table array from it.
I’m basically generating something like this:
[ 'moon' => true, 'times' => true, 'location-arrow' => true, '3x' => true, 'map-marker-alt' => true, 'exclamation-circle' => true, ]
Notice that the algorithm doesn’t ignore the 3x
entry, which isn’t really an icon, but a special sizing class that increases the size of the icon 3 times. I’m not bothered by it, because, since there’s no icon for it, it’s not going to associate it to anything.
This is how the actual code looks, if you need to use it for inspiration. Keep in mind that this won’t work for everyone’s specific scenario, so you might need to adapt it to your needs.
$results = trim(shell_exec("cd " . escapeshellarg(BASE_PATH) . " && grep -Ro 'fal fa-[a-zA-Z0-9_-]\+' app/ public/js/ | uniq")); $results = explode("\n", $results); $icons = array_map(function ($value) { return @end(explode(':fal ', $value)); }, $results); $iconsScss = $uniqueIcons = []; $destFile = '/tmp/fontawesome-icons.scss'; $icons = array_merge(['fa-sign-out', 'fa-trash-alt'], $icons); foreach ($icons as $icon) { foreach (explode(' ', $icon) as $fa) { $uniqueIcons[substr($fa, 3)] = true; } }
Not that I have the hash table ($uniqueIcons
) with all of the icons (about 90 icons in my case,) I’m going to parse the source SCSS FontAwesome file and only pick the lines that match my icons.
This is how it looks in code:
$faSourceFile = file_get_contents(BASE_PATH . '/node_modules/@fortawesome/fontawesome-pro/scss/_icons.scss'); $faSourceFile = explode("\n", trim($faSourceFile)); foreach ($faSourceFile as $line) { if (substr($line, 0, 4) != '.#{$') continue; $icon = @current(explode(':', substr($line, 19))); if (isset($uniqueIcons[$icon])) { $iconsScss[] = $line; } }
The FA source file looks like this:
... .#{$fa-css-prefix}-yin-yang:before { content: fa-content($fa-var-yin-yang); } .#{$fa-css-prefix}-yoast:before { content: fa-content($fa-var-yoast); } .#{$fa-css-prefix}-youtube:before { content: fa-content($fa-var-youtube); } .#{$fa-css-prefix}-youtube-square:before { content: fa-content($fa-var-youtube-square); } .#{$fa-css-prefix}-zhihu:before { content: fa-content($fa-var-zhihu); }
That’s the reason I’m ignoring every line that doesn’t start with .#{$
.
For every line, I’m extracting the icon name and checking if it exists in the hash table. If it exists, then it means that I’m interested in it and I’m going to save the SCSS line.
At the end, I’m writing all the saved lines to a file to be compiled later into CSS.
This leaves me with a 90 lines of SCSS file, as opposed to the 2165 source file. The difference is very high, so you can begin to see why I’ve spent time on this.
After compilation, before the tree shaking, we were having a GZIPPED file of 45.1KB, and now we’re down 15KB to 30.1KB. That’s a 36% optimization, which is excellent by any standard. Now, all I had to do was make sure to run the new CLI task before compiling all of the SCSS assets.
Tree Shaking the Font File
As you may already know, one way of using sharp icons on the Web, is through custom web fonts. Fonts themselves are compressed mathematical representations of letters and symbols and are not constricted to a specific width or height.
If we were to use image icons, whenever a user zoomed the page, they’d look pixelated and loose their appealing real quick. However, with fonts, because maths magic, we can scale them to whatever size we want and they won’t pixelate like images.
That’s all great, but font files are still files, and if you end up creating thousands of characters or symbols, as is the case for FontAwesome font files, you will inevitably increase their size, too. In our case, the Light version of FontAwesome Pro was about 142KB. That’s… a lot, especially when you’re trying to optimize for Performance.
Therefore, I started looking for ways to edit font files programmatically, so that I can apply the same tree shaking principle to the font file too. I wasn’t really expecting much of a difference, because I didn’t really understand how they worked or were structured, but I knew it would decrease the size somewhat, so I decided that I could spend one or two hours on this.
Learning to Edit a WOFF2 Font File
Because this is the main font file used by the browser, I decided to start with it.
I started by looking up libraries on packagist.org, hoping to find something to edit them directly in PHP, as I had my CLI task written in it. However, I couldn’t find anything related to it there, so I moved on to npmjs.com.
On NPM I was able find a tool called fonteditor-core. I was pretty impressed with it as it seemed to be just what I was looking for. The documentation isn’t that great, but after a few trial-and-error type attempts at reading a WOFF2 files and writing it to a different one, I found a working formula.
Writing the Script
const Font = require('fonteditor-core').Font; const woff2 = require('fonteditor-core').woff2; const fs = require('fs'); const fontPath = process.argv[2]; const savePath = process.argv[3]; const subset = fs.readFileSync(process.argv[4]).toString().split(','); const fonts = subset.map(name => parseInt(name, 16)); woff2.init().then(() => { const font = Font.create(fs.readFileSync(fontPath), { type: 'woff2', hinting: true, subset: fonts, }); fs.writeFileSync(savePath, font.write({ type: 'woff2', hinting: true, })); });
The only thing complicated here was converting the subsets from their encoded unicode representation to their numeric value. That’s why I’m remapping all the subsets using the following conversion code:
const subset = fs.readFileSync(process.argv[4]).toString().split(','); const fonts = subset.map(name => parseInt(name, 16));
This bit:
const font = Font.create(fs.readFileSync(fontPath), { type: 'woff2', hinting: true, subset: fonts, });
Reads from the source font file only the subsets that I’m feeding it. So, basically, I’m reading the file and keeping only the icons that I’m interested in.
To run it, it’s as simple as:
node scripts/woff2-tree-shaker.js ../path/to/font.woff2 ../path/to/destination.woff2 ../path/to/subsets
The first path is to the source file of the font itself. The second path is where you want it to be saved after it’s been tree shaken. And the third path is for where you’ve saved your icons that are being used currently. I’m using my previously written CLI task to generate a list of identifiers for the icons in use.
The identifiers are the unicode representation of the icon inside the font file. This is the value that the content:
property uses to know what icon to represent from the font. For example, for the fa-search icon, the unicode representation would be: \f002
.
Now, because I’m using the SCSS version of the FontAwesome project, these are not as easily accessible, as they’re stored in a separate “variables” file.
So, as we’ve seen in my previous code, one icon is represented by a line similar to:
.#{$fa-css-prefix}-zhihu:before { content: fa-content($fa-var-zhihu); }
I’ve highlighted in bold the name if the icon and the name if the corresponding variable. As you can see, they’re deterministic, and since I already have the list of icons, all I have to do is scan the variables file and find the value for each variable.
The “variables” file looks something like this:
$fa-var-yin-yang: \f6ad; $fa-var-yoast: \f2b1; $fa-var-youtube: \f167; $fa-var-youtube-square: \f431; $fa-var-zhihu: \f63f;
Putting it all Together
All I had to do is scan that file line by line, extract the name of the icon, look if it exists in my hash table from before, and if it does, store the value somewhere. I used the same hash table to store the value, I just replaced the true
value with it, for every icon.
Here’s the code:
$variables = file_get_contents(BASE_PATH . '/node_modules/@fortawesome/fontawesome-pro/scss/_variables.scss'); $variables = explode("\n", trim($variables)); $woff2Path = BASE_PATH . '/node_modules/@fortawesome/fontawesome-pro/webfonts/fa-light-300.woff2'; $toPath = BASE_PATH . '/public/fonts/vendor/@fortawesome/fontawesome-pro/webfa-light-300.woff2'; $subsetPath = '/tmp/font-shaker.subset'; foreach ($variables as $variable) { if (strpos($variable, '$fa-var-') !== 0) continue; list($icon, $value) = explode(': \\', rtrim(substr($variable, 8), ';')); if (isset($uniqueIcons[$icon])) { $uniqueIcons[$icon] = $value; } } // Save the subset to be used by the font tree shaker. file_put_contents($subsetPath, implode(',', array_values($uniqueIcons))); // Tree shake the font. shell_exec("cd " . escapeshellarg(BASE_PATH) . " && node scripts/woff2-tree-shaker.js " . escapeshellarg($woff2Path) . ' ' . escapeshellarg($toPath) . ' ' . escapeshellarg($subsetPath));
That’s it. Now, every time I compile the assets, the task runs just before and tree shakes both the CSS and font file to get rid of everything that’s not needed.
By the way, the resulting font file is 12KB. Down from 142KB, that’s a 96% improvement. 🙂
Let me know if you have any questions about this and I’ll try to answer them in the comments below.
2 Replies to “Implementing Tree Shaking for FontAwesome”
Just wondering why you choose not to use FontAwesome SVG Javascript Core? https://fontawesome.com/how-to-use/on-the-web/advanced/svg-javascript-core
It allows you to subset the icons as well.
Hi! The reason is that the site is too complex at this point and it would simply take more time to replace everything with SVGs. That would’ve been my approach if I had to start from scratch.