DEV Community

Cover image for How to 10x downsize fonts - Building a font optimizer
Lukas Mauser
Lukas Mauser

Posted on • Updated on

How to 10x downsize fonts - Building a font optimizer

I was using fonts wrong.

I usually got most of my fonts from Google Fonts.

For a long time, I just downloaded the font files from their website and added them directly to my projects.

Not realizing, that this was a mistake...

I just blindly assumed, that what can be downloaded from Google must be the gold standard, and can be used right away.

The problem is:

The fonts that can be downloaded from Google (not talking about using the CDN here), and many other providers, are not optimized for the web. The files can be several hundred KBs large and therefore significantly increase the bundle size and loading times.

My good friend and CSS magician mrflix recently showed me 2 simple optimizations that drastically decreased the font file sizes, that I want to share with you today.

With these 2 tips, I was able to bring down a 232 KB font down to only 20 KB, that's more than a 10x improvement!

And going even further:

I'll also show you, how we can create our own font optimization script, turn it into a webapp and deploy it, so that we end up with something similar to https://fontconverter.com.

You can find all the code from this demo in this repository: font-optimizer

What I am going to cover:

  1. How to optimize fonts for the web
  2. Create your own font optimization script
  3. Turn the script into a web app
  4. Containerize the app with Docker
  5. Deploy the app with Sliplane
  6. Summary

How to optimize fonts for the web

Step 1: Convert the font to woff2 (Web Open Font Format 2.0).

What you can download from Google is usually in ttf format (True Type), an uncompressed font format used by operating systems and printers.

woff2 on the other hand is designed for the web, the font format is compressed and therefore significantly smaller. It is nowadays supported in most browsers but if you want to be extra safe, you can use woff.

There are some free online font converters out there, e.g. https://fontconverter.com, https://www.fontconverter.io, https://www.fontconverter.org, where you can upload your font and have them convert it to woff2.

But we can do even better!

Step 2: We can optimize the font even further by excluding glyphs, that we don't need.

Fonts typically contain a bunch of special characters or glyphs in foreign alphabets (e.g. greek or cyrillic), that you might not need for your project. By excluding them, we can downsize even further.

So let's start writing our own font optimizer!

Not only is it more efficient, but we also get some additional privacy and security benefits, since we don't need to share any data and we know that what's coming back is virus free.

Create your own font optimization script

There is a popular Python library called fonttools. We can use the pyftsubset command to throw out any unused glyphs and convert our ttf fonts to woff2.

I stole this little bash script from mrflix (slightly modified):

# path to .ttf file that should be converted
input_file=$1

# get rid of the extension
filename=$(echo "$input_file" | cut -f 1 -d '.')

# settings for pyftsubset
# Define a subset of unicodes and layout features to be included. Here:
# Basic Latin, Latin-1 Supplement, Double Quotation Marks, €, „, “, “, EN Dash, EM Dash, Minus, EM Space, EN Space
unicodes="U+0020-007F,U+0080-00FF,U+201E,U+201C,U+20AC,U+201E,U+201C,U+201D,U+2013,U+2014,U+2212,U+2002,U+2003"
layout_features="tnum,ss01,ss02,ss03,ss04,ss05,ss06,ss07,ss08,ss09,ss10,ss11,ss12,ss13,ss14,ss15"
flavor="woff2"

# run the command
pyftsubset ${input_file} --unicodes=${unicodes} --layout_features=${layout_features} --flavor=${flavor} --output-file=${filename}.woff2

Enter fullscreen mode Exit fullscreen mode

Note: Bash scripts are not natively supported on Windows. Check out the next section to see how you can run this with Python.

The script defines a subset of unicode characters and layout features that should be included in our optimized font and then executes the pyftsubset command.

Simply add or remove any other unicode characters that you might need or want to discard for your project. You can checkout this site, to get the unicodes.

Before we can use the script, we need to make sure Python 3.8 or later is installed on our system. Follow the official installation guide from the Python website.

Next, open we open terminal in our project and run

python3 -m venv myenv
Enter fullscreen mode Exit fullscreen mode

This creates a virtual environment named myenv, where we can install python dependencies to avoid dependency conflicts with the host. We activate the environment with

source myenv/bin/activate
Enter fullscreen mode Exit fullscreen mode

Our shell should change. All subsequent commands are now executed in this virtual environment.

You can exit the virtual environment with the deactivate command

Let's install fonttools and it's sub dependency brotliby running

pip install brotli fonttools
Enter fullscreen mode Exit fullscreen mode

Next, we create a file named font-optimizer.sh and copy the bash code from above in there.

Make the file executable by running

chmod +x font-optimzer.sh
Enter fullscreen mode Exit fullscreen mode

We can now call the optimizer by running

./font-optimizer.sh font-file.ttf
Enter fullscreen mode Exit fullscreen mode

