We use cookies to enhance your experience on the site
CodeWorlds

Font Optimization and the Font Component in Next.js 15

In Metropolis Quantum, city designers know that typography is a key element of the city's visual identity. Every sign, information display, and urban terminal interface uses carefully selected typefaces that are not only aesthetic but also optimized for rendering speed and readability on various devices. In the web world, fonts play a similar role - they are an important design element but can also significantly affect website performance.

Next.js 15 introduces a new, optimized way of managing fonts through the

next/font
component. In this chapter, we will explore this solution and best practices for font optimization in Next.js applications.

Why is Font Optimization Important?

Custom web fonts can significantly impact page performance:

  1. Slower page loading - web fonts are additional resources that must be downloaded
  2. Text flickering (FOUT - Flash of Unstyled Text) - when text initially displays with the default font and then changes to the custom one after it loads
  3. Invisible text (FOIT - Flash of Invisible Text) - when text is hidden until the font loads
  4. Impact on Core Web Vitals - especially on Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS)

The newly introduced font system in Next.js 15 solves these problems by providing:

  • Zero additional HTTP requests to external servers
  • Automatic self-hosting of font files
  • Automatic fallback system
  • No layout shifts
  • Built-in font swap hints for browsers

Komponent Font w Next.js 15

Next.js 15 provides the

next/font
module, which supports:

  • Google Fonts (
    next/font/google
    )
  • Lokalne pliki czcionek (
    next/font/local
    )

Google Fonts

Here is how to use Google Fonts in Next.js 15:

1// app/fonts.ts
2import { Inter, Roboto_Mono } from 'next/font/google';
3
4// Export variables allow reuse of the same font instances
5export const inter = Inter({
6  subsets: ['latin'],
7  display: 'swap',
8});
9
10export const roboto_mono = Roboto_Mono({
11  subsets: ['latin'],
12  display: 'swap',
13  weight: ['400', '700'],
14});

Then you can use these fonts in your layout:

1// app/layout.tsx
2import { inter } from './fonts';
3import './globals.css';
4import type { Metadata } from 'next';
5
6export const metadata: Metadata = {
7  title: 'Metropolis Quantum',
8  description: 'Official information portal of the city of the future',
9};
10
11export default function RootLayout({
12  children,
13}: {
14  children: React.ReactNode;
15}) {
16  return (
17    <html lang="pl" className={inter.className}>
18      <body>{children}</body>
19    </html>
20  );
21}

Lokalne pliki czcionek

You can also use local font files, which is recommended for custom, non-standard typefaces:

1// app/fonts.ts
2import localFont from 'next/font/local';
3
4export const quantumSans = localFont({
5  src: [
6    {
7      path: '../public/fonts/QuantumSans-Regular.woff2',
8      weight: '400',
9      style: 'normal',
10    },
11    {
12      path: '../public/fonts/QuantumSans-Bold.woff2',
13      weight: '700',
14      style: 'normal',
15    },
16    {
17      path: '../public/fonts/QuantumSans-Italic.woff2',
18      weight: '400',
19      style: 'italic',
20    },
21  ],
22  display: 'swap',
23  variable: '--font-quantum-sans',
24});

Font Configuration Options

Komponent Font w Next.js 15 oferuje szereg opcji konfiguracyjnych:

Dla Google Fonts:

1import { Roboto } from 'next/font/google';
2
3const roboto = Roboto({
4  weight: ['400', '700'],  // Selecting specific font weights
5  style: ['normal', 'italic'],  // Selecting font styles
6  subsets: ['latin'],  // Selecting character subsets
7  display: 'swap',  // Display strategy: 'auto', 'block', 'swap', 'fallback', 'optional'
8  preload: true,  // Whether to preload the font
9  fallback: ['system-ui', 'Arial'],  // Czcionki zapasowe
10  adjustFontFallback: true,  // Automatyczne dostosowywanie czcionek zapasowych
11  variable: '--font-roboto',  // CSS variable if using variable API
12});

