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

NextJS Hydration失败问题及动态组件加载延迟解决方案咨询

解决Next.js中Swiper组件的Hydration错误(保留SSR无延迟)

问题根源

你遇到的Hydration错误,本质是服务端渲染的Swiper组件DOM结构和客户端Hydrate后的DOM不一致。比如Swiper的loop模式会在客户端初始化时复制Slide实现循环,但服务端渲染时不会生成这些额外DOM,导致前后结构不匹配;另外Swiper部分逻辑依赖浏览器API,服务端环境下无法正常执行,也会引发差异。

可行解决方案(无需禁用SSR)

方案1:控制loop模式仅在客户端启用

通过状态标记是否处于客户端环境,服务端渲染时关闭loop,避免生成额外DOM;客户端挂载后再开启loop,此时Hydration已完成,不会触发结构不匹配。

import { useEffect, useState, useRef } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import SwiperCore, { Navigation, A11y } from 'swiper';
import Image from 'next/image';

SwiperCore.use([Navigation, A11y]);

type Props = {
  className?: string;
  title: string;
};

export default function Slider({ className, title }: Props): JSX.Element {
  const swiperRef = useRef<SwiperCore>();
  const [isClient, setIsClient] = useState(false);

  // 客户端挂载后标记状态
  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <div className={className}>
      <h3 className="pb-8 text-center font-primary text-xl md:text-left">
        {title}
      </h3>
      <div className="flex flex-row items-center">
        <button 
          onClick={() => swiperRef.current?.slidePrev()}
          disabled={!isClient} // 客户端前禁用按钮避免无效操作
        >
          <Image
            src="/img/homepage/slider-skip.svg"
            alt="Previous button"
            height="48"
            width="48"
          />
        </button>
        <Swiper
          modules={[Navigation, A11y]}
          loop={isClient} // 仅客户端启用循环
          spaceBetween={20}
          slidesPerView={3}
          breakpoints={{
            320: { slidesPerView: 1 },
            540: { slidesPerView: 2 },
            768: { slidesPerView: 3 },
            1100: { slidesPerView: 4 },
          }}
          onBeforeInit={(swiper) => {
            if (isClient) {
              swiperRef.current = swiper;
            }
          }}
          style={{ marginLeft: "1.3rem", marginRight: "1.3rem" }}
        >
          <SwiperSlide>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlide>
          <SwiperSlide>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlide>
          <SwiperSlide>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlide>
          <SwiperSlide>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlide>
        </Swiper>
        <button 
          onClick={() => swiperRef.current?.slideNext()}
          disabled={!isClient}
        >
          <Image
            src="/img/homepage/slider-skip.svg"
            alt="Next button"
            height="48"
            width="48"
            className="scale-x-[-1]"
          />
        </button>
      </div>
    </div>
  );
}

方案2:启用Swiper官方SSR兼容

Swiper React组件内置了ssr属性,用于优化SSR环境下的渲染一致性,直接添加该属性即可尝试修复:

<Swiper
  ssr={true} // 开启SSR兼容模式
  modules={[Navigation, A11y]}
  loop={true}
  spaceBetween={20}
  slidesPerView={3}
  // 其他配置项...
>

方案3:动态导入Swiper并匹配占位结构

如果上述方案无效,可通过动态导入Swiper组件,同时在服务端渲染时输出与客户端初始结构一致的占位内容,确保前后DOM匹配:

import { useEffect, useState, useRef } from 'react';
import Image from 'next/image';

type Props = {
  className?: string;
  title: string;
};

export default function Slider({ className, title }: Props): JSX.Element {
  const [SwiperComponent, setSwiperComponent] = useState<typeof import('swiper/react').Swiper | null>(null);
  const [SwiperSlideComponent, setSwiperSlideComponent] = useState<typeof import('swiper/react').SwiperSlide | null>(null);
  const swiperRef = useRef<any>(null);

  useEffect(() => {
    const loadSwiper = async () => {
      // 动态导入Swiper及依赖模块
      const { Swiper, SwiperSlide } = await import('swiper/react');
      const { Navigation, A11y } = await import('swiper');
      await import('swiper/css');
      await import('swiper/css/navigation');
      await import('swiper/css/a11y');

      (await import('swiper')).default.use([Navigation, A11y]);
      setSwiperComponent(Swiper);
      setSwiperSlideComponent(SwiperSlide);
    };
    loadSwiper();
  }, []);

  // 服务端/加载中显示占位结构,与客户端初始DOM一致
  if (!SwiperComponent || !SwiperSlideComponent) {
    return (
      <div className={className}>
        <h3 className="pb-8 text-center font-primary text-xl md:text-left">
          {title}
        </h3>
        <div className="flex flex-row items-center">
          <button disabled>
            <Image
              src="/img/homepage/slider-skip.svg"
              alt="Previous button"
              height="48"
              width="48"
            />
          </button>
          <div style={{ marginLeft: "1.3rem", marginRight: "1.3rem", display: 'flex', gap: '20px', flex: 1, overflow: 'hidden' }}>
            {[1,2,3].map(i => (
              <div key={i} className="h-44 w-full rounded-md bg-primary-400 flex-shrink-0">
                <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                  Let's make up
                </p>
              </div>
            ))}
          </div>
          <button disabled>
            <Image
              src="/img/homepage/slider-skip.svg"
              alt="Next button"
              height="48"
              width="48"
              className="scale-x-[-1]"
            />
          </button>
        </div>
      </div>
    );
  }

  // 客户端渲染完整Swiper组件
  return (
    <div className={className}>
      <h3 className="pb-8 text-center font-primary text-xl md:text-left">
        {title}
      </h3>
      <div className="flex flex-row items-center">
        <button onClick={() => swiperRef.current?.slidePrev()}>
          <Image
            src="/img/homepage/slider-skip.svg"
            alt="Previous button"
            height="48"
            width="48"
          />
        </button>
        <SwiperComponent
          ref={swiperRef}
          modules={[Navigation, A11y]}
          loop={true}
          spaceBetween={20}
          slidesPerView={3}
          breakpoints={{
            320: { slidesPerView: 1 },
            540: { slidesPerView: 2 },
            768: { slidesPerView: 3 },
            1100: { slidesPerView: 4 },
          }}
          style={{ marginLeft: "1.3rem", marginRight: "1.3rem", flex: 1 }}
        >
          <SwiperSlideComponent>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlideComponent>
          <SwiperSlideComponent>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlideComponent>
          <SwiperSlideComponent>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlideComponent>
          <SwiperSlideComponent>
            <div className="relative h-44 w-full rounded-md bg-primary-400">
              <p className="absolute bottom-4 left-4 font-primary text-base font-semibold text-white">
                Let's make up
              </p>
            </div>
          </SwiperSlideComponent>
        </SwiperComponent>
        <button onClick={() => swiperRef.current?.slideNext()}>
          <Image
            src="/img/homepage/slider-skip.svg"
            alt="Next button"
            height="48"
            width="48"
            className="scale-x-[-1]"
          />
        </button>
      </div>
    </div>
  );
}

方案选择建议

  1. 优先尝试方案2,仅需添加一个属性,成本最低;
  2. 若方案2无效,使用方案1,逻辑简单且能有效避免DOM差异;
  3. 方案3适合复杂场景,确保服务端与客户端初始结构完全一致,无Hydration风险。

以上方案均保留SSR渲染,不会出现组件加载延迟问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 04:55:23