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

Next.js/React使用lazy/dynamic实现组件懒加载未达预期

问题根因

你当前的实现没有达到预期,核心是对React.lazy和Next.jsdynamic()的能力存在认知偏差:这两个API默认只实现代码分割能力,会把目标组件的代码拆分为独立的JS chunk,但只要组件被挂载到React渲染树中,浏览器就会立刻拉取对应chunk、执行组件逻辑(包括触发useEffect里的接口请求),不会等待组件进入视口,也不会等待首屏渲染完成。
你现有写法里,被<Suspense>包裹的组件在首屏渲染阶段就已经进入渲染队列,自然会在初始加载时触发接口请求,和你的预期不符。


原生实现方案(无需第三方依赖)

以下两种方案完全基于React/Next.js原生能力实现,满足你“首屏HTML不包含组件内容、初始加载不触发组件请求”的要求。

方案1:页面完全加载完成后再加载组件

通过监听页面加载状态,等首屏所有资源加载完成后再挂载懒加载组件即可:

import { useState, useEffect, Suspense, lazy } from 'react'
// Next.js项目可将React.lazy替换为next/dynamic
const ProductTabbedWidget = lazy(() => import('../../../components/ProductSearch/ProductTabbed/ProductTabbedWidget'))

export default function IndexPage() {
  const [mountLazyWidget, setMountLazyWidget] = useState(false)

  useEffect(() => {
    // 页面完全加载完成后再挂载组件
    const mountWidget = () => setMountLazyWidget(true)
    if (document.readyState === 'complete') {
      mountWidget()
    } else {
      window.addEventListener('load', mountWidget, { once: true })
    }

    return () => window.removeEventListener('load', mountWidget)
  }, [])

  return (
    <main>
      {/* 其余首屏内容 */}
      {mountLazyWidget && (
        <Suspense fallback={<div>Loading</div>}>
          <ProductTabbedWidget vehicleType={type} />
        </Suspense>
      )}
    </main>
  )
}

方案2:组件进入视口后再加载

基于浏览器原生Intersection Observer API实现视口检测,组件滚动到可视区域附近时再挂载加载:

import { useState, useEffect, Suspense, lazy, useRef } from 'react'
const ProductTabbedWidget = lazy(() => import('../../../components/ProductSearch/ProductTabbed/ProductTabbedWidget'))

// 视口检测自定义Hook,无第三方依赖
function useInView(observerOptions = { rootMargin: '200px' }) {
  const targetRef = useRef(null)
  const [isInView, setIsInView] = useState(false)

  useEffect(() => {
    if (!targetRef.current) return
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setIsInView(true)
        observer.disconnect()
      }
    }, observerOptions)

    observer.observe(targetRef.current)
    return () => observer.disconnect()
  }, [observerOptions])

  return [targetRef, isInView]
}

export default function IndexPage() {
  const [widgetWrapperRef, widgetInView] = useInView()
  return (
    <main>
      {/* 其余首屏内容 */}
      <div ref={widgetWrapperRef} style={{ minHeight: '200px' }}>
        {widgetInView && (
          <Suspense fallback={<div>Loading</div>}>
            <ProductTabbedWidget vehicleType={type} />
          </Suspense>
        )}
      </div>
    </main>
  )
}

配置rootMargin: '200px'可以让组件在距离视口还有200px时就提前加载,避免用户滚动到位置时看到长时间的加载态。Next.js 13+ App Router场景下直接替换为next/dynamic即可,逻辑完全一致。


生效验证方式
  • 打开Chrome DevTools的Network面板,勾选禁用缓存,刷新页面:首屏加载阶段不会出现组件对应的JS chunk请求,也不会出现组件内部触发的/api/get-data接口请求
  • 查看页面原始源代码(Ctrl+U / Cmd+U):首屏返回的HTML中不存在组件渲染的DOM内容
  • 等待页面完全加载/滚动到组件位置后,才会出现对应JS资源和接口请求

注意事项
  • dynamic()配置项中的suspense: true仅表示开启Suspense渲染模式支持,不会改变组件的挂载触发时机
  • 只要懒加载组件被放入React渲染树,哪怕它处于视口外,React都会立刻拉取组件代码并执行内部逻辑,必须通过条件渲染控制组件挂载时机才能实现真正的懒加载

内容的提问来源于stack exchange,提问作者Vivek Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 08:01:04