Mark Ku's Blog

Background

Two years ago, the previous general manager had just retired. In the past, he had acted as a gatekeeper for the Taiwan IT department, which meant we could prioritize requirements. But by the time I joined, that gatekeeper was gone. Every request became urgent and rushed. The system had just gone live and was riddled with problems, and a group of people had already left. I walked into the company right in the middle of this chaos.

When I first joined, the Next.js site had just gone live. It was using the Page Router, and we kept serving stale pages. I had only been with the company for about a month when I had to listen to a user from the US complain for an hour about the unstable e-commerce system. I wasn't very familiar with Next.js at the time, and the US holiday shopping season was just about to kick off.

Here's a summary of the two major problems we solved back then.

Problem 1: Incrementally Static Regeneration (ISR) pages kept serving stale content; even after the revalidate time expired, visiting the page again did not trigger a re-render.

During the Black Friday sales period, our daily page traffic was high (over 20,000 visits). We noticed that after being deployed for a while, our Docker containers would grow from 2GB to 8GB. After some analysis, we discovered that the original image files uploaded by our users were very large and uncompressed, some up to 10MB. My theory was that because Node.js is single-threaded and relies on the event loop to schedule tasks, Next.js was trying to process images while also rendering pages. This image processing was creating a major performance bottleneck.

Solution

During the investigation, I discovered that Next.js has a built-in Image Loader designed to optimize images by serving different versions for different resolutions. However, image processing is I/O-intensive. While this might not be a problem for a low-traffic site, it becomes a significant performance bottleneck under heavy load. I then discovered that we had a Cloudflare Enterprise plan. So, I offloaded the image optimization task to the Cloudflare Image API, which finally solved our site's performance problems.

Problem 2: Inaccurate inventory on ISR pages

To serve content to users quickly, we adopted Incremental Static Regeneration (ISR). However, during the peak sales season, we often ran into issues with items selling out. The product and detail pages would show that an item was in stock, but when a user tried to add it to their cart, they would get an "insufficient stock" error.

Short-term Solution - Since I couldn't find a fundamental solution at the time, I wrote a "rebuild page" API. It could be triggered by adding ?forceUpdate=true to the URL. I asked the back-end engineers to call this API whenever product information was updated.

import { LogLevel } from '@/lib/log/const-logging';
import { serverSideLog } from '@/lib/log/server-side-log';
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function rebuild(req: NextApiRequest, res: NextApiResponse) {
    const secret = req.headers.$secret || req.query.secret; // req.headers.$secret => backoffice
    const refreshUrl: string = req?.query?.page?.toString()?.startsWith('/')
        ? req.query.page?.toString()
        : `/${req.query.page?.toString()}`;

    const serverUrl = req.headers?.host;
    const result: IReBuildResponse = { revalidated: true };
    const param = { revalidatedHost: serverUrl };

    if (secret && secret === $projects.secret) {
        try {
            serverSideLog(
                `[RebuildPage][Manual]Server rebuild is success! (${refreshUrl})`,
                LogLevel.Information,
                param
            );
            await res.revalidate(refreshUrl);
            return res.json(result);
        } catch (err) {
            serverSideLog(`[RebuildPage][Manual]Trigger rebuild is fail!(${refreshUrl})`, LogLevel.Error, param);
            return res.status(500).json({ message: JSON.stringify(err) });
        }
    }

    return res.status(503).json({ message: `Invalid token: ${$projects.secret}, ${secret}` });
}

Long-term Solution

After gaining a deeper understanding of the business domain, I realized that inventory information simply shouldn't be cached. Discovering an item is out of stock only when adding it to the cart creates a poor user experience. However, if we didn't cache the page and used server-side rendering for every request, our large product catalog and high traffic would quickly lead to blocked requests and overloaded servers.

So, we decided to keep the pages as ISR but fetch the inventory data on the client side. This would resolve the inconsistency between what was displayed and the actual stock. I asked the back-end engineers to write the real-time inventory data to Redis, and we would then fetch that data on the client side at render time.

export default function ProductCardsSection({ productData = [], className = '' }: Readonly<IProductCardsSectionProps>) {
    const {
        data: stockData = {},
        isLoading,
        isError,
    } = useGetStockByModelIDsQuery(productData.map((product) => product.modelId));
	...
}
Get stock from client side
Get stock from client side

P.S. During this process, I also discovered that if you want to reduce the size of your Next.js container image, you can use the standalone output mode. It optimizes the build by packaging only the files and modules truly needed at runtime, which can reduce the bundle size and simplify deployment. P.S. Additionally, by specifying the lightweight alpine-node image in the Dockerfile, the size of the Next.js Docker container can be reduced by about 30%.

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Readers also read