Web Page Font Flickering Issues

· 2 min read

Phenomenon

Analysis

When a web page uses custom fonts, during DOM parsing and rendering, fonts haven’t finished loading yet, so the browser immediately displays content using fallback fonts. Once font loading completes, it re-renders and replaces the text with custom fonts.

Solutions

Now that we’ve identified the problem, let’s work on optimization. Improvement approaches include the following aspects:

CSS Extraction

Currently, web styles are bundled into JS and dynamically inserted into the header section each time, which is slower. Additionally, JS changes actually cause fingerprint (content hash) changes, affecting styles and preventing caching, so extraction is beneficial for performance improvement.

Webpack loader and plugins need the following configuration:


 {
          test: /\.less$/,
          use: [
            {
              loader: MiniCssExtractPlugin.loader
            }]
            }
new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      filename: 'static/css/[name].[hash].css',
      chunkFilename: 'static/css/[name].[hash].css'
    })

Font Preloading

Notes

  1. Preloading prevents fonts from only starting to load during CSS parsing, improving font resource loading priority
  2. Considering the current web development targets browsers like Chrome, IE11+, Safari, etc., I only load woff2 and woff. Note that in terms of file size: woff2 < woff < ttf
  3. Since woff2 is not supported in IE, woff configuration is needed

Configuration


<link rel="preload" as="font" type="font/woff2" href="static/font/Lato-Regular.woff2">
<link rel="preload" as="font" type="font/woff2" href="static/font/Lato-Bold.woff2">
@font-face {
  font-family: 'Lato-Regular';
  src: url('../static/fonts/Lato-Regular.woff2') format('woff2'), url('../static/fonts/Lato-Regular.woff') format('woff');
  font-display: auto;
}

@font-face {
  font-family: 'Lato-Bold';
  src: url('../static/fonts/Lato-Bold.woff2') format('woff2'), url('../static/fonts/Lato-Bold.woff') format('woff');
  font-display: auto;
}

Note that if font loading is cross-origin, the Link tag needs to be configured with crossorigin, such as crossorigin=“anonymous”. Otherwise, even if preloading succeeds, fonts won’t actually be applied for security reasons, causing fonts to load twice due to both preloading and CSS font-face declarations.

Font File Size

The above preloading can separate CSS parsing from font resource loading, but large font files are also problematic. For this reason, configure as above to prioritize downloading woff2, with woff only for IE, and ttf not considered.

Regarding file size differences between font formats, for example lato-regular:

  • woff-309KB
  • woff2-183KB
  • ttf-608KB

Additionally, there are 2 more approaches to further reduce font file size:

  • Base64 encode font files, which merges fonts and styles, reducing requests. I didn’t adopt this approach, but it’s an option
  • Use subset embedding, since not all character encodings included in font files are actually used

Font Display Strategy

If font file loading takes an acceptable time within 3 seconds, when font loading isn’t complete, the browser can display content with fallback fonts or hide it first. After 3 seconds, if the font file download is complete, display text with custom fonts; if not, use fallback fonts and swap when font file download completes.

Font switching creates a flickering effect, so I changed the display strategy to auto, which follows the default strategy - if fonts haven’t loaded within 3 seconds, text remains invisible.

Using swap would immediately display with default fonts while font loads, creating a visible font change effect, which was the original problem.

However, swap isn’t necessarily bad - which strategy to use depends on the situation.

Auto and block ensure no font change issues occur, but longer text invisibility degrades user experience, so continuous loading experience optimization is needed, such as CDN usage.

Web performance is actually an important topic, with terminology such as:

  • FOUC (Flash of Unstyled Content)

    During web page rendering, when external styles haven’t loaded yet, content is briefly displayed with browser default styles, then returns to normal once external styles load - this page flickering process

    This problem is completely avoidable in today’s web if resource loading order is handled properly.

Browser Loading Order for HTML, CSS, JS

The direct cause of the initial problem was just needing to adjust font display strategy while improving font download speed, but behind this lies browser resource loading workflow processing, so let me outline this here.

Using an actual SPA index page as an example, the browser loading to parsing and rendering process is as follows:

<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
    <base href="/" />
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta http-equiv="Cache-Control" content="no-cache" />
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <title>loading</title>
    <meta name="description" content="Description for quotation">
    <meta name="google" value="notranslate">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="preload" as="font" href="static/font/Lato-Regular.ttf">
    <link rel="preload" as="font" href="static/font/Lato-Bold.ttf">
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="manifest" href="manifest.webapp" />
    <link rel="stylesheet" href="static/css/antd.min.css">
    <style type="text/css">
        .browser-upgrade {
            margin: 0.2em 0;
            background: #ccc;
            color: #000;
            padding: 0.2em 0;
        }
    </style>
    <link href="static/css/vendors.2a1bd1c1e210b6a1697e.css" rel="stylesheet">
    <link href="static/css/main.2a1bd1c1e210b6a1697e.css" rel="stylesheet">
</head>
<body>
<div id="root">
</div>

<script type="text/javascript" src="app/vendors.2a1bd1c1e210b6a1697e.chunk.js"></script>
<script type="text/javascript" src="app/main.2a1bd1c1e210b6a1697e.bundle.js"></script>

<script src="static/js/stomp.min.js" defer></script>
<script src="static/js/sockjs-1.0.0.min.js" defer></script>
</body>
</html>

Please point out any errors

Additional Notes

Browsers have two ways to handle CSS blocking rendering: either wait for CSS parsing to complete before rendering together (Chrome does this, but causes white screen), or render unstyled DOM first then render again once CSS parsing is complete (Firefox does this, but causes unstyled content flashing).

Final Thoughts

Browser-related knowledge points are quite fragmented and complex… Let’s work together on this.

Reference Documents