服务器与客户端时区不一致导致的水合问题

技术
2025年12月6日
4分钟阅读

本文介绍了nextjs中服务器与客户端时区不一致导致的水合问题以及解决方案

nextjs问题记录

问题描述

页面发生报错

image.png

官方错误链接

问题代码:

一个展示文章创建时间的组件, date 格式为 yyyy-MM-dd HH:mm:ss

import { Calendar } from "lucide-react";
import { useFormatter } from "next-intl";
import { cn } from "@/lib/utils";

export default function PostDate({
  date,
  className,
}: {
  date: Date;
  className?: string;
}) {
  const format = useFormatter();

  if (!date) {
    return null;
  }
  return (
    <div
      className={cn("text-main-label flex items-center space-x-1", className)}
    >
      <Calendar />
      <span>
        {format.dateTime(new Date(date), {
          year: "numeric",
          month: "short",
          day: "numeric",
        })}
      </span>
    </div>
  );
}

format.dateTime 是国际化框架 next-intl 的格式化时间函数,主要作用是根据不同的地区展示不同的时间格式

e.g.

zh 地区展示 2025年12月4日

en 地区展示 Dec 4, 2025

具体参考 Date and time foramtting

问题原因

代码的服务器部署在西5区,访问代码的浏览器在东8区

代码中 new Date(date) 在服务端渲染时根据西5区的时区取得时间对象,在浏览器访问时根据东8区的时区取得时间对象

在通过 format.dateTime 方法取得年月日时,采用的是 UTC 时区,不同时区的时间对象返回的年月日不同,造成服务端的渲染结果与客户端的渲染结果不一致

🌰 举个例子

const time = '2025-11-15 10:00:00'

// 东8时区
// Sat Nov 15 2025 06:00:00 GMT+0800 (中国标准时间)
const east_time = new Date(time)
// 采用 UTC 计时,转换为 2025-11-14T22:00:00Z
// 获取年月日:2025-11-14
const east_date = format.dateTime(east_time)

// 西5时区
// Sat Nov 15 2025 06:00:00 GMT-0500 (纽约标准时间)
const west_date = new Date(date)
// 采用 UTC 计时,转换为 2025-11-15T11:00:00Z
// 获取年月日:2025-11-15
const west_date = format.dateTime(east_time)

// west_date !== east_date

解决方案

有两种解决方案,这里推荐第二种

简单方案

通过 useEffect 等前端hook强行让组件跳过服务端渲染,直接在前端渲染

'use client'

import { Calendar } from "lucide-react";
import { useFormatter } from "next-intl";
import { cn } from "@/lib/utils";

export default function PostDate({
  date,
  className,
}: {
  date: Date;
  className?: string;
}) {
  const format = useFormatter();
  const [dateFormat, setDateFormat] = useState(null)
  
  useEffect(() => {
	  setDateFormat(() => {
		  return format.dateTime(new Date(date), {
         year: "numeric",
         month: "short",
         day: "numeric",
       })
	  })
  },[date])

  if (!date) {
    return null;
  }
 
  return (
    <div
      className={cn("text-main-label flex items-center space-x-1", className)}
    >
      <Calendar />
      <span>
        {dateFormat}
      </span>
    </div>
  );

前端渲染容易造成闪烁问题,不推荐

带时区展示

问题的根本在于时区不同导致的展示区别,所以将时区固定就好了

首先保证传入的 date 携带时区,如 2025-11-15T10:00:00+08:00 或直接使用时间戳这样保证获取的时间对象是相同的绝对时间

然后在展示时传入时区参数,保证获取的时间一样

import { Calendar } from "lucide-react";
import { useFormatter, useTimeZone } from "next-intl";
import { cn } from "@/lib/utils";

export default function PostDate({
  date,
  className,
}: {
  date: string;
  className?: string;
}) {
  const format = useFormatter();
  const timeZone = useTimeZone();

  if (!date) {
    return null;
  }
  return (
    <div
      className={cn("text-main-label flex items-center space-x-1", className)}
    >
      <Calendar />
      <span>
        {format.dateTime(new Date(date), {
          year: "numeric",
          month: "short",
          day: "numeric",
          timeZone,
        })}
      </span>
    </div>
  );
}

我的项目中有使用国际化,所以直接根据当前的国家获取时区了,也就是 useTimeZone 方法

如果没有使用国际化,dayjs、moment 中都有根据时区获取格式化时间的方法,这里就不赘述