Dla lokalnych czcionek:

1import localFont from 'next/font/local';
2
3const quantumDisplay = localFont({
4  src: [
5    {
6      path: './fonts/QuantumDisplay-Regular.woff2',
7      weight: '400',
8      style: 'normal',
9    },
10    {
11      path: './fonts/QuantumDisplay-Bold.woff2',
12      weight: '700',
13      style: 'normal',
14    },
15  ],
16  display: 'swap',
17  preload: true,
18  variable: '--font-quantum-display',
19  fallback: ['system-ui', 'Arial'],
20  adjustFontFallback: true,
21});

Using Fonts in the Application

1. Global Use for the Entire Application

1// app/layout.tsx
2import { quantumSans } from './fonts';
3import './globals.css';
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl" className={quantumSans.className}>
12      <body>{children}</body>
13    </html>
14  );
15}

2. Using CSS Variables for Greater Flexibility

1// app/fonts.ts
2import { Inter, Roboto_Mono } from 'next/font/google';
3
4export const inter = Inter({
5  subsets: ['latin'],
6  display: 'swap',
7  variable: '--font-inter',
8});
9
10export const roboto_mono = Roboto_Mono({
11  subsets: ['latin'],
12  display: 'swap',
13  weight: ['400', '700'],
14  variable: '--font-roboto-mono',
15});
1// app/layout.tsx
2import { inter, roboto_mono } from './fonts';
3import './globals.css';
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl" className={`${inter.variable} ${roboto_mono.variable}`}>
12      <body>{children}</body>
13    </html>
14  );
15}
1/* app/globals.css */
2:root {
3  --font-inter: 'Inter', system-ui, sans-serif;
4  --font-roboto-mono: 'Roboto Mono', monospace;
5}
6
7body {
8  font-family: var(--font-inter);
9}
10
11code {
12  font-family: var(--font-roboto-mono);
13}

3. Using for Specific Components

1// components/Heading.tsx
2import { quantumDisplay } from '@/app/fonts';
3
4export function Heading({ children }: { children: React.ReactNode }) {
5  return (
6    <h1 className={quantumDisplay.className}>
7      {children}
8    </h1>
9  );
10}

Zaawansowane techniki optymalizacji czcionek

1. Optimizing Subsets for Different Languages

If your application supports multiple languages, you can optimize font files by loading only the needed character subsets:

1import { Roboto } from 'next/font/google';
2
3// Dla strony po polsku
4export const robotoPolish = Roboto({
5  subsets: ['latin', 'latin-ext'],  // latin-ext contains characters used in Polish
6  weight: ['400', '700'],
7});
8
9// For a Japanese page
10export const robotoJapanese = Roboto({
11  subsets: ['latin', 'japanese'],
12  weight: ['400', '700'],
13});

2. Using Variable Fonts

Variable fonts are a modern font format that allows storing multiple styles and weights in a single file, significantly reducing the amount of data to download:

1import { Roboto_Flex } from 'next/font/google';
2
3export const roboto = Roboto_Flex({
4  subsets: ['latin'],
5  display: 'swap',
6  // Variable fonts often offer a wider range of weights
7});

3. Preload vs. Lazy loading

By default, Next.js preloads fonts, but you can disable this feature for fonts that are not used immediately:

1import { Roboto } from 'next/font/google';
2
3// Main font, preloaded
4export const robotoMain = Roboto({
5  subsets: ['latin'],
6  weight: ['400', '700'],
7  preload: true,
8});
9
10// Font used only in some components, without preloading
11export const robotoSpecial = Roboto({
12  subsets: ['latin'],
13  weight: ['900'],
14  preload: false,
15});

4. Display Strategies (font-display)

The

display
property determines how the browser should handle the font loading phase:

  • swap
    (default): Immediately shows text using the fallback font, swaps it for the custom font when it loads.
  • block
    : Briefly hides text, then shows it with the fallback font, swaps to the target after loading.
  • fallback
    : Similar to
    swap
    , but with a short period of hidden text. If loading takes too long, the fallback font is used for the session.
  • optional
    : Similar to
    fallback
    , but the browser may completely skip the custom font based on network conditions.