The command should spit out an optimized font-file.woff2.

I tested it with the font Playwrite Cuba from Google fonts and compressed the light font weight down from 232 KB to 20 KB.

In comparison: By only using fontconverter.com I ended up with 77 KB.

Turn the script into a web app

To push things even further, let me show you, how we can turn this little script into a web app that we can deploy and use from anywhere.

I will keep it very minimal here, so feel free to extend this example application as you like.

Since we are dealing with Python here, let's use it for our web app, too!

The only issue is, I don't really know Python very well... 😓

Thanks to GPT, this is shouldn't be a problem.

My initial prompt looked like this:

Create a Python webapp that converts ttf files to woff2 using the subset command from fonttools

And after around 11 iterations and some manual adjustments I ended up with this app.py file:

from flask import Flask, request, jsonify, send_file
import io
from fontTools.subset import Subsetter, Options
from fontTools.ttLib import TTFont

app = Flask(__name__)

@app.route('/')
def index():
    return '''
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>TTF to WOFF2 Converter</title>
    </head>
    <body>
        <h1>TTF to WOFF2 Converter</h1>
        <form action="/convert" method="post" enctype="multipart/form-data">
            <label for="ttfFile">Upload TTF file:</label>
            <input type="file" id="ttfFile" name="ttfFile" accept=".ttf" required>
            <button type="submit">Convert</button>
        </form>
    </body>
    </html>
    '''

