金数据技术博客 · №19

One Button, 440 KB: The Cost of Importing antd in a Server Component

· flanker · frontend / React / nextjs · 中文版

中文版

TL;DR

Background

Jinshuju (金数据) is an online form builder SaaS. Our new frontend is a fairly large Next.js application, and we use antd as the UI library for internal-facing pages. While working on bundle size optimization, we noticed something odd: some routes that use only one or two antd components ended up shipping the complete antd library.

The culprit turned out to be a single line of perfectly innocent-looking code — an antd import inside a Server Component.

Minimal Reproduction

Two Next.js projects (antd 6.2.0 / Next.js 16.2.6 / React 19.2.1), each rendering nothing but a single Button.

A. Server Import

// app/page.tsx (Server Component)

import { Button } from 'antd'

export default function Page() {
  return <Button type="primary">hello</Button>
}

B. Client Import

// app/page.tsx (Server Component)

import ClientButton from './ClientButton'

export default function Page() {
  return <ClientButton />
}

// app/ClientButton.tsx

'use client'

import { Button } from 'antd'

export default function ClientButton() {
  return <Button type="primary">hello</Button>
}

Both pages render byte-for-byte identical HTML:

<button type="button" class="ant-btn css-var-root ant-btn-primary ant-btn-color-primary ant-btn-variant-solid"><span>hello</span></button>

But the JS bundles are wildly different:

A. Server Import B. Client Import
JS Bundle (RAW) 1458 KB 176 KB
JS Bundle (gzip) 440 KB 56 KB
antd component dirs in output 76 12

Dev Mode Comparison

Server import: 54 requests, 5486 KB. steps, table, transfer, upload… components that don't exist anywhere on the page all get loaded.

dev server import

Client import: 23 requests, 4446 KB (uncompressed dev mode), no unnecessary components.

dev client import

Production Bundle Analysis

Server import: All Route Modules 3.54 MB (uncompressed), 1545 modules, containing every antd component.

prod server import

Client import: All Route Modules 1.05 MB (uncompressed), 365 modules, containing only Button and its required antd dependencies.

prod client import

Why This Happens

The first line of antd/es/index.js is:

"use client";

So antd's barrel file is itself a client module.

The moment a Server Component imports it, Next.js establishes an RSC client reference on antd/es/index.js — the entire barrel becomes a client entry point, and all 76 components it re-exports become reachable code.

optimizePackageImports works by rewriting barrel imports into imports of concrete subpaths, but that rewrite cannot take effect here, so the whole barrel ends up in the output.

This is not specific to antd: any component library whose barrel file starts with "use client" has the same trap when imported wholesale from a Server Component.

The Fix

Move antd usage into a Client Component and keep the page / layout as a Server Component. The rendered output is identical, and the bundle only contains the components you actually use.

In the Jinshuju codebase we turned this into an architectural rule: the app/ routing layer must not import antd; antd configuration (Provider, message / notification, etc.) lives in client components in the views layer.

Alternatives

Deep imports also bypass the barrel:

import Button from 'antd/es/button'

But we don't recommend it: antd/es/button is not a public API, and not every component has a corresponding path.