嘉庆的个人网站

Next.js 中 CSS 模块 FOUC 问题的解决方案

Next.js CSS FOUC 

Henry Hung

洪嘉庆

发布于 2022-10-11  •  更新于 2022-10-11  •  3.25 分钟阅读

概述

在 Next.js 中使用 CSS 模块时,可能会遇到一种称为 FOUC(Flash of Unstyled Content)的问题。 这种问题会导致页面在加载时短暂地显示未经样式处理的内容,然后再应用样式这篇文章将介绍如何解决这个问题。

问题

直到 Next.js 14.2.6,在使用APP ROUTER的时候,<Link>组件的预加载 (prefetch) 都不包括css模块,只会预加载链接页面中的html和js内容。 这导致了在使用<Link>组件跳转页面时,html内容会先被浏览器渲染到 dom tree 中,然后再向服务器请求css文件。 请求css的过程中,页面会出现一段时间的未经样式处理的内容,这就是FOUC问题。 这个问题从2018年就已经有人在Next.js的GitHub上提出了,但官方的态度非常敷衍,有不下3个类似的 Issue 在未被解决的情况下关闭了。 在失望的同时,我只能尝试自己解决这个问题。

解决方案

经过三天时间的尝试,我终于找到了一个最佳解决方案,这个解决方案是基于 import() 动态导入的。

大致上,我们需要在RootLayout组件中,动态的导入一次所有的css模块。这样所有的css模块都会被存入同一个css文件内。 进而在首屏加载时,就把所有的css文件都加载进来,从根源上避免了FOUC问题。

  1. 安装globby库

    bash
    Copy Code
    npm i globby
  2. 修改根目录下的jsconfig.json文件,确保 src/../../ 是可以正确的指向项目根目录的 src 文件夹

    json
    Copy Code
    {
        "compilerOptions": {
            "baseUrl": "./",
            "include": ["./"]
        }
    }
  3. 在root layout中插入这个loadAllCssUnderAppDirectory()方法。 该方法使用了globby去获取所有src/app/下所有.module.css的路径,然后在root layout中进行import. 这样就使root css文件包含了所有的src/app/下的css,从而在首屏加载时fetch所有的css文件,避免了FOUC问题。

    之所以使用模版字符串,是因为next.js的webpack需要路径是静态的,并且至少前两段路径是静态的,不然就会报错。如果你的.module.css文件还分布在其他地方,如src/components/,你也可以自行添加一个loadAllCssUnderComponentsDirectory()方法。

    javascript
    Copy Code
    const loadAllCssUnderAppDirectory = async () => {
        const pathPrefix = 'src/app/';
        const pathSuffix = '.module.css';
        // 获取所有的 CSS 模块路径
        const paths = await globby(`${pathPrefix}**/*${pathSuffix}`);
        // 动态加载所有的 CSS 模块
        for (let path of paths) {
            const truncatedPath = path.slice(pathPrefix.length, -pathSuffix.length);
            (await import(`src/app/${truncatedPath}.module.css`)).default;
        }
    };
    
    const Layout = async ({ children }) => {
    
        await loadAllCssUnderAppDirectory();
    
        return (
            <html>
                <body>
                    {children}
                </body>
            </html>
        );
    };
    
    export default Layout;