news 2026/8/22 0:48:41

Antd 在 Next.js 项目中,初次渲染样式丢失

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Antd 在 Next.js 项目中,初次渲染样式丢失

问题

因为之前 Next 和 React 接连出现安全问题,于是把博客的依赖升级了一下,没想到就搞出问题了,如下图所示:

初次渲染时样式丢失,在客户端上会短暂展示 Antd 组件无样式界面,出现样式闪烁的情况。项目是 Next 14,React 18 的 App Router 项目,依赖版本:"@ant-design/nextjs-registry": "^1.3.0""antd": "^5.14.2"

解决思路

因为 Antd 是 CSS-in-js 的 UI 库,按照官方文档呢,我们需要一个 @ant-design/nextjs-registry 包裹整个页面,在 SSR 时收集所有组件的样式,并且通过<script>标签在客户端首次渲染时带上。

/* by 01130.hk - online tools website : 01130.hk/zh/chaodai.html */ // src/app/layout.tsx import { AntdRegistry } from '@ant-design/nextjs-registry' export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( <html lang="en"> <head> {/* ... */} </head> <body> <AntdRegistry> {/* ... 假装这是页面代码 */} </AntdRegistry> </body> </html> ) }

对照了一下官方文档也问了下 AI,没发现我的写法有什么问题。就在这个时候,我猛然间看见了 Antd 的 Pages Router 使用的注意事项:

我寻思,可能我遇到的情况和这里一样,是内部依赖版本@ant-design/cssinj不对引起的。

输入npm ls @ant-design/cssinjs看了一下,

/* by 01130.hk - online tools website : 01130.hk/zh/chaodai.html */ ├─┬ @ant-design/nextjs-registry@1.3.0 │ └── @ant-design/cssinjs@2.0.1 └─┬ antd@5.14.2 └── @ant-design/cssinjs@1.24.0 deduped

@ant-design/nextjs-registry内部也使用了@ant-design/cssinjs,而且它的版本和antd内置版本还不一样,这就是问题的所在了。

接下来把@ant-design/nextjs-registry的版本降到了 1.2.0,这时候版本对上了,bug 也就修复了。

├─┬ @ant-design/nextjs-registry@1.2.0 │ └── @ant-design/cssinjs@1.24.0 └─┬ antd@5.14.2 └── @ant-design/cssinjs@1.24.0 deduped

@ant-design/nextjs-registry 的内部发生了什么

AntdRegistry

这勾起了我的好奇心,就让我们来看看@ant-design/nextjs-registry干了些什么:

https://github.com/ant-design/nextjs-registry

// /src/AntdRegistry.tsx 'use client'; import type { StyleProviderProps } from '@ant-design/cssinjs'; import type { FC } from 'react'; import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; import { useServerInsertedHTML } from 'next/navigation'; import React, { useState } from 'react'; type AntdRegistryProps = Omit<StyleProviderProps, 'cache'>; const AntdRegistry: FC<AntdRegistryProps> = (props) => { const [cache] = useState(() => createCache()); useServerInsertedHTML(() => { const styleText = extractStyle(cache, { plain: true, once: true }); if (styleText.includes('.data-ant-cssinjs-cache-path{content:"";}')) { return null; } return ( <style id="antd-cssinjs" // to make sure this style is inserted before Ant Design's style generated by client >@ant-design/cssinjs

首先来看上文const [cache] = useState(() => createCache())这一行。

@ant-design/cssinjs 部分仓库在 https://github.com/ant-design/cssinjs

它干了几件事:

  1. 生成唯一实例 ID。
  2. (仅客户端)将 body 中的样式移到 head 中,并且去重。