1import { Roboto } from 'next/font/google';
2
3// For main text, we use swap to ensure immediate readability
4export const robotoBody = Roboto({
5  subsets: ['latin'],
6  weight: ['400'],
7  display: 'swap',
8});
9
10// For decorative headings, we can use optional since they are not critical
11export const robotoHeading = Roboto({
12  subsets: ['latin'],
13  weight: ['900'],
14  display: 'optional',
15});

Implementation in Different Layouts

1. Basic Layout with Two Fonts

1// app/fonts.ts
2import { Inter, Playfair_Display } from 'next/font/google';
3
4export const inter = Inter({
5  subsets: ['latin'],
6  display: 'swap',
7  variable: '--font-sans',
8});
9
10export const playfair = Playfair_Display({
11  subsets: ['latin'],
12  display: 'swap',
13  variable: '--font-serif',
14});
1// app/layout.tsx
2import { inter, playfair } from './fonts';
3import './globals.css';
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl" className={`${inter.variable} ${playfair.variable}`}>
12      <body>
13        <header className={playfair.className}>
14          <h1>Metropolis Quantum</h1>
15        </header>
16        <main className={inter.className}>
17          {children}
18        </main>
19      </body>
20    </html>
21  );
22}
1/* app/globals.css */
2:root {
3  --font-sans: 'Inter', system-ui, sans-serif;
4  --font-serif: 'Playfair Display', Georgia, serif;
5}
6
7h1, h2, h3, h4, h5, h6 {
8  font-family: var(--font-serif);
9}
10
11body {
12  font-family: var(--font-sans);
13}

2. Dashboard with Monospace Font for Data

1// app/dashboard/layout.tsx
2import { roboto_mono } from '../fonts';
3
4export default function DashboardLayout({
5  children,
6}: {
7  children: React.ReactNode;
8}) {
9  return (
10    <div className="dashboard-layout">
11      <div className="sidebar">
12        {/* Sidebar content */}
13      </div>
14      <div className={`content ${roboto_mono.className}`}>
15        {children}
16      </div>
17    </div>
18  );
19}

3. Typography Component System

1// components/Typography.tsx
2import { ReactNode } from 'react';
3import { inter, playfair, roboto_mono } from '@/app/fonts';
4
5type FontVariant = 'sans' | 'serif' | 'mono';
6
7interface TypographyProps {
8  children: ReactNode;
9  variant?: FontVariant;
10  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div';
11  className?: string;
12}
13
14export function Typography({
15  children,
16  variant = 'sans',
17  as: Component = 'p',
18  className = '',
19}: TypographyProps) {
20  let fontClass = '';
21  
22  switch (variant) {
23    case 'sans':
24      fontClass = inter.className;
25      break;
26    case 'serif':
27      fontClass = playfair.className;
28      break;
29    case 'mono':
30      fontClass = roboto_mono.className;
31      break;
32  }
33  
34  return (
35    <Component className={`${fontClass} ${className}`}>
36      {children}
37    </Component>
38  );
39}

Example usage:

1// app/page.tsx
2import { Typography } from '@/components/Typography';
3
4export default function HomePage() {
5  return (
6    <div>
7      <Typography as="h1" variant="serif" className="text-4xl mb-4">
8        Witaj w Metropolis Quantum
9      </Typography>
10      
11      <Typography variant="sans" className="mb-6">
12        The city of the future where technology meets everyday life.
13      </Typography>
14      
15      <div className="code-example">
16        <Typography variant="mono" className="text-sm">
17          console.log("Hello, Quantum World!");
18        </Typography>
19      </div>
20    </div>
21  );
22}

Impact on Core Web Vitals

Proper font implementation has a direct impact on key performance metrics:

Largest Contentful Paint (LCP)

