嘉慶的個人網站

Next.js 中 CSS 模塊 FOUC 問題的解決方案

Next.js CSS FOUC 

Henry Hung

洪嘉慶

發布於 2022-10-14  •  更新於 2022-10-14  •  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;