You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Next.js13(App Router)实现非装饰性、可计算资源加载时长的启动屏

解决方案:Next.js 13 App Router 全局预加载器实现

核心疑问解答:能否为整个应用配置loading.js?

App Router 的loading.js是路由级别的,无法直接全局配置,但可以通过客户端组件+全局状态管理模拟全局加载逻辑,同时结合路由级loading.js实现路由切换时的加载状态同步。


方法一:客户端组件监听全局资源加载

利用'use client'组件操作DOM,监听所有资源(包括图片)的加载状态,控制预加载器的显示/隐藏。

1. 创建全局预加载器组件

// app/components/Preloader.tsx
'use client';

import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';

export default function Preloader() {
  const [isLoading, setIsLoading] = useState(true);
  const pathname = usePathname(); // 监听路由切换,重置加载状态

  useEffect(() => {
    // 检查所有图片是否加载完成
    const checkAllImagesLoaded = () => {
      const allImages = Array.from(document.querySelectorAll('img'));
      return allImages.every(img => img.complete || img.naturalWidth > 0);
    };

    // 隐藏预加载器的逻辑
    const hidePreloader = () => {
      if (checkAllImagesLoaded()) {
        setIsLoading(false);
      }
    };

    // 初始触发一次检查
    const timer = setTimeout(hidePreloader, 100); // 避免DOM未就绪

    // 监听全局资源加载完成事件
    window.addEventListener('load', hidePreloader);

    // 监听动态添加的图片(比如懒加载、路由切换后新增的图片)
    const observer = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        const newImages = Array.from(mutation.addedNodes).filter(
          node => node instanceof HTMLImageElement
        );
        newImages.forEach(img => {
          img.addEventListener('load', hidePreloader);
          img.addEventListener('error', hidePreloader); // 加载失败也算完成
        });
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // 路由切换时重置加载状态
    setIsLoading(true);

    return () => {
      clearTimeout(timer);
      window.removeEventListener('load', hidePreloader);
      observer.disconnect();
    };
  }, [pathname]);

  if (!isLoading) return null;

  return (
    <div className="fixed inset-0 bg-white/90 z-[9999] flex items-center justify-center">
      {/* 自定义加载动画 */}
      <div className="w-16 h-16 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
    </div>
  );
}

2. 在RootLayout中引入组件

// app/layout.tsx
import Preloader from './components/Preloader';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className="min-h-screen">
        <Preloader />
        {children}
      </body>
    </html>
  );
}

方法二:结合路由loading.js与全局状态

如果需要同步路由切换的加载状态(比如Server Components数据加载),可以用全局状态管理配合路由级loading.js。

1. 创建全局加载状态上下文

// app/context/LoadingContext.tsx
'use client';

import { createContext, useContext, useState } from 'react';

const LoadingContext = createContext({
  isGlobalLoading: false,
  setIsGlobalLoading: (state: boolean) => {},
});

export function LoadingProvider({ children }: { children: React.ReactNode }) {
  const [isGlobalLoading, setIsGlobalLoading] = useState(true);

  return (
    <LoadingContext.Provider value={{ isGlobalLoading, setIsGlobalLoading }}>
      {children}
    </LoadingContext.Provider>
  );
}

export function useLoading() {
  return useContext(LoadingContext);
}

2. 修改RootLayout包裹Provider

// app/layout.tsx
import { LoadingProvider } from './context/LoadingContext';
import Preloader from './components/Preloader';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className="min-h-screen">
        <LoadingProvider>
          <Preloader />
          {children}
        </LoadingProvider>
      </body>
    </html>
  );
}

3. 修改Preloader使用全局状态

// app/components/Preloader.tsx
'use client';

import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { useLoading } from '../context/LoadingContext';

export default function Preloader() {
  const { isGlobalLoading, setIsGlobalLoading } = useLoading();
  const pathname = usePathname();

  useEffect(() => {
    const checkAllImagesLoaded = () => {
      const allImages = Array.from(document.querySelectorAll('img'));
      return allImages.every(img => img.complete || img.naturalWidth > 0);
    };

    const hidePreloader = () => {
      if (checkAllImagesLoaded()) {
        setIsGlobalLoading(false);
      }
    };

    window.addEventListener('load', hidePreloader);
    const observer = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        const newImages = Array.from(mutation.addedNodes).filter(
          node => node instanceof HTMLImageElement
        );
        newImages.forEach(img => {
          img.addEventListener('load', hidePreloader);
          img.addEventListener('error', hidePreloader);
        });
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // 路由切换时重置状态
    setIsGlobalLoading(true);
    setTimeout(hidePreloader, 100);

    return () => {
      window.removeEventListener('load', hidePreloader);
      observer.disconnect();
    };
  }, [pathname, setIsGlobalLoading]);

  if (!isGlobalLoading) return null;

  return (
    <div className="fixed inset-0 bg-white/90 z-[9999] flex items-center justify-center">
      <div className="w-16 h-16 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
    </div>
  );
}

4. 在路由中配置loading.js

// app/products/loading.js
'use client';

import { useLoading } from '../context/LoadingContext';
import { useEffect } from 'react';

export default function Loading() {
  const { setIsGlobalLoading } = useLoading();

  useEffect(() => {
    setIsGlobalLoading(true);
    return () => setIsGlobalLoading(false);
  }, [setIsGlobalLoading]);

  return null; // 全局预加载器已处理显示逻辑
}

关键注意事项

  • 所有DOM操作逻辑必须放在'use client'组件中,Server Components无法直接访问浏览器API。
  • 必须处理图片加载失败的情况,避免预加载器无限显示。
  • 路由切换时要重置加载状态,因为window.load事件只会在页面首次加载时触发。
  • 不要依赖固定超时(比如4秒),需根据实际资源加载状态动态控制。

内容的提问来源于stack exchange,提问作者Ritik Jangir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 09:03:10