angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Using icons on your Flask website and reducing 'First Contentful Paint'

29 May 2020 Updated 29 May 2020 by Peter

Use a Jinja icon macro to put icons on your pages and use <symbol> to define icons once and use multiple times on the same page.

post main image
https://unsplash.com/@codingtim

There are many types of vector icons. In this post I only look at SVG-icons, and limit myself to navigation icons, sometimes also called interface icons. These icons not only look nice on websites, they also color and scale like fonts. And they are very functional. Imagine a button with the text ' Edit' in it. Replace this text with a pencil-icon and you get more space on the page while it still is very clear what will happen when you click the button.

Another example is the contact page. Instead of listing a phone number, an email address and a visit address as text only, you can put icons in front of them and it becomes easier to read and use.

Probably the most well known icons are the search icon and the login/account (person) icon. On your mobile phone screen there is not as much space as on a desktop computer. In many cases the menu bar changes on a phone and the horizontal menu items are placed in a dropdown menu that can be activated by clicking a hamburger-icon. To keep the search and login / account functions always accessible, often the search box is replaced by a search-icon and the login / account texts are replaced by a person-icon.

Ways to add icons to your website

The most easy way is to add icons as a font. You download the font (we do not use a CDN for privacy reasons), add the font family and you are done. When I started this website I choose Font Awesome. Why? I searched the internet for bootstrap icons and there were many hits mentioning Font Awesome. Font Awesome really has a very nice set of free icons. To add them to your pages is easy. For example to add a user-icon you add the code:

	<i class="fas fa-user"></i>

Another way to add icons is inline. Instead of adding code to select an icon from the Font Awesome font, we add the actual SVG-icon code. You can download the SVG-icon code from the Font Awesome website, the code for the user-icon:

	<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user" class="svg-inline--fa fa-user fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"></path></svg>

There are more ways but with most of them you loose the ability to color and scale. A nice overview is presented in the article 'Using SVG', see links below.

Icons and page load time

When you build a website one of the most important aspects is the time it takes for a page to appear on the screen. Many search engines have made the loading time of your website a ranking factor. Chromium Developer Tools has a nice tool to measure this time called 'Audits'. This is the Lighthouse program that gives you ratings for Performance, Accessibility, Best Practices and SEO. For performance, 'First Contentful Paint' is very important.

Lighthouse showed me that loading and rendering the Font Awesome files was responsible for almost one second! If you check the Font Awesome website you see that there are some 1500 free icons in the font. Very nice but I only use 25 of them.

Because I wanted to reduce the loading time I considered using a custom icon font. For example, Icomoon, see links below, allows you to create an icon font with only the icons you use. Very nice, but I decided to go for inline icons because this eliminates loading extra files. The downside is that the page size increases. A nice discussion about performance aspects can be found in the article 'Inline SVG vs Icon Fonts [CAGEMATCH]', see links below.

Use a Jinja macro to put icons on the page

Thinking about icons, it probably is a good idea to keep in mind that you want to change the icons tomorrow. Or even want to maintain multiple icon sets. As a test I selected the new Bootstrap 5 icons as a second icon set. To support both icon sets I created the following Jinja macro:

{%- macro icon(icon_name, fill=false, collection=none, inline=false) -%}

	{%- if not collection %}
		{%- set collection = 'fontawesome' -%}
	{%- endif %}

	{%- if collection == 'fontawesome' -%}

		{%- if icon_name in ['angle_up', 'chevron_up'] -%}
				
			{%- if inline -%}
				<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="angle-up" class="svg-inline--fa fa-angle-up fa-w-10" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M177 159.7l136 136c9.4 9.4 9.4 24.6 0 33.9l-22.6 22.6c-9.4 9.4-24.6 9.4-33.9 0L160 255.9l-96.4 96.4c-9.4 9.4-24.6 9.4-33.9 0L7 329.7c-9.4-9.4-9.4-24.6 0-33.9l136-136c9.4-9.5 24.6-9.5 34-.1z"></path></svg>
			{%- else -%}
				<i class="fas fa-angle-up"></i>
			{%- endif -%}

		{%- elif icon_name in ['arrow_clockwise', 'redo'] -%}
		...

	{%- endif -%}


	{%- if collection == 'bootstrap5' -%}

		{%- if icon_name in ['angle_up', 'chevron_up'] -%}

			{%- if inline -%}
				<svg class="bi bi-chevron-up" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/></svg>			{%- else -%}
			{%- else -%}

			{%- endif -%}

		{%- elif icon_name in ['arrow_clockwise', 'redo'] -%}
		...

	{%- endif -%}

{%- endmacro -%}

