# Improving Our Next.js Rendering Strategy

> That's not how you're supposed to use Next.js

- Published: 2025-08-08
- Source: https://yujiseok.blog/en/posts/nextjs-rendering-optimization

In this post, I take a close look at the inefficiencies and problems caused by Next.js route segment options in our service,
walk through how I fixed them, and write down what I learned along the way.

> ✅ This document is written for **Next.js v14.2.29**.
> Our deployment environment is GCP.

## Server Rendering Methods and Route Segment Options

Before getting into the problems, we first need to understand Next.js's rendering methods and route segment options.

### Server Rendering Methods

Next.js is a server-side framework for React, and by default it renders every page on the server.
This server-side rendering comes in 3 flavors, depending on the strategy.

1. **SSR** (Server Side Rendering)
   - Called Dynamic Rendering in Next.js
   - This strategy generates the HTML page at request time and returns it
   - Suited for pages that need personalized content or dynamic data

2. **SSG** (Static Site Generation)
   - Called Static Rendering in Next.js
     - Without any dynamic APIs or special configuration, every page is statically rendered by default
   - This strategy generates the HTML ahead of time at build time and returns it

3. **ISR** (Incremental Static Regeneration)
   - Built on top of SSG -> it gets a lifecycle, so pages are regenerated on a schedule
   - In Next.js, it can be implemented in a time-based or request-based way
     - Time-based: the cache is automatically invalidated and the page regenerated at a set time interval
     - On-demand: the cache is manually invalidated and the page regenerated by a specific request (tag / path)

> Comparing each rendering strategy to a bakery makes it much easier to understand. 🥐
>
> SSR is like a personal baker: the moment a customer places an order, they start kneading the dough. The bread is always fresh out of the oven, but the customer has to wait.
>
> SSG is like a big bakery: all the bread is baked in the morning and put on display. Customers can buy right away, but if they show up late, the bread might not be fresh.
>
> ISR is a clever neighborhood bakery that combines the best of both. It bakes in the morning so it can serve customers right away (the SSG way). Then, following a "1-hour rule," when a customer asks for stale bread, it hands over that bread for now and immediately starts baking a fresh batch for the next customer.

There's also a strategy called **PPR** (Partial Prerendering). It prepares the unchanging parts (the static shell) ahead of time so they show up instantly, and renders the dynamic parts that need real-time data through streaming.

