Degraded Performance Caused by Next.js Webfont Optimization: A Case Study
Tags: nextjs, performance, webdev
Published: 2023-10-05 15:09:24

Photo by <a href="https://unsplash.com/@chollz?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Charlie M</a> on <a href="https://unsplash.com/photos/CHAFV-0U7b8?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
  
---
## TL;DR

- A website built with Next.js experienced performance issues under certain conditions.
- The cause of the problem was a significant expansion of HTML size due to the Automatic Webfont Optimization feature.
- As a solution, we utilized `next/font` to optimize the method of loading Webfonts and reduced the HTML size.

## Specific Circumstances of the Project

- Deploying and test-running the Next.js application on ECS
- Providing multilingual support (Japanese, Korean, Chinese (Traditional, Simplified))
- Originally started building with Next.js v12, and just last month, finally completed the update to v13

## The Problem That Occurred

Our team had an opportunity to measure the performance of a particular website built with Next.js. Surprisingly, even though the number of requests was not so high, we noticed IPC (Inter-Process Communication) related errors occurring on the server side.

{% details (Part of the occurring error) %}

```
TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11457:11)
    at async invokeRequest (/home/snaka/xxxxx/node_modules/next/dist/server/lib/server-ipc/invoke-request.js:17:12)
    at async invokeRender (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:254:29)
    at async handleRequest (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:447:24)
    at async requestHandler (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:464:13)
    at async Server.<anonymous> (/home/snaka/xxxxx/node_modules/next/dist/server/lib/start-server.js:117:13) {
  cause: Error: read ECONNRESET
      at TCP.onStreamRead (node:internal/stream_base_commons:217:20) {
    errno: -104,
    code: 'ECONNRESET',
    syscall: 'read'
  }
}
```
Another error also occurred:
```
cause: SocketError: other side closed
at Socket.onSocketEnd (/app/node_modules/next/dist/compiled/undici/index.js:1:63301)
at Socket.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1359:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'UND_ERR_SOCKET',
  socket: {
    localAddress: '127.0.0.1',
    localPort: 45850,
    remoteAddress: undefined,
    remotePort: undefined,
    remoteFamily: undefined,
    timeout: undefined,
    bytesWritten: 2937,
    bytesRead: 87753
  }
}
```

{% enddetails %}

Upon further investigation, it became clear that the HTML built by Next.js was abnormally large. Specifically, its size was about 2MB, which was significantly larger compared to the size of a typical webpage's HTML.

## The Cause of the Problem

To resolve the performance issue occurring on our website, we began a series of investigations to pinpoint the cause. During load testing, it was revealed that network bandwidth was a clear bottleneck, and we were particularly concerned about the significant bandwidth consumption when downloading a single HTML file.

Further investigation into the HTML revealed that it contained a large number of font definitions (`@font-face` rules). These definitions were not present in the original source code and were automatically inserted by some mechanism. Further research revealed that these font definitions originated from the Automatic Webfont Optimization feature introduced in Next.js v10.2.

Reference: [Next.js 10.2 Release Notes](https://nextjs.org/blog/next-10-2#automatic-webfont-optimization)

The problematic section was the following description in _document.tsx.

```tsx
// _document.tsx
<Head>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap" />
</Head>
```

Sample code is available here:

- https://github.com/snaka/code-pocket/tree/master/framework/nextjs/webfont/legacy-optimization

Size of the built HTML (about 2MB)

```text
-rw-r--r--@ 1 snaka  staff  2359534 Sep 29 09:48 ja.html
-rw-r--r--@ 1 snaka  staff  2359533 Sep 29 09:48 ko.html
-rw-r--r--@ 1 snaka  staff  2359540 Sep 29 09:48 zh-CN.html
-rw-r--r--@ 1 snaka  staff  2359540 Sep 29 09:48 zh-TW.html
```

## Solution

The solution was simple. We stopped loading Webfonts in `_document.tsx` and switched to using `next/font`, introduced in Next.js v13.

- [Optimizing: Fonts | Next.js](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts)

This change significantly reduced the size of the HTML and resolved the IPC error when loading the server. Additionally, by using `next/font`, it became possible to self-host Webfonts and control caching periods, etc., through the CDN.

The code for using Webfonts became as follows:

```tsx
// index.tsx
import { Noto_Sans_JP, Noto_Sans_KR, Noto_Sans_SC, Noto_Sans_TC } from 'next/font/google'

const notoSansJP = Noto_Sans_JP({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansSC = Noto_Sans_SC({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansTC = Noto_Sans_TC({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansKR = Noto_Sans_KR({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
// Use the loaded webfont as className={notoSans_JP.className}, etc.
```

Sample code is available here:

- https://github.com/snaka/code-pocket/tree/master/framework/nextjs/webfont/next-font

Size of the built HTML (about 5KB)

```text
-rw-r--r--@ 1 snaka  staff   5176 Sep 29 14:19 ja.html
-rw-r--r--@ 1 snaka  staff   5175 Sep 29 14:19 ko.html
-rw-r--r--@ 1 snaka  staff   5182 Sep 29 14:19 zh-CN.html
-rw-r--r--@ 1 snaka  staff   5182 Sep 29 14:19 zh-TW.html
```

## In Choosing the Approach

In this case, we adopted a method of changing the Webfont loading method and using `next/font` without preload. This decision was made as a result of evaluating the following technical trade-offs.

- **Problems Before Improvement**
  - Increased communication caused instability in inter-process communication within the container
  - The allocated CPU and memory performance for the container could not be fully utilized, necessitating the parallel operation of numerous containers
- **Negative Impact from Changing to `next/font`**
  - Using `next/font` without preload → A slight time lag occurred until the Webfont was applied
- **Consideration of Alternatives**
  - Change the deployment destination to a FaaS platform? → Judged as high-risk
  - Personally, it was very interesting but...
- **Final Decision**
  - Utilizing `next/font` provided benefits of resolving communication bottlenecks and effectively utilizing container resources
  - Compared cost-performance and a slight UX degradation, and prioritized cost-performance
  - By loading divided files, we anticipate achieving service stability efficiently by distributing server load using CDNs.

Through this choice, while the client-side overhead increased due to the additional files to be loaded, the final performance evaluation (Lighthouse) showed a slight improvement. (Approximately 13% enhancement)


## In Conclusion

In this article, we presented an example of a performance issue and its solution that occurred on a website built with Next.js. We confirmed that the Automatic Webfont Optimization feature could cause a significant expansion in HTML size in certain cases, which, in turn, could potentially trigger IPC errors on the server side.

As a solution, by utilizing `next/font`, we optimized the method of loading Webfonts, reduced the HTML size, and were able to alleviate the bottleneck on the server. This also enabled us to self-host Webfonts and control caching.

The speed of evolution in front-end technology is staggering, with new technologies and optimization methods being provided daily. While they can enhance the performance of a website when used appropriately, they are not omnipotent and can cause unexpected problems in some cases. Therefore, thorough testing is indispensable when introducing new features.

We hope this article proves helpful to someone.

*This article was proofread by ChatGPT.*

## Pages Referred to

- [Optimizing: Fonts | Next.js](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts)
- [Next.js 10.2 | Next.js](https://nextjs.org/blog/next-10-2#automatic-webfont-optimization)