@app.route('/convert', methods=['POST'])
def upload_file():
    # some basic validation
    if 'ttfFile' not in request.files:
        return jsonify({'error': 'No file part'}), 400

    file = request.files['ttfFile']

    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400

    if not file.filename.endswith('.ttf'):
        return jsonify({'error': 'File is not a TTF font'}), 400

    # Read the TTF file into memory
    ttf_data = io.BytesIO(file.read())

    # Convert TTF to WOFF2 in memory
    try:
        # Load the font
        font = TTFont(ttf_data)

        # Subsetting options
        options = Options()
        options.flavor = 'woff2'
        options.layout_features = [
            'tnum', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 
            'ss10', 'ss11', 'ss12', 'ss13', 'ss14', 'ss15'
        ]
        options.unicodes = (
            list(range(0x0020, 0x007F + 1)) +
            list(range(0x0080, 0x00FF + 1)) +
            [0x201E, 0x201C, 0x20AC, 0x201E, 0x201C, 0x201D, 0x2013, 0x2014, 0x2212, 0x2002, 0x2003]
        )

        # Subsetting the font
        subsetter = Subsetter(options=options)
        subsetter.populate(unicodes=options.unicodes)
        subsetter.subset(font)

        # Save the subsetted font to WOFF2
        woff2_data = io.BytesIO()
        font.flavor = 'woff2'
        font.save(woff2_data)
        woff2_data.seek(0)

        # Send the WOFF2 file as a response
        return send_file(woff2_data, mimetype='font/woff2', as_attachment=True, download_name=file.filename.replace('.ttf', '.woff2'))

    except Exception as e:
        return jsonify({'error': 'Conversion failed', 'details': str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Enter fullscreen mode Exit fullscreen mode

Note: This is very minimal example and should only serve as a starting point for you to further develop it, make it beatiful and sprinkle in a bit of your favorite JavaScript framework 🪄

The app spins up a simple web server using the flask framework.

On GET / it serves HTML code containing a form to upload ttf font files. On submit the files are sent to the /convert endpoint. This endpoint does some basic validation first, and converts the font file in memory using the fonttools library.

Before we can give it a try, we need to also install flask by running

pip install flask
Enter fullscreen mode Exit fullscreen mode

Then execute the script with

python app.py
Enter fullscreen mode Exit fullscreen mode

The app should now be running on http://localhost:5000 🥳

Containerize the app with Docker

If you have not heard of Docker yet, Docker is a handy tool, that allows you to bundle your application together with all the depenedencies it needs. This bundle is called "Docker Image" and you can run instances of this image which are called "Containers".

Remember that you had to install Python, Flask, brotli and fonttools manually, in order to get the optimzer running?

You can put all of these installation instructions inside a Dockerfile like this:

# Start from a base image, that already has Python installed
FROM python:3.12.4-slim

# Install any additionally needed packages
RUN pip install flask brotli fonttools

# Copy our app.py file into the container
COPY app.py .

# Run the application
CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Now we only need to install Docker once, instead of every single dependendency for every single app that we ever want to run, which can lead to dependency conflicts, if e.g. one app uses Python 3 and another one Python 2... It's similar to a virtual machine, but more lightweight, since Docker images do not include the operating system.

For development purposes use Docker Desktop and follow the installation instructions on their website.

Once Docker is installed we can build the Dockerfile into a Docker image by running

docker build . -t font-optimizer
Enter fullscreen mode Exit fullscreen mode

in our app directory. The image will be tagged "font-optimizer".

To run it, use

docker run -p 5000:5000 font-optimizer
Enter fullscreen mode Exit fullscreen mode

This command will spin up a container, that runs our app. For security reasons, containers are shielded off from the host machine. Everything that happens in a container, stays in the container. To access the app inside the container, we used the -p 5000:5000 option, which tells docker to forward everything that happens inside the container on port 5000 to our host port 5000.

So we can now access the app by visiting http://localhost:5000 🥳

There are two more modification we should probably to do.

  1. First, we get a warning, that states, we are running a development server and we should use a web server like gunicorn to run the app in production mode.

  2. Second, it makes sense to pin our dependencies to specific versions, so that we don't run into compatibility issues, since running the installation commands without a pinned version, will always fetch the newest.

Let's create a requirements.txt file, and move our dependency definitions there:

flask==3.0.3 
brotli==1.1.0
fonttools==4.53.1
gunicorn==22.0.0
Enter fullscreen mode Exit fullscreen mode

Now update the Dockerfile to

# Use an official Python runtime as a parent image
FROM python:3.12.4-slim

# COPY requirements.txt into the container
COPY requirements.txt .

# Install dependencies defined in requirements.txt
RUN pip install -r requirements.txt

# Copy app.py into the container
COPY app.py .

# Run the application
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]

Enter fullscreen mode Exit fullscreen mode

Deploy the app with Sliplane

To deploy this application I will use Sliplane. Sliplane is a simple Docker hosting platform, that I co founded. All we have to do is push our code to GitHub and connect the repo to Sliplane.

To try out the app, you can create free demo servers. Just take note that demo servers are for testing purposes and will be deleted after 48 hours.

Login to GitHub and create a new repository named "font-optimizer".

Open a terminal in your project folder and run

# initialize a local repository
git init
# select the files, that you want to include in your repo (staging)
# . means everything inside your current directory
git add .
# include your staged files alongside a commit message
git commit -m "initial commit"
# add a remote branch
git remote add origin <INSERT REMOTE GITHUB PATH HERE>
# push your local code to the remote repo
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Next, login to Sliplane with your GitHub account and do the following:

  • Create a new Project named "Font Optimizer"
  • Navigate to the project and click on "Deploy Service"
  • Select the server you want to deploy to, or create a new one if needed
  • Select "Repository" as the deploy source.
  • Search for "font-optimizer" in the repositories list. > If your repository does not show up, you need to configure access to the repository first. Click on "Configure Repository Access" and select the "font-optimizer" repository. Take note, that it might take a minute for the newly connected repo to show up here. You can hit "Refresh list" to see, if the repo is available.
  • After selecting the repo, go with the default settings and hit "Deploy" at the bottom of the page

After the deployment finished, you can access your app via on your own sliplane.app domain! 🥳

Summary

Using raw ttf fonts slows down your website drastically.

Optimize your fonts by

  1. converting ttf fonts to woff2
  2. throwing out any unused glyphs

We used the Python library fonttools to create a simple optimization script and turned it into a webapp.

We then used Docker to containerize the app and Sliplane to deploy it.

You can find all the code in this repository: font-optimizer

Hope you learned something.

Share, Like, Comment, Subscribe.

Top comments (5)

Collapse
 
jangelodev profile image
João Angelo

Hi Lukas Mauser,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
wimadev profile image
Lukas Mauser

You are welcome!

Collapse
 
code42cate profile image
Jonas Scholz

What does this do?

options.layout_features = [
            'tnum', 'ss01', 'ss02', 'ss03', 'ss04', 'ss05', 'ss06', 'ss07', 'ss08', 'ss09', 
            'ss10', 'ss11', 'ss12', 'ss13', 'ss14', 'ss15'
        ]
        options.unicodes = (
            list(range(0x0020, 0x007F + 1)) +
            list(range(0x0080, 0x00FF + 1)) +
            [0x201E, 0x201C, 0x20AC, 0x201E, 0x201C, 0x201D, 0x2013, 0x2014, 0x2212, 0x2002, 0x2003]
        )
Enter fullscreen mode Exit fullscreen mode
Collapse
 
wimadev profile image
Lukas Mauser

It defines a subset of unicodes and layout features of the font, that we want to keep.
Font files not only contain the shape of single characters, but also additional layouting information like the behavior of ligatures, alignment for numbers, ... see this wikipedia article on layout features
In this case our subset includes: Basic Latin, Latin-1 Supplement, Double Quotation Marks, €, „, “, “, EN Dash, EM Dash, Minus, EM Space, EN Space unicodes

Collapse
 
code42cate profile image
Jonas Scholz

nice i feel slightly smarter now

Some comments may only be visible to logged-in visitors. Sign in to view all comments.