Tell browsers to preload fonts

When looking at Google’s PageSpeed Insights tool as part of the
compression work I noticed a suggestion that we preload our font files.
The tool suggests this should save about 300ms on first page load time.

***

Our font files are referenced from our CSS. This means that the browser
has to download and parse the CSS before it knows where to find the font
files. This means the requests happen in sequence.

We can make the requests happen in parallel by using a `<link>` tag with
`rel=preload`. This tells the browser to start downloading the fonts
before it’s even started downloading the CSS (the CSS will be the next
thing to start downloading, since it’s the next `<link>` element in the
head of the HTML).

Downloading fonts before things like images is important because once
the font is downloaded it causes the layout to repaint, and shift
everything around. So the page doesn’t feel stable until after the fonts
have loaded.

Google call this [cumulative layout shift](https://web.dev/cls/) which
is a score for how much the page moves around. A lower score means a
better experience (and, less importantly for us, means the page might
rank higher in search results)

We’re only preloading the WOFF2 fonts because only modern browsers
support preload, and these browsers also all support WOFF2.

We set an empty `crossorigin` attribute (which means anonymous-mode)
because the preload request needs to match the origin’s CORS mode. See
https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content#CORS-enabled_fetches
for more details.

We set `as=font` because this helps the browser use the correct content
security policy, and prioritise which requests to make first.
This commit is contained in:
Chris Hill-Scott
2020-12-29 13:38:27 +00:00
parent e5c34907c3
commit ea124f2886
5 changed files with 40 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
import os
import pathlib
import re
import urllib
from datetime import datetime, timedelta, timezone
@@ -202,6 +203,11 @@ def init_app(application):
application.before_request(load_organisation_before_request)
application.before_request(request_helper.check_proxy_header_before_request)
font_paths = [
str(item)[len(asset_fingerprinter._filesystem_path):]
for item in pathlib.Path(asset_fingerprinter._filesystem_path).glob('fonts/*.woff2')
]
@application.context_processor
def _attach_current_service():
return {'current_service': current_service}
@@ -228,7 +234,8 @@ def init_app(application):
return {
'asset_path': application.config['ASSET_PATH'],
'header_colour': application.config['HEADER_COLOUR'],
'asset_url': asset_fingerprinter.get_url
'asset_url': asset_fingerprinter.get_url,
'font_paths': font_paths,
}
application.url_map.converters['uuid'].to_python = lambda self, value: value

View File

@@ -24,7 +24,9 @@ class AssetFingerprinter(object):
self._asset_root = asset_root
self._filesystem_path = filesystem_path
def get_url(self, asset_path):
def get_url(self, asset_path, with_querystring_hash=True):
if not with_querystring_hash:
return self._asset_root + asset_path
if asset_path not in self._cache:
self._cache[asset_path] = (
self._asset_root +

View File

@@ -12,6 +12,9 @@
{% endblock %}
{% block head %}
{%- for font in font_paths %}
<link rel="preload" href="{{ asset_url(font, with_querystring_hash=False) }}" as="font" type="font/woff2" crossorigin>
{%- endfor %}
<link rel="stylesheet" media="screen" href="{{ asset_url('stylesheets/main.css') }}" />
<link rel="stylesheet" media="print" href="{{ asset_url('stylesheets/print.css') }}" />
{% block extra_stylesheets %}

View File

@@ -89,6 +89,16 @@ class TestAssetFingerprint(object):
'app/static/application.css'
)
def test_without_hash_if_requested(self, mocker):
fingerprinter = AssetFingerprinter()
assert fingerprinter.get_url(
'application.css',
with_querystring_hash=False,
) == (
'/static/application.css'
)
assert fingerprinter._cache == {}
class TestAssetFingerprintWithUnicode(object):
def test_can_read_self(self):

View File

@@ -331,3 +331,19 @@ def test_letter_spec_redirect_with_non_logged_in_user(client_request):
'/documentation/images/notify-pdf-letter-spec-v2.4.pdf'
),
)
def test_font_preload(
client_request,
mock_get_service_and_organisation_counts,
):
client_request.logout()
page = client_request.get('main.index', _test_page_title=False)
preload_tags = page.select('link[rel=preload][as=font][type="font/woff2"][crossorigin]')
assert len(preload_tags) == 4, 'Run `npm build` to copy fonts into app/static/fonts/'
for element in preload_tags:
assert element['href'].startswith('https://static.example.com/fonts/')
assert element['href'].endswith('.woff2')