> As of now, this feature is [experimental](https://nextjs.org/docs/14/app/api-reference/next-config-js/partial-prerendering). 🧪

Now that we've covered rendering methods, let's look at route segment options.

### Route Segment Options

Route segment options let you configure how pages, layouts, and route handlers behave.
You can set them by directly `export`ing them, like this:

```tsx
export const dynamic = "auto";
export const dynamicParams = true;
export const revalidate = false;
export const fetchCache = "auto";
export const runtime = "nodejs";
export const preferredRegion = "auto";
export const maxDuration = 5;

export default function MyComponent() {}
```

There are 7 options in total, but the ones worth paying close attention to are `dynamic` and `revalidate`.
These two have a very direct effect on rendering and are the key factors that decide the rendering strategy.

Let's see how each option works.

1. **dynamic**

```tsx
export const dynamic = "auto" | "force-dynamic" | "error" | "force-static";
```

The dynamic option determines how a layout or page behaves.
It can take 4 values, and the behavior differs depending on the value.

- auto (default)
  - Next.js generates the page statically as much as possible
  - If the page contains dynamic APIs (`cookies`, `headers`, etc.) -> it switches to SSR
  - Or if it finds a `fetch` request that doesn't use the cache -> it switches to SSR

- force-dynamic
  - Forces dynamic rendering
  - Renders anew on the server for every request
  - Behaves the same as `getServerSideProps` in the `pages` directory

- error
  - Forces static rendering
  - Throws an error if the page uses dynamic APIs or finds uncached data

- force-static
  - Forces static rendering
  - `cookies`, `headers`, `useSearchParams`, and the like return empty values

2. **revalidate**

```tsx
export const revalidate = false | 0 | number;
```

The revalidate option sets the default cache revalidation time (in seconds) for a layout or page, controlling ISR or dynamic rendering.

- false (default)
  - Caches the generated page indefinitely (same as Infinity)
  - Behaves statically (SSG) by default, but switches to dynamic rendering (SSR) when it hits a dynamic API or an uncached fetch

- 0
  - Forces the page to render dynamically,
  - and changes fetch's default behavior to not cache (no-store)
  - If a fetch has a more specific cache option (force-cache or a positive revalidate value), that setting is kept

- number (in seconds)
  - Enables ISR
  - Serves the cached page for the set time (in seconds), and once that time has passed, the next request triggers a background regeneration of the page

> Even if you set revalidate to a number (e.g. 3600) to make it behave as ISR, the page switches to dynamic rendering (SSR) as soon as it hits a dynamic API. Dynamic APIs always take priority over static settings.
>
> Also, the lowest-value-wins rule applies: the smallest value among those set on the layout, the page, and the `fetch` options is the one that takes effect.

To sum up the two options: `dynamic` sets how the page behaves, and `revalidate` sets the revalidation time.

Now let's get into the problems.

## The Problem

Next.js works a bit of black magic 🔮 to make pages static by default, but in our actual service every single page was being generated dynamically.

It all started with the question, <strong>"Why is every page being generated dynamically at build time?"</strong>,
and that's how I began digging into the problem.

### 1. Every Page Generated Dynamically

Next.js is supposed to have a philosophy of generating things as statically as possible, yet the pages in our service
were all being generated dynamically at build time, as shown below.

<div className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/before-build.png"
    width={1200}
    height={1000}
    alt="Pages generated dynamically"
  />
  <small>Dynamic page generation at build time</small>
</div>

> ƒ means dynamic generation, and ○ means static generation.
>
> It felt like getting an F grade, so I wasn't exactly thrilled..🎓

Honestly, having every page generated dynamically isn't something you'd call a problem from a purely functional point of view.
But when every page is generated dynamically, we can't take advantage of Next.js's Full Route Cache, and it just gets ignored.
As a result, instead of reusing already-built output, the server has to generate the HTML and data (RSC Payload) from scratch on every user request.
That ends up causing inefficiencies like server overload and higher costs, page flicker and slow page transitions, and a worse user experience from lost client state.

There was another problem with this dynamic generation: all caching was being ignored.

### Dynamic Generation Is Forced, and All Caching Is Ignored

Even if pages are generated dynamically, if the data needed to render them is cached, the performance hit of dynamic rendering can be softened.
But if the cache for all data is ignored, the database becomes a bottleneck under load and drags down the performance of the whole service.

<div id="cache-skipped" className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/before-cache.png"
    width={1200}
    height={1000}
    alt="Pages generated dynamically"
  />
  <small>Cache skipped</small>
</div>

Looking at the log above, we can learn two things.

1. Every API request is skipping the cache.
2. The reason for skipping the cache is clear.
   - `Cache skipped reason: (revalidate: 0)`

Thanks to this hard evidence, we were able to track down the culprit that had been stealing our service's performance. 🦹
Narrowing the dragnet based on the evidence, the culprit was easy to spot: the code in the `layout.tsx` file defined at the very top of the app was the mastermind.

### A Setting in the Root Layout That Forces Dynamic Rendering

To find the culprit, I looked at the root layout and found code like this:

```tsx
export const dynamic = "force-dynamic";
export const revalidate = 300;

// ...
```

When I first saw this code, I was baffled.
Why would someone set `force-dynamic`, which generates every page dynamically, together with `revalidate`, which revalidates the page every 300 seconds? I couldn't figure it out.

> These two options conflict with each other. It's a bit like saying "I drank, but I didn't drink and drive"..😵‍💫

Because of the force-dynamic setting on the root layout, under Next.js's priority rules, **dynamic rendering was being forced and the data cache for every page was being ignored**.

Now that we've found the problem, at last..! Let's look at how to fix it.

## The Fix

### Removing the Segment Option That Forces Dynamic Rendering in the Root Layout

The fix is very simple. Just remove the segment option applied to the root layout without hesitation.

```tsx {1}
export const dynamic = "force-dynamic"; //  제거
export const revalidate = 300;
```

Once you remove the option that forces dynamic rendering, Next.js renders the places that use dynamic APIs dynamically, and renders the rest of the pages statically.

Was the problem solved, then? Not quite..
As soon as I removed `export const dynamic = 'force-dynamic'`, a different problem showed up.

### The API Server Isn't Running During the Build

To check whether the improved code was actually working, I cheerfully ran the build command, and got the following error:

```bash
{
  code: 500,
  message: 'INTERNAL SERVER ERROR',
  cause: 'UNKNOWN ERROR',
  digest: '3141274559'
}
TypeError: fetch failed
    at node:internal/deps/undici/undici:15422:13
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
  [cause]: AggregateError [ECONNREFUSED]:
      at internalConnectMultiple (node:net:1134:18)
      at afterConnectMultiple (node:net:1715:7) {
    code: 'ECONNREFUSED',
    [errors]: [ [Error], [Error] ]
  }
}
```

I'd never run into this error before, so I was pretty thrown.
But the error message was so helpful that I could figure out the problem easily.

You can sum this error up in one line: <strong>"The API server isn't running during the build, so how are you planning to make API requests?"</strong>

Breaking the error down step by step, it looks like this:

1. `code: 'ECONNREFUSED'`
   - ECONNREFUSED stands for **Connection Refused**
   - It's not an HTTP error, but one that occurs at a lower level, the operating system's network level
   - It went to a specific port, but there was no process at that address at all -> basically getting the door slammed in its face..💧

2. `[cause]: AggregateError [ECONNREFUSED]:`
   - This is the 'ECONNREFUSED' from the operating system brought into the JavaScript world
   - It's an error raised by Node.js's built-in `net` module
   - It's Node.js reporting the error situation

3. `TypeError: fetch failed`
   - An error that occurs at the application level
   - It's raised by the `undici` module -> the actual implementation behind the `fetch` function
   - It's thrown because it concluded that communication wasn't possible

So which code in the service was spewing out this error like fire?
Just like when I caught force-dynamic earlier, I went through the process of hunting down the culprit and finally found a suspect.

At first, I thought the `rewrites` option was the culprit.
But the real culprit turned out to be the URL used when calling the `fetch` function.

### The rewrites Option

So let's look at the rewrites option that had me confused.

```js
async rewrites() {
  return [
    {
      // (1) 출발지 (Source)
      source: '/api/:path*',

      // (2) 목적지 (Destination)
      destination: {
        development: 'http://dev.api.kr/:path*',
        staging: 'http://staging.api.kr/:path*',
        qa: 'http://qa.api.kr/:path*',
        production: 'https://api.kr/:path*',
      }[ENV], // (3) 환경 변수(ENV)에 따라 목적지 선택
    },
  ];
},
```

This setting internally proxies every request that starts with `/api/` from the frontend to the real API server address for the current runtime environment (ENV), such as development, staging, or production.
If you look at the log in the attached [image](#cache-skipped), you can see that requests coming in on the `/api/` path are being handled according to the rewrites setting.

### The Real Cause of the Build Error

The reason the error occurred was that, during the build, the API was being called at a non-existent `http://localhost:`.
In other words, it was a problem caused by the difference between the development environment and the build environment.

In the development environment, the web server is running via the `next dev` command, so the localhost address is valid. But `next build` is just the process of converting code into static files, and it doesn't actually start a web server.
So I was calling the API with no server there to respond, and the connection got refused.

```ts title="fetch 함수 호출 코드"
const apiServer = `http://localhost:${process.env.USE_DYNAMIC_PORTS ?? 8080}`;

fetch(`${apiServer}/api/...`);
```

To generate pages at build time, Next.js needs to make API requests for the data those pages depend on.
At that point, it tried `fetch` using the `apiServer` variable in the code above, sent the request to an address that doesn't exist, and triggered the ECONNREFUSED error.

To fix this, I clearly defined the API address for each environment and changed the code to use the API address that matches the actual environment at build time.

```ts title="환경별 API 주소 정의"
// ...
const apiBaseUrl = {
  development: "http://dev.api.kr",
  staging: "http://staging.api.kr",
  qa: "http://qa.api.kr",
  production: "https://api.kr",
};

const apiServer = apiBaseUrl[environment];
```

Now everything's resolved. Let's check that it was really applied properly.

<div className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/after-build.png"
    width={1200}
    height={1000}
    alt="Pages generated statically"
  />
  <small>Static page generation at build time</small>
</div>

Ta-da.. the static pages are finally being generated.

Through this process, I felt that the rewrites option was causing unnecessary confusion not just for me but for my teammates too, and that we didn't need the proxy setup anyway, so after making a decision, I removed it.

> CORS is already handled on the server, and the API address for each environment is clearly defined, so I judged that calling the real API address directly from the code was more intuitive than keeping a proxy setup.

## So Why Did the force-dynamic Option Exist?

My teammates and I put our heads together to figure out why that option was in the code, and we came up with two likely reasons.

1.  Technical debt from not thinking things through when adopting the Next.js 13 App Router
    - At the time of adoption, a lack of understanding of the newly introduced mental model
    - Overlooking the fact that it generates static pages by default

2.  An attempt to work around the API call error at build time
    - In the development environment, calling localhost works fine, but in the build environment the local server isn't running, so calling localhost is impossible

My conclusion was that we hit a technical problem, the build error from reason 2, and solved it in the way described in reason 1, without fully understanding the framework.

Now let's look at the actual effects of the improvement.

## Results

The results were more dramatic than I expected. From real production data collected on GCP Cloud Run, I could clearly see how positively this optimization affected server load, response time, cost efficiency, and more.

> 💡 All analysis is based on quantitative logs collected from our real production environment, starting from the deployment on May 14, 2025.

Let's start with the visual data. The graphs below clearly show the key metrics stabilizing dramatically around the time of the deployment.

<div className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/cpu.png"
    width={1200}
    height={1000}
    alt="Container CPU utilization"
  />
  <small>Container CPU utilization</small>
</div>

<div className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/request.png"
    width={1200}
    height={1000}
    alt="Request count"
  />
  <small>Request count</small>
</div>

<div className="flex flex-col items-center">
  <Image
    src="/images/nextjs-rendering-optimization/traffic.png"
    width={1200}
    height={1000}
    alt="Received bytes"
  />
  <small>Received bytes</small>
</div>

The quantitative analysis of these changes is in the table below. Let's walk through how each metric improved and what it means technically.

<table>
  <tbody>
    <tr>
      <th>
        <p>
          <strong>Metric</strong>
        </p>
      </th>
      <th>
        <p>
          <strong>Before</strong>
        </p>
      </th>
      <th>
        <p>
          <strong>After</strong>
        </p>
      </th>
      <th>
        <p>
          <strong>Improvement</strong>
        </p>
      </th>
      <th>
        <p>
          <strong>Technical Meaning</strong>
        </p>
      </th>
    </tr>
    <tr>
      <td>
        <p>
          <strong>Requests</strong>
        </p>
      </td>
      <td>
        <p>~80 req/s</p>
      </td>
      <td>
        <p>~20 req/s</p>
      </td>
      <td>
        <p>
          <strong>75% ▼</strong>
        </p>
      </td>
      <td>
        <p>A fundamental reduction in origin server load</p>
      </td>
    </tr>
    <tr>
      <td>
        <p>
          <strong>Request latency (p95)</strong>
        </p>
      </td>
      <td>
        <p>~800 ms</p>
      </td>
      <td>
        <p>~200 ms</p>
      </td>
      <td>
        <p>
          <strong>75% ▼</strong>
        </p>
      </td>
      <td>
        <p>Big gains in perceived speed and Core Web Vitals scores</p>
      </td>
    </tr>
    <tr>
      <td>
        <p>
          <strong>Container CPU utilization</strong>
        </p>
      </td>
      <td>
        <p>~70%</p>
      </td>
      <td>
        <p>~40%</p>
      </td>
      <td>
        <p>
          <strong>43% ▼</strong>
        </p>
      </td>
      <td>
        <p>Lower server compute costs and better resource efficiency</p>
      </td>
    </tr>
    <tr>
      <td>
        <p>
          <strong>Network traffic</strong>
        </p>
      </td>
      <td>
        <p>~25 MB/s</p>
      </td>
      <td>
        <p>~5 MB/s</p>
      </td>
      <td>
        <p>
          <strong>80% ▼</strong>
        </p>
      </td>
      <td>
        <p>Lower network costs and faster content delivery</p>
      </td>
    </tr>
    <tr>
      <td>
        <p>
          <strong>Active containers</strong>
        </p>
      </td>
      <td>
        <p>~10</p>
      </td>
      <td>
        <p>~4</p>
      </td>
      <td>
        <p>
          <strong>60% ▼</strong>
        </p>
      </td>
      <td>
        <p>A smaller infrastructure footprint and direct cost savings</p>
      </td>
    </tr>
  </tbody>
</table>

1. A fundamental reduction in server load and request count (requests 75%▼, CPU utilization 43%▼)

   The most important change was separating user requests from the server's compute load. With the cache handling most of the traffic, the number of actual requests reaching the origin server dropped by 75%.
   That in turn cut the amount of computation the server has to do by 43%, maximizing overall system efficiency and stability.

2. Better user experience (request latency 75%▼)

   The drop in server load immediately showed up as a better user experience.
   Request latency fell from 800ms to 200ms, a quarter of what it was, so users now get almost instant page loads.
   This also makes a decisive contribution to improving Core Web Vitals, which is directly tied to how satisfied people are with the service.

3. Direct savings on infrastructure and network costs (active containers 60%▼, network traffic 80%▼)

   This optimization paid off in cost savings too.
   - Infrastructure cost: The number of containers needed to handle the same traffic dropped by a full 60%. That means a big reduction in 'billable instance time', the core billing metric of GCP Cloud Run, which directly cut infrastructure costs.

   - Network cost: With less unnecessary data transfer, network traffic dropped by 80%. That means lower cloud costs, and it also means users can download content quickly with less data.

In conclusion, just by properly understanding and applying Next.js's rendering strategies, we were able to dramatically cut server load and costs, and greatly improve both user experience and service stability.

## What I Learned Along the Way (Behavior May Differ by Version)

While using Next.js in practice, I was worn down by how often the specs and APIs change, and "Vercel, you again?" crossed my mind dozens of times a day. 🥹
There was technical debt in the code, but I could feel technical debt piling up in myself, too.
I felt I couldn't go on like this, so during this improvement I decided to really understand the concepts behind the features I'd been using without full confidence.

### 1. revalidate = 0 === dynamic = ‘force-dynamic’?

**How revalidate = 0 works**

revalidate = 0 sets the route's default revalidation interval to 0 seconds, so that **data is re-fetched on every request**. This setting has the following characteristics.

- **Dynamic rendering by default:** The page is freshly rendered on the server on every request.
- **Respects the cache settings of individual `fetch` requests:** If an individual `fetch` request in the page explicitly sets `cache: 'force-cache'` or a positive `revalidate` value, that `fetch` request can be cached. In other words, the page itself is rendered dynamically, but some of the data that makes up the page can use cached values, which gives it some flexibility.

**How dynamic = 'force-dynamic' works**

dynamic = 'force-dynamic' **disables all caching for the route and forces it to render dynamically no matter what**. This setting has the following characteristics:

- **Forced dynamic rendering:** The page is always rendered at request time, leaving no room to be analyzed as static. This is the closest behavior to `getServerSideProps` from the old Pages Router.
- **Ignores the cache for all `fetch` requests:** A `fetch` request in the page with no special settings is treated the same as `{ cache: 'no-store' }`. If you set a `cache` option on an individual `fetch` request, that cache option takes priority.

---

### 2. revalidate = 0 and Individual fetch Cache Options

`revalidate = 0`: follows individual `fetch` cache options ✅

This setting only makes the page's **default behavior** dynamic; it doesn't ignore the cache settings set on individual `fetch` requests.

So the page itself is set up to be rendered fresh every time, but if a particular `fetch` request inside it has a cache option like `next: { revalidate: 60 }`, that data **follows the cache option you set, as is.** This gives you the flexibility to combine dynamic page rendering with selective data caching.

---

### 3. dynamic = ‘force-dynamic’ and Individual fetch Cache Options

`dynamic = 'force-dynamic'`: ignores the Full Route Cache ✅

This setting is a stronger, more sweeping rule. It's like ordering the whole route: "Don't allow the Full Route Cache, and only ever behave dynamically."

As a result, if an individual `fetch` has no explicit cache option, `{ cache: 'no-store' }` is forced as the default, but if you've set an option yourself, that setting takes priority.

---

### 4. The Lowest revalidate Value Wins

When generating a page, if several `fetch` requests each have a different `revalidate` time, Next.js uses the **shortest `revalidate` time** among them as the revalidation interval for the whole page.

This is the core working principle of **ISR**.

**Why it follows the minimum `revalidate`**

The reason Next.js picks the shortest interval is to **guarantee the freshness of the data**.

For example, let's say a page has the following two data requests:

- **Real-time trending news list:** needs refreshing every 10 seconds (`revalidate: 10`)
- **Weather info:** needs refreshing every hour (`revalidate: 3600`)

If this page were refreshed on a 1-hour cycle, the trending news list could stay stale for up to an hour. To prevent that, Next.js sets the shortest interval, **10 seconds**, as the revalidation interval for the whole page.
That's how it guarantees the page stays fresh. 🧐

So the rule that "the minimum revalidate wins" is a defining trait and behavior of ISR, which gives a static page the dynamic element of "regeneration."

---

### 5. When Does the Lowest-Value-Wins Rule Apply?

This was the part that confused me the most.

The following settings act as **"mode switches"** that force a page into dynamic rendering.

- `export const revalidate = 0;`
- `export const dynamic = 'force-dynamic';`
- Using dynamic APIs like `cookies()` and `headers()` in a page/layout

I wondered whether, if I used `export const revalidate = 0;`, that setting should take priority since the minimum value is 0.
But once one of these dynamic rendering switches is flipped on, the page behaves as SSR rather than ISR. **So the "lowest value wins" rule that applies in ISR doesn't apply.**

---

### 6. Data Cache vs. Full Route Cache

Next.js caching is broadly split into two kinds.

- **Data Cache**: caches **individual pieces of data** fetched via `fetch`. It's shared across requests.
- **Full Route Cache**: caches the rendered page and the RSC payload. Thanks to this, static pages (SSG/ISR) can load very quickly. It also lets navigation between pages be fast by using the RSC payload.

**How each rendering strategy uses the caches**

Which cache gets used depends on which rendering strategy you use.

- **SSG / ISR (static pages)**: **uses both caches ✅**
  - Data fetched with `fetch` is stored in the **Data Cache**, and the page rendered with that data is stored in the **Full Route Cache**.
- **SSR (dynamic pages)**: **uses only the Data Cache ✅**
  - Since the page is rendered fresh on every request, the **Full Route Cache** isn't used.
  - But during rendering, it uses the **Data Cache** to cut down on unnecessary API calls and speed things up.

---

### 7. fetch no-store

**The difference in behavior: bottom-up vs. top-down**

- `fetch(..., { cache: 'no-store' })`: a bottom-up decision
  - It declares that this one piece of data shouldn't be cached.
  - Because of this data's dynamic nature, the whole page that uses it automatically switches to dynamic rendering.
  - But this decision **doesn't affect other `fetch` requests** in the page. If another `fetch` has `force-cache` or a `revalidate` time, that cache is still respected.

- `dynamic = 'force-dynamic'`: a top-down decision
  - It's a setting that orders the whole page to be rendered dynamically no matter what. (Disables the Full Route Cache)
  - This route segment setting **propagates to all `fetch` requests beneath it**, and unless otherwise specified, `{ cache: 'no-store' }` is forced as the default.
  - However, if there's an explicit cache setting, that setting takes priority.

---

### 7. Changes in Next 15

**fetch default changed: `auto no cache`**

Next.js 15's default (auto no cache) always fetches fresh data in the development environment, and in build/production it's either prerendered statically or runs as SSR on every request, depending on whether dynamic APIs are present.

---

### 8. When a Page Switches to Dynamic Rendering Because It Hit a Dynamic API (Not `dynamic = 'force-dynamic'`), Is the Data Cache Still Kept?

- **When using dynamic APIs like `cookies()` and `headers()`:**
  1. Next.js detects the dynamic API and decides, "This page has to be rendered at request time."
  2. The `fetch` requests encountered during page rendering **keep their own cache options (such as the `revalidate` time) as they are**.
  3. So if the cached data is still valid, rendering gets faster by pulling the value from the Data Cache, and if it's not valid, it fetches fresh data. **It makes active use of the Data Cache**.
- **When using `export const dynamic = 'force-dynamic'`:**
  1. Next.js sees this setting and decides, "Everything about this page has to be dynamic, no exceptions."
  2. This setting changes the **default** for the fetch requests in the page to `{ cache: 'no-store' }`. `fetch`es with no options specified aren't cached.
  3. If the developer has made an explicit setting, that setting takes priority.

## Final Thoughts

Through this improvement, I got to experience raising the problem in the first place, confirming it through a PoC, and then solving it.
By constantly asking myself "why?", I learned a lot during the problem-solving process, and I was able to clearly understand and apply concepts that had been fuzzy for me.

It didn't stop at a simple improvement: I picked up how Next.js's rendering strategies and route segment behavior work, and based on that I was able to apply a rendering strategy that fits the characteristics of our service.
I'm especially pleased that I could prove the effect of the improvement quantitatively through data-driven measurement.

> I felt that I should build the habit of always checking and learning about the versions as they change.
