Skip to content

Commit 4e294f9

Browse files
authored
Merge pull request #3 from codeAdrian/feature/options
Code improvement, added timeout & interval options, docs update
2 parents ed62c31 + febd5d4 commit 4e294f9

File tree

3 files changed

+64
-24
lines changed

3 files changed

+64
-24
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Performant asynchronous font loading plugin for Gatsby.
66
* Preloads the files & preconnects to the URL
77
* Loads fonts asynchronously to avoid render blocking
88
* Implemented with [fast loading snippets](https://csswizardry.com/2020/05/the-fastest-google-fonts/)
9-
* Loading status listener for avoiding FOUT & FOUC
9+
* Loading status listener for avoiding FOUT
1010
* Small size & minimal footprint
1111

1212
## Install
@@ -29,12 +29,18 @@ Add the following snippet to `gatsby-config.js` plugins array.
2929
/* Plugin options */
3030
options: {
3131

32-
/* Enable font loading listener to handle FOUC */
32+
/* Enable font loading listener to handle FOUT */
3333
enableListener: true,
3434

3535
/* Preconnect URL-s. This example is for Google Fonts */
3636
preconnect: ["https://fonts.gstatic.com"],
3737

38+
/* Font listener interval (in ms). Default is 300ms. Recommended: >=300ms */
39+
interval: 300,
40+
41+
/* Font listener timeout value (in ms). Default is 30s (30000ms). Listener will no longer check for loaded fonts after timeout, fonts will still be loaded and displayed, but without handling FOUT. */
42+
timeout: 30000,
43+
3844
/* Self-hosted fonts config. Add font files and font CSS files to "static" folder */
3945
custom: [
4046
{
@@ -57,9 +63,9 @@ Add the following snippet to `gatsby-config.js` plugins array.
5763
}
5864
```
5965

60-
## Handling FOUC with Font loading listener
66+
## Handling FOUT with Font loading listener
6167

62-
When loading fonts asynchronously, Flash Of Unstyled Content (FOUC) might happen because fonts load few moments later after page is displayed to the user.
68+
When loading fonts asynchronously, Flash Of Unstyled Text (FOUT) might happen because fonts load few moments later after page is displayed to the user.
6369

6470
To avoid this, we can use CSS to style the fallback font to closely match the font size, line height and letter spacing of the main font that is being loaded.
6571

@@ -75,7 +81,7 @@ Here is the example of how `body` element will look like after all fonts are bei
7581
<body class="wf-lazy-monday--loaded wf-font-awesome-5-brands--loaded wf-font-awesome-5-free--loaded wf-staatliches--loaded wf-henny-penny--loaded">
7682
```
7783

78-
<img alt="FOUC example" src="https://res.cloudinary.com/dazdt97d3/image/upload/v1604140006/fouc.gif">
84+
<img alt="FOUT example" src="https://res.cloudinary.com/dazdt97d3/image/upload/v1604140006/fouc.gif">
7985

8086

8187
## Issues and Contributions

components/FontListener.tsx

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
1-
import React, { useEffect, useState } from "react"
1+
import React, { useEffect, useMemo, useRef, useState } from "react"
22
import { Helmet } from "react-helmet"
33

44
declare var document: { fonts: any }
55

6-
export const FontListener: React.FC<{ fontNames: string[] }> = ({
6+
interface Props {
7+
fontNames: string[]
8+
interval?: number
9+
timeout?: number
10+
}
11+
12+
export const FontListener: React.FC<Props> = ({
713
fontNames,
14+
interval = 300,
15+
timeout = 30000,
816
}) => {
917
const [hasLoaded, setHasLoaded] = useState<Boolean>(false)
1018
const [loadedFonts, setLoadedFonts] = useState<string[]>([])
1119
const [intervalId, setIntervalId] = useState<number>(-1)
20+
const attempts = useRef<number>(Math.floor(timeout / interval))
21+
22+
const pendingFonts = useMemo(
23+
() => fontNames.filter(fontName => !loadedFonts.includes(fontName)),
24+
[loadedFonts, fontNames]
25+
)
26+
27+
const loadedClassname = useMemo(getLoadedFontClassNames, [loadedFonts])
1228

1329
const apiAvailable = "fonts" in document
1430

1531
useEffect(() => {
1632
if (!apiAvailable) {
17-
setHasLoaded(true)
18-
setLoadedFonts(fontNames)
33+
handleApiError("Font loading API not available")
1934
return
2035
}
2136

22-
if (!hasLoaded && intervalId < 0) {
23-
const id = window.setInterval(isFontLoaded, 100)
37+
if (apiAvailable && !hasLoaded && intervalId < 0) {
38+
const id = window.setInterval(isFontLoaded, interval)
2439
setIntervalId(id)
2540
}
2641
}, [hasLoaded, intervalId, apiAvailable])
@@ -31,13 +46,9 @@ export const FontListener: React.FC<{ fontNames: string[] }> = ({
3146
}
3247
}, [hasLoaded, intervalId])
3348

34-
const loadedClassname = Boolean(loadedFonts.length)
35-
? loadedFonts.map(fontName => `wf-${kebabCase(fontName)}--loaded`).join(" ")
36-
: ""
37-
3849
return (
3950
<Helmet>
40-
{Boolean(loadedFonts.length) && <body className={loadedClassname} />}
51+
<body className={loadedClassname} />
4152
</Helmet>
4253
)
4354

@@ -49,18 +60,39 @@ export const FontListener: React.FC<{ fontNames: string[] }> = ({
4960
.join("-")
5061
}
5162

63+
function getLoadedFontClassNames() {
64+
return Boolean(loadedFonts.length)
65+
? loadedFonts
66+
.map(fontName => `wf-${kebabCase(fontName)}--loaded`)
67+
.join(" ")
68+
: ""
69+
}
70+
71+
function errorFallback() {
72+
setHasLoaded(true)
73+
setLoadedFonts(fontNames)
74+
}
75+
76+
function handleApiError(error) {
77+
console.info(`document.fonts API error: ${error}`)
78+
console.info(`Replacing fonts instantly. FOUT handling failed due.`)
79+
errorFallback()
80+
}
81+
5282
function isFontLoaded() {
5383
const loaded = []
84+
attempts.current = attempts.current - 1
85+
86+
if (attempts.current < 0) {
87+
handleApiError("Interval timeout reached, maybe due to slow connection.")
88+
}
5489

55-
const fontsLoading = fontNames.map(fontName => {
90+
const fontsLoading = pendingFonts.map(fontName => {
5691
let hasLoaded = false
5792
try {
5893
hasLoaded = document.fonts.check(`12px '${fontName}'`)
5994
} catch (error) {
60-
console.info(`document.fonts API error: ${error}`)
61-
console.info(`Replacing fonts instantly. FOUT handling failed.`)
62-
setHasLoaded(true)
63-
setLoadedFonts(fontNames)
95+
handleApiError(error)
6496
return
6597
}
6698

@@ -70,7 +102,7 @@ export const FontListener: React.FC<{ fontNames: string[] }> = ({
70102

71103
const allFontsLoaded = fontsLoading.every(font => font)
72104

73-
if (loadedFonts.length !== loaded.length) {
105+
if (Boolean(loaded.length)) {
74106
setLoadedFonts(loaded)
75107
}
76108

gatsby-browser.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ import { getFontFiles, getFontNames } from "./utils"
44

55
export const wrapRootElement = (
66
{ element },
7-
{ custom = [], web = [], enableListener }
7+
{ custom = [], web = [], enableListener, interval, timeout }
88
) => {
99
const allFonts = [...custom, ...web]
1010
const fontFiles = getFontFiles(allFonts)
1111
const fontNames = getFontNames(allFonts)
1212

13+
const listenerProps = { fontNames, interval, timeout }
14+
1315
const hasFontFiles = Boolean(fontFiles.length)
1416
const hasFontNames = Boolean(fontNames.length)
1517

1618
return (
1719
<>
1820
{hasFontNames && <AsyncFonts hrefs={fontFiles} />}
19-
{enableListener && hasFontFiles && <FontListener fontNames={fontNames} />}
21+
{enableListener && hasFontFiles && <FontListener {...listenerProps} />}
2022
{element}
2123
</>
2224
)

0 commit comments

Comments
 (0)