export function createCache() { const cssinjsInstanceId = Math.random().toString(12).slice(2); // Tricky SSR: Move all inline style to the head. // PS: We do not recommend tricky mode. if (typeof document !== 'undefined' && document.head && document.body) { const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || []; const { firstChild } = document.head; Array.from(styles).forEach((style) => { (style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId; // Not force move if no head if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) { document.head.insertBefore(style, firstChild); } }); // Deduplicate of moved styles const styleHash: Record<string, boolean> = {}; Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach( (style) => { const hash = style.getAttribute(ATTR_MARK)!; if (styleHash[hash]) { if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) { style.parentNode?.removeChild(style); } } else { styleHash[hash] = true; } }, ); } return new CacheEntity(cssinjsInstanceId); }
  1. 返回一个类包裹的Map结构,在StyleProvider中由后代组件把首屏所需样式传回。结构如下所示:
export type KeyType = string | number; type ValueType = [number, any]; /** Connect key with `SPLIT` */ export declare function pathKey(keys: KeyType[]): string; declare class Entity { instanceId: string; constructor(instanceId: string); /** @private Internal cache map. Do not access this directly */ cache: Map<string, ValueType>; extracted: Set<string>; get(keys: KeyType[]): ValueType | null; /** A fast get cache with `get` concat. */ opGet(keyPathStr: string): ValueType | null; update(keys: KeyType[], valueFn: (origin: ValueType | null) => ValueType | null): void; /** A fast get cache with `get` concat. */ opUpdate(keyPathStr: string, valueFn: (origin: ValueType | null) => ValueType | null): void; } export default Entity;

至于StyleProvider,除了整合上层StyleProvider注入的样式外,它基本上是一个普通的Context.Provider,作用也很好猜,把createCache返回的Map结构注入到下层组件中。

const StyleContext = React.createContext<StyleContextProps>({ hashPriority: 'low', cache: createCache(), defaultCache: true, autoPrefix: false, }) export const StyleProvider: React.FC<StyleProviderProps> = (props) => { // ... return ( <StyleContext.Provider value={context}>{children}</StyleContext.Provider> ); };

Antd 组件的调用路径

具体源码就不细看了,以按钮组件 Button 为例,调用路径大致如下:

flowchart TD subgraph CSSInJS 底层机制 genStyleUtils["@ant-design/cssinjs-utils<br/>genStyleUtils"] genStyleHooks["@ant-design/cssinjs-utils<br/>genStyleHooks"] genComponentStyleHook["@ant-design/cssinjs-utils<br/>genComponentStyleHook"] useStyleRegister["@ant-design/cssinjs<br/>useStyleRegister"] useGlobalCache["@ant-design/cssinjs<br/>useGlobalCache"] end subgraph Antd 组件层 useStyleAntd[useStyle] Button[Button组件] JSX[写入到JSX并返回] end genStyleUtils -->|生成| genStyleHooks genStyleHooks -->|调用| genComponentStyleHook genComponentStyleHook -->|调用| useStyleRegister useStyleRegister -->|调用| useGlobalCache genStyleHooks -->|返回| useStyleAntd Button -->|调用| useStyleAntd useStyleAntd -->|样式注入| JSX

在 useGlobalCache 中 调用React.useContext(StyleContext)cache.onUpdate方法更新缓存。

总结

这次碰到的问题其实挺典型的:升级了依赖,结果页面出问题了。解决方法很简单——把 @ant-design/nextjs-registry 从 1.3.0 降级到 1.2.0,让它跟 antd 用的 @ant-design/cssinjs 内部版本对上就行了。

以后要是用 Next.js App Router 配 Ant Design 遇到类似情况,可以先看看这两个包的版本是不是兼容。有时候问题没看起来那么复杂,可能就是版本没对上。

出于好奇,我还顺便看了一下 AntdRegistry 内部的实现——发现它主要是通过StyleProvider在服务端收集样式,然后通过useServerInsertedHTML在客户端首次渲染时注入到style标签中,这样就能避免样式闪烁的问题。

大家的阅读是我发帖的动力,本文首发于我的博客:deer.shika-blog.xyz,欢迎大家来玩, 转载请注明出处。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/21 23:04:47

微服务架构设计 - 高并发缓存设计

决胜毫秒级:车贷核心系统的高并发缓存架构实战与方法论 在车贷金融领域,用户体验与风控效率是天平的两端。进件系统的响应速度直接决定了转化率,而额度查询的准确性则关乎资金安全。面对日均百万级的调用和瞬间的流量洪峰,简单的“Redis get/set”已无法满足生产环境的严苛…

作者头像 李华
网站建设 2026/8/21 6:02:01

LobeChat能否实现数据库持久化存储?避免数据丢失的关键

LobeChat 能否实现数据库持久化存储&#xff1f;避免数据丢失的关键 在如今大模型应用爆发的背景下&#xff0c;越来越多开发者选择搭建私有化的 AI 对话系统。LobeChat 凭借其简洁现代的界面、对主流大模型的良好支持以及高度可扩展的插件体系&#xff0c;迅速成为个人用户和小…

作者头像 李华
网站建设 2026/8/21 1:03:18

Qwen3-8B镜像安装全攻略:轻松部署高性价比大模型

Qwen3-8B镜像安装全攻略&#xff1a;轻松部署高性价比大模型 在AI应用快速落地的今天&#xff0c;越来越多开发者面临一个现实问题&#xff1a;如何在有限预算和硬件条件下&#xff0c;运行一个真正“能用、好用”的大语言模型&#xff1f;许多开源模型虽然参数庞大、宣传亮眼&…

作者头像 李华
网站建设 2026/8/20 8:35:23

XC7Z020-2CLG484I, 双核ARM Cortex-A9处理器, 现货库存

型号介绍今天我要向大家介绍的是 Xilinx 的一款处理器——XC7Z020-2CLG484I。 它搭载了相当规模的FPGA逻辑资源&#xff0c;包括278,624个逻辑单元、36个18x18位的DSP48E1乘法器、2个块RAM&#xff08;Block RAM&#xff09;以及大量的分布式RAM和移位寄存器。PL部分允许用户实…

作者头像 李华
网站建设 2026/8/22 0:39:30

DBLens 的数据安全、登录方式与离线使用说明

在选择数据库管理工具时&#xff0c;开发者和企业用户通常会重点关注以下问题&#xff1a; 数据库连接信息是否安全&#xff1f;工具会不会获取或上传数据库数据&#xff1f;执行日志、AI 对话是否会被收集&#xff1f;是否支持离线使用&#xff1f;登录方式是否可控&#xff0…

作者头像 李华
网站建设 2026/8/21 7:44:55

基于Modbus的电机控制上位机程序——用Qt打造高效控制界面

qt例程&#xff5e;基于Modbus的电机控制上位机程序 功能包括&#xff1a; 1、基于委托的表格自定义样式&#xff1b; 2、基于Modbus的电机配置文件读取和写入&#xff1b; 3、电机状态的实时显示。 在工业控制领域&#xff0c;基于Modbus协议的电机控制上位机程序至关重要。今…

作者头像 李华