Custom fonts can delay the rendering of the largest content element. The Font component in Next.js 15 minimizes this problem by:

  • Automatic self-hosting of fonts
  • Preloading critical fonts
  • Display strategies enabling immediate text rendering

Cumulative Layout Shift (CLS)

Automatic calculation of fallback font sizes minimizes layout shifts when the custom font is finally loaded.

First Input Delay (FID) i Interaction to Next Paint (INP)

Fonts do not have a direct impact on these metrics, but efficient font loading can reduce the load on the browser's main thread.

Analyzing Font Performance

1. Lighthouse

Use the Lighthouse tool to analyze font performance:

  1. Open Chrome Developer Tools (F12)
  2. Go to the "Lighthouse" tab
  3. Zaznacz "Performance" i uruchom test
  4. Check the "Opportunities" and "Diagnostics" sections for potential font issues

2. Network panel w DevTools

Analyze font loading in the Network tab:

  1. Filtruj po "Font" lub "Woff2"
  2. Check download time, file sizes, and loading priority

3. WebPageTest

The WebPageTest tool (webpagetest.org) offers detailed analysis of resource loading, including fonts, on different devices and internet connections.

Best Practices

  1. Use the Font component from Next.js 15 instead of importing fonts directly from Google Fonts or other external sources
  2. Limit the number of fonts used to 2-3 families (e.g., one for headings, one for text, possibly one monospace)
  3. Choose only the needed weights and styles - each additional weight means extra data to download
  4. Use subsets specific to your application's languages
  5. Consider variable fonts for families with many weights
  6. Choose the appropriate display strategy (
    display
    ) depending on the font's purpose
  7. Use CSS variables for greater flexibility in styling
  8. Test performance on different devices and connections

Comprehensive Implementation Example

Below is an example of a comprehensive font system implementation in a Next.js 15 application:

1// app/fonts.ts
2import { Inter, Playfair_Display, Roboto_Mono } from 'next/font/google';
3import localFont from 'next/font/local';
4
5// Main font for text
6export const inter = Inter({
7  subsets: ['latin', 'latin-ext'],
8  display: 'swap',
9  variable: '--font-sans',
10  weight: ['400', '500', '700'],
11});
12
13// Font for headings
14export const playfair = Playfair_Display({
15  subsets: ['latin', 'latin-ext'],
16  display: 'optional',
17  variable: '--font-serif',
18  weight: ['700', '900'],
19});
20
21// Czcionka dla kodu
22export const roboto_mono = Roboto_Mono({
23  subsets: ['latin'],
24  display: 'swap',
25  variable: '--font-mono',
26  weight: ['400', '700'],
27});
28
29// Custom, non-standard font (logo and highlighted elements)
30export const quantumDisplay = localFont({
31  src: [
32    {
33      path: '../public/fonts/QuantumDisplay-Regular.woff2',
34      weight: '400',
35      style: 'normal',
36    },
37    {
38      path: '../public/fonts/QuantumDisplay-Bold.woff2',
39      weight: '700',
40      style: 'normal',
41    },
42  ],
43  display: 'swap',
44  variable: '--font-quantum',
45});
1// app/layout.tsx
2import { inter, playfair, roboto_mono, quantumDisplay } from './fonts';
3import './globals.css';
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl" className={`
12      ${inter.variable} 
13      ${playfair.variable} 
14      ${roboto_mono.variable} 
15      ${quantumDisplay.variable}
16    `}>
17      <body className={inter.className}>{children}</body>
18    </html>
19  );
20}
1/* app/globals.css */
2:root {
3  --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
4  --font-serif: 'Playfair Display', Georgia, Cambria, 'Times New Roman', Times, serif;
5  --font-mono: 'Roboto Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
6  --font-quantum: 'Quantum Display', var(--font-sans);
7  
8  /* Font sizes */
9  --fs-xs: 0.75rem;
10  --fs-sm: 0.875rem;
11  --fs-base: 1rem;
12  --fs-lg: 1.125rem;
13  --fs-xl: 1.25rem;
14  --fs-2xl: 1.5rem;
15  --fs-3xl: 1.875rem;
16  --fs-4xl: 2.25rem;
17  --fs-5xl: 3rem;
18  
19  /* Line-heights */
20  --lh-tight: 1.25;
21  --lh-snug: 1.375;
22  --lh-normal: 1.5;
23  --lh-relaxed: 1.625;
24  --lh-loose: 2;
25}
26
27body {
28  font-family: var(--font-sans);
29  font-size: var(--fs-base);
30  line-height: var(--lh-normal);
31}
32
33h1, h2, h3, h4, h5, h6 {
34  font-family: var(--font-serif);
35  line-height: var(--lh-tight);
36  font-weight: 700;
37}
38
39h1 {
40  font-size: var(--fs-4xl);
41  margin-bottom: 1.5rem;
42}
43
44h2 {
45  font-size: var(--fs-3xl);
46  margin-bottom: 1.25rem;
47}
48
49.logo {
50  font-family: var(--font-quantum);
51  font-weight: 700;
52}
53
54code, pre {
55  font-family: var(--font-mono);
56  font-size: var(--fs-sm);
57}
58
59.title-quantum {
60  font-family: var(--font-quantum);
61  font-weight: 700;
62  letter-spacing: -0.025em;
63}

Common Problems and Their Solutions

Problem 1: FOUT (Flash of Unstyled Text)

Solution: Use

display: 'swap'
and make sure fonts are preloaded.

Problem 2: Large Font File Sizes

Solution:

  • Limit the number of weights and styles
  • Use only necessary subsets
  • Consider variable fonts
  • Optymalizuj pliki WOFF2 (Next.js robi to automatycznie)

Problem 3: Fonts Delay Page Loading

Solution:

  • For less critical fonts, use
    display: 'optional'
  • For fonts used only in specific components, disable preloading

Problem 4: Differences in Appearance Between Fallback and Custom Fonts

Solution: Next.js 15 automatically matches fallback font metrics to target fonts, minimizing shifts. You can also use

adjustFontFallback: true
.

Browser Compatibility

The Font component in Next.js 15 automatically delivers fonts in WOFF2 format, which is supported by all modern browsers. For older browsers that do not support WOFF2, fallback fonts defined in the configuration are displayed.

Support for Languages with Non-Standard Writing Systems

When working with multilingual applications, remember:

  1. Appropriate subsets for each language
  2. Larger file sizes for languages with extensive character systems (e.g., Chinese, Japanese)
  3. Rendering performance - some complex writing systems can burden the browser
1// Example for a multilingual application
2import { Noto_Sans_JP, Noto_Sans } from 'next/font/google';
3
4// For Latin languages (e.g., English, Polish)
5export const notoSansLatin = Noto_Sans({
6  subsets: ['latin', 'latin-ext'],
7  weight: ['400', '700'],
8  variable: '--font-latin',
9});
10
11// For Japanese language
12export const notoSansJP = Noto_Sans_JP({
13  subsets: ['japanese'],
14  weight: ['400', '700'],
15  variable: '--font-japanese',
16});

Summary

The Font component in Next.js 15 provides a comprehensive solution for managing and optimizing fonts in web applications:

  1. Automatic self-hosting - eliminates external HTTP requests
  2. Optimized WOFF2 format - smaller files to download
  3. Layout shift prevention - better CLS
  4. Display strategies - control over font loading behavior
  5. Preload and lazy loading - optimal use of network bandwidth

Just like in Metropolis Quantum, where every aspect of visual communication is carefully designed and optimized, a well-implemented font system in Next.js 15 ensures the perfect balance between aesthetics and performance. Thanks to the Font component, Next.js applications can offer a consistent, beautiful, and efficient typographic experience without compromising performance.

In the next chapter, we will discuss managing static assets in the

public
directory, which will allow us to better understand how to efficiently organize and serve different types of resources in Next.js 15 applications.

Go to CodeWorlds