I can use this macro both with Font Awesome and Bootstrap 5 icons. In addition, I can specify if I want the fill type icon, and if I want the SVG-icon inline. By modifying the macro I can also output both the Font Awesome icon and the Bootstrap 5 icon for visual inspection. Using this macro is very easy, in your page you can use:

	{{ icon('home') }} Hoogerheid, NL

Preventing identical inline SVG-icons

A quick test showed me that inline SVG icons indeed reduced 'First Contentful Paint' by almost a second. Great! But sometimes an icon was on a page multiple times. With an average size of 400 - 600 characters per SVG-icon this can have a serious impact on page loading time. An example is a comments section where every comment has a calendar-icon and a @-icon and there are hundred comments.

Fortunately there is a way to define a block of icons on a page and use them later. We do this using the <svg> and <symbol> tags:

<svg class="d-none">
	<symbol id="icon-angle-up" viewBox="0 0 16 16">
		<title>angle-up</title>
		<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
	</symbol>
	<symbol id="icon-arrow-clockwise" viewBox="0 0 16 16">
		<title>arrow-clockwise</title>
		<path fill-rule="evenodd" d="M3.17 6.706a5 5 0 0 1 7.103-3.16.5.5 0 1 0 .454-.892A6 6 0 1 0 13.455 5.5a.5.5 0 0 0-.91.417 5 5 0 1 1-9.375.789z"/><path fill-rule="evenodd" d="M8.147.146a.5.5 0 0 1 .707 0l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 1 1-.707-.708L10.293 3 8.147.854a.5.5 0 0 1 0-.708z"/>
	</symbol>
	...
</svg>

More information in the article 'SVG 'symbol' a Good Choice for Icons', see links below. We put this block immediately below the <body> tag. Note that we only put the paths of an icon in the definition. The title is optional. To use the icon, we only have to specify its id:

	<svg class="svg-icon"><use xlink:href="#icon-calendar"></use>

We can also wrap it in a <span> with a class e.g. to change its size.

Implementation

There are two things that need editing when adding a new icon:

  • the symbol icon set
  • the icon macro

In addition, I want HTML-code that I can put on a page for a quick visual inspection. Very soon I got tired of editing and decided to write a script that can be used to generate this data. I also decided to go for the Bootstrap 5 icons only, I liked them more, they are MIT licensed and all have the same viewBox. The script was written very fast so do not shoot me:

import sys

class SVGIcon():

    def __init__(self,
        icon_collection=None,
        icon_name=None,
        icon_id=None,
        svg_class=None,
        svg_fill=None,
        svg_view_box=None,
        svg_path=None,
        symbol_title=None
        ):

        self.icon_collection = icon_collection
        self.icon_name = icon_name
        self.icon_id = icon_id
        self.svg_class = svg_class
        self.svg_fill = svg_fill
        self.svg_view_box = svg_view_box
        self.svg_path = svg_path
        self.symbol_title = symbol_title


