NextJS预加载样式解决FOUC问题:附项目核心代码示例
问题背景
使用NextJS与Ant Design开发的网站出现FOUC(无样式内容闪烁)问题,需要实现JS加载前预加载样式。项目包含globals.css全局样式文件,同时每个组件使用独立的CSS模块文件。当前/pages/_app.js与/pages/_document.js代码如下:
当前/pages/_app.js代码
import { useEffect } from "react"; import { useRouter } from "next/router"; import Head from "next/head"; import "../styles/globals.css"; import TagManager from "react-gtm-module"; import AppHeader from "../components/MainMenu/header"; import AppHeaderEN from "../components/MainMenu/MenuEN/headerEN"; import ScrollButton from "../components/ScrollToTopWidget/scrollToTopButton"; import CookiesEN from "../components/Cookies/cookiesEn"; import CookiesAF from "../components/Cookies/cookiesAf"; import Footer from "../components/MainFooter/Footer"; import FooterEN from "../components/MainFooter/FooterEN"; function App({ Component, pageProps }) { const router = useRouter(); useEffect(() => { if (typeof window !== 'undefined') { const tagManagerArgs = { gtmId: "GTM-NV63HC6", }; TagManager.initialize(tagManagerArgs); } }, []); useEffect(() => { const jssStyles = document.querySelector('#jss-server-side'); if (jssStyles) { jssStyles.parentElement.removeChild(jssStyles); } }, []); useEffect(() => { const tidioScript = document.createElement("script"); tidioScript.src = "//code.tidio.co/x6prkzftri0czsnncigtqpailkwuzx3o.js"; tidioScript.async = true; document.head.appendChild(tidioScript); return () => { document.head.removeChild(tidioScript); }; }, []); return ( <> <div className="main"> <Head> <script dangerouslySetInnerHTML={{ __html: ` !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window,document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '205242273286619'); fbq('track', 'PageView'); `, }} /> </Head> {router.pathname === "/en" || router.pathname === "/en/" || router.pathname === "/about-us" || router.pathname === "/press-room" || router.pathname === "/benefits" || router.pathname === "/terms-and-conditions" ? ( <AppHeaderEN /> ) : ( <AppHeader /> )} <Component {...pageProps} /> <ScrollButton /> {router.pathname === "/en" || router.pathname === "/en/" || router.pathname === "/about-us" || router.pathname === "/press-room" || router.pathname === "/benefits" || router.pathname === "/terms-and-conditions" ? ( <CookiesEN /> ) : ( <CookiesAF /> )} {router.pathname === "/en" || router.pathname === "/en/" || router.pathname === "/about-us" || router.pathname === "/press-room" || router.pathname === "/benefits" || router.pathname === "/terms-and-conditions" ? ( <FooterEN /> ) : ( <Footer /> )} </div> </> ); } export default App;
当前/pages/_document.js代码
import Document, { Html, Head, Main, NextScript } from 'next/document'; class MyDocument extends Document { render() { return ( <Html> <Head> <link rel="icon" href="https://res.cloudinary.com/brojure-site/image/upload/v1696333014/assets/logos/fav_afqres.ico" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } } export default MyDocument;
解决方案
FOUC核心原因是Ant Design的JSS样式未在服务端渲染阶段注入到HTML中,导致页面初始加载时无样式,JS加载完成后才渲染样式。以下是针对性修复步骤:
1. 修改_document.js,实现Ant Design样式的服务端注入
NextJS的_document.js是自定义服务端渲染HTML的入口,需要在这里集成Ant Design的服务端样式收集逻辑,确保样式在HTML返回时就已存在。
替换原/pages/_document.js代码为:
import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheet as AntServerStyleSheet, createGenerateClassName } from '@ant-design/cssinjs'; export default class MyDocument extends Document { static async getInitialProps(ctx) { // 初始化Ant Design样式收集器 const antSheet = new AntServerStyleSheet(); const generateClassName = createGenerateClassName(); try { const originalRenderPage = ctx.renderPage; ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => antSheet.collectStyles(<App generateClassName={generateClassName} {...props} />), }); const initialProps = await Document.getInitialProps(ctx); // 将收集到的样式注入HTML头部 return { ...initialProps, styles: ( <> {initialProps.styles} {antSheet.getStyleElement()} </> ), }; } finally { antSheet.seal(); } } render() { return ( <Html> <Head> <link rel="icon" href="https://res.cloudinary.com/brojure-site/image/upload/v1696333014/assets/logos/fav_afqres.ico" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); } }
2. 确保全局样式优先级
当前_app.js中已正确导入globals.css,NextJS会自动将全局样式打包并在服务端注入到HTML中,无需额外修改,但需保证全局样式导入语句在组件导入之前,确保加载顺序正确。
3. 保留组件CSS模块的正确使用
NextJS的CSS模块默认支持服务端渲染,只要组件按import styles from './Component.module.css'方式导入,就会自动在服务端生成对应样式并注入,无需额外配置。
4. 保留JSS样式清理逻辑
原_app.js中移除#jss-server-side的逻辑是正确的,用于清理服务端注入的样式,避免客户端重复渲染,保留该代码即可。
原理说明
通过在_document.js的getInitialProps中使用Ant Design的ServerStyleSheet收集所有组件的JSS样式,并将这些样式作为<style>标签注入到HTML头部,使得页面加载时就能获取完整样式,避免JS加载完成后才渲染样式导致的闪烁问题。
内容的提问来源于stack exchange,提问作者Kayla-May Smit