def doit():
    
    svg_icons = []

    icon_name2icon_details = {
        'angle_up': {
            'refs': ['angle_up', 'chevron_up'],
            'svg_path': '<path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>',
        },
        'arrow_clockwise': {
            'refs': ['arrow_clockwise', 'redo', 'resend_mail'],
            'svg_path': '<path fill-rule="evenodd" d="M3.17 6.706a5 5 0 0 1 7.103-3.16.5.5 0 1 0 .454-.892A6 6 0 1 0 13.455 5.5a.5.5 0 0 0-.91.417 5 5 0 1 1-9.375.789z"/><path fill-rule="evenodd" d="M8.147.146a.5.5 0 0 1 .707 0l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 1 1-.707-.708L10.293 3 8.147.854a.5.5 0 0 1 0-.708z"/>',
        },
        ...
    }

    icon_collection = 'bootstrap5'
    svg_view_box = "0 0 16 16"
    svg_fill = "currentColor"

    for icon_name in icon_name2icon_details:
        icon_details = icon_name2icon_details[icon_name]
        svg_path = icon_details['svg_path']

        svg_icons.append(SVGIcon(
            icon_collection = icon_collection,
            icon_name = icon_name,
            icon_id = 'icon-' + icon_name.replace('_', '-'),
            svg_fill = svg_fill,
            svg_view_box = svg_view_box,
            svg_path = svg_path,
            symbol_title = icon_name.replace('_', '-'),
        ))

    # generate svg symbols list
    symbols_list_lines = []
    symbols_list_lines.append( '<svg class="d-none">' )

    for svg_icon in svg_icons:
        symbols_list_lines.append( "\t" + '<symbol id="' + svg_icon.icon_id + '" viewBox="' + svg_icon.svg_view_box + '">' )
        symbols_list_lines.append( "\t\t" + '<title>' + svg_icon.symbol_title + '</title>' )
        symbols_list_lines.append( "\t\t" + svg_icon.svg_path )
        symbols_list_lines.append( "\t" + '</symbol>' )

    symbols_list_lines.append( '</svg>' )
    symbols_list = "\n".join(symbols_list_lines)

    # generate svg symbols preview
    preview_lines = []
    for svg_icon in svg_icons:
        preview_lines.append( 'abc' + ' ' + '<span class="font-size: 1em;"><svg class="svg-icon">' + '<use xlink:href="#' + svg_icon.icon_id + '"></use></svg>' + '</span>' + ' ' + svg_icon.icon_name )

    preview_list = '<p>' + "</p>\n<p>".join(preview_lines) + '</p>'

    # generate icon macro code
    icon_macro_lines = []
    first = True        
    for icon_name in icon_name2icon_details:
        icon_details = icon_name2icon_details[icon_name]
        if 'refs' not in icon_details:
            print('refs missing for {}'.format(icon_name))
            sys.exit()
        refs = icon_details['refs']
        # get icon
        found = False
        for svg_icon in svg_icons:
            if svg_icon.icon_name == icon_name:
                found = True
                break
        if not found:
            print('cannot find icon, icon_name = {}'.format(icon_name))
            sys.exit()
                
        if_start = 'elif'
        if first:
            if_start = 'if'
            first = False

        # expand refs
        ref_items = []
        for ref in refs:
            ref_items.append( "'" + ref + "'" )
            ref_list = '[' + ', '.join(ref_items) + ']'

        icon_macro_lines.append( "\t" + '{%- ' + if_start + ' icon_name in ' + ref_list + ' -%}' )
        icon_macro_lines.append( '<span class="font-size: 1em;"><svg class="svg-icon">' + '<use xlink:href="#' + svg_icon.icon_id + '"></use></svg>' + '</span>' )

    icon_macro_lines.append( "\t" + '{%- endif -%}' )
    icon_macro_list = "\n".join(icon_macro_lines)

    print('symbols_list:')
    print('{}'.format(symbols_list))
    print('preview_list:')
    print('{}'.format(preview_list))
    print('icon_macro_list:')
    print('{}'.format(icon_macro_list))

Afterwards I just copy-paste the parts symbols_list, preview_list and icon_macro_list into the base template, the preview file, and the macro file.

Some problems

In my list tables, columns that can be sorted had the sort-icon attached to the last word of the column name. When a column gets smaller I want the icon to stick with the last word. This no longer works. I have not found a solution for this yet but did not look into this in detail.

Another thing is the vertical positioning of icons. There are solutions for this, one is described in 'Align SVG Icons to Text and Say Goodbye to Font Icons', see links below. After starting using the Bootstrap 5 icons as described above it appeared that it was not necessary to do any positioning, but you may want to look into it.

Summary

Using SVG-icons really makes a website look and navigate better. Because I use only 25 icons I decided to use the SVG-symbol method to include the icons on the pages which adds an extra 11.5 KB. Compared to the Font Awesome solution the 'First Contentful Paint' went down almost one second! And using a Jinja macro icon() gives me the flexibility to change the icons in one place instead of having to update all pages. At the moment all icons are loaded in every page. In future I can optimize this by including only the icons used on a page.

Links / credits

Align SVG Icons to Text and Say Goodbye to Font Icons
https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4

Blog Optimization: Replacing Font Awesome with SVG
https://www.wouterbulten.nl/blog/tech/blog-optimization-replacing-font-awesome-with-svg/

Flask | JINJA 2: render_template_string() with macro imported in context
https://stackoverflow.com/questions/61338841/flask-jinja-2-render-template-string-with-macro-imported-in-context

Icomoon
https://icomoon.io

Inline SVG vs Icon Fonts [CAGEMATCH]
https://css-tricks.com/icon-fonts-vs-svg/

Insert image after each list item
https://stackoverflow.com/questions/946403/insert-image-after-each-list-item

Is there a way to use SVG as content in a pseudo element :before or :after
https://stackoverflow.com/questions/19255296/is-there-a-way-to-use-svg-as-content-in-a-pseudo-element-before-or-after

Lighthouse | Tools for Web Developers | Google Developers
https://developers.google.com/web/tools/lighthouse

SVG 'symbol' a Good Choice for Icons
https://css-tricks.com/svg-symbol-good-choice-icons/

Using SVG
https://css-tricks.com/using-svg/

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.