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

Nuxt3中<client-only>解决初始布局闪烁问题的原因探究

为什么用<client-only>能解决页面初始加载的布局闪烁问题?

问题背景

在default.vue中,我们用<client-only>组件包裹了所有内容,如果不这么做,页面初始加载时会出现布局未完全构建、部分内容短暂闪烁的现象。结合代码来看,核心原因和Nuxt的SSR(服务端渲染)机制以及代码里的浏览器环境依赖有关。

原因解析

  1. Nuxt SSR的特性
    Nuxt默认会执行服务端渲染:服务端先生成页面的HTML并返回给浏览器,浏览器拿到HTML后会先渲染这个"初始快照",同时加载客户端JS,之后再进行hydration(激活)过程,把静态HTML变成可交互的Vue应用。

  2. 代码里的浏览器环境依赖导致布局不匹配
    你的代码里存在依赖浏览器专属API的逻辑:

  • handleResize函数依赖window.innerWidth判断屏幕宽度,进而设置shouldShowDrawer和mobileLayout的值
  • 这些变量的初始值都是false,而它们的正确值要等到客户端的onMounted钩子执行时才会计算更新

服务端渲染时根本没有window对象,所以服务端生成的HTML是基于变量初始false值渲染的——比如不会显示侧边栏(SideMenu)、不会显示移动端TOC(ResponsiveTableOfContents)。当浏览器加载完客户端JS后,onMounted执行,handleResize计算出正确的屏幕宽度并更新变量,Vue会立刻重新渲染DOM,补上之前缺失的侧边栏、TOC,这就导致了布局突变的闪烁。

  1. <client-only>的作用
    <client-only>是Nuxt提供的组件,核心功能是:让包裹的内容完全跳过服务端渲染,只在客户端执行渲染。

这样一来,服务端返回的HTML里不会包含<client-only>包裹的内容,浏览器加载完客户端JS后,会直接根据当前浏览器的window.innerWidth计算出正确的shouldShowDrawer和mobileLayout值,渲染出符合当前屏幕尺寸的完整布局——从一开始就是正确的布局,自然不会出现先渲染服务端"错误快照"再替换的闪烁问题。

相关代码

<script setup lang="ts">
import { inject } from 'vue';

const route = useRoute();
const { baseUrl } = useRuntimeConfig().public;
const { locale } = useI18n();
const shouldShowDrawer = ref(false);
const mobileLayout = ref(false);

const handleResize = () => {
  shouldShowDrawer.value = window.innerWidth >= 1023;
  mobileLayout.value = window.innerWidth <= 1023;
};

onMounted(() => {
  window.addEventListener('resize', handleResize);
  handleResize();
  console.log('mobileLayout', mobileLayout.value);
});

onUnmounted(() => {
  window.removeEventListener('resize', handleResize);
});

let redirectPath = route.path.endsWith('/')
  ? route.path.slice(0, -1)
  : route.path;

if (redirectPath.startsWith('/en')) {
  locale.value = 'en';
} else if (redirectPath.startsWith('/ja')) {
  locale.value = 'ja';
}

if (process.client) {
  switch (redirectPath) {
    case '':
      navigateTo(redirectPath + '/ja', { replace: true });
      break;
  }
}

let ogLocale = '';
if (redirectPath.startsWith('/ja')) {
  ogLocale = 'ja_JP';
} else if (redirectPath.startsWith('/en')) {
  ogLocale = 'en_US';
}

useSeoMeta({
  ogLocale,
  ogUrl: baseUrl + route.path,
  ogType: 'website',
  ogSiteName: '',
  ogImage: baseUrl + '/images/social-image.jpg',
  ogImageWidth: 1200,
  ogImageHeight: 627,
  ogImageType: 'image/png',
  twitterCard: 'summary_large_image',
  twitterImage: baseUrl + '/images/social-image.jpg',
  twitterSite: '',
});
const mobileDrawer = ref(false);
watch(mobileDrawer, (newVal, oldVal) => {
  console.log('mobileDrawer changed from', oldVal, 'to', newVal);
});
function handleMobileDrawerClicked(newValue: boolean) {
  mobileDrawer.value = newValue;
  console.log('変更');
}
</script>

<template>
  <client-only>
    <v-app>
      <div v-if="mobileDrawer" class="responsive-side-menu-wrapper">
        <ResponsiveSideMenu></ResponsiveSideMenu>
      </div>
      <Header
        :mobileDrawer="mobileDrawer"
        @mobileDrawerClicked="handleMobileDrawerClicked"
      />

      <div class="responsive-toc-wrapper" v-if="mobileLayout">
        <ResponsiveTableOfContents></ResponsiveTableOfContents>
      </div>
      <v-container>
        <div class="d-flex flex-0-1-100">
          <SideMenu v-if="shouldShowDrawer" />
          <v-main
            class="pl-0 pr-0 flex-shrink-1"
            :class="{ 'v-main-border': !mobileLayout }"
          >
            <slot />
            <NavigationLink />
          </v-main>
          <TableofContents v-if="shouldShowDrawer" />
        </div>
      </v-container>
      <Footer />
    </v-app>
  </client-only>
</template>
<style lang="scss">
.v-main-border {
  border-left: 1px solid $border-color-gray;
  border-right: 1px solid $border-color-gray;
}
video {
  width: 100%;
}
// headerの背景色はresponsive-toc-wrapperのbackground-colorに設定されている
header {
  position: sticky !important;
  backdrop-filter: saturate(200%) blur(20px);
}
@media (max-width: 1023px) {
  header {
    background: none !important;
  }
}
.responsive-side-menu-wrapper {
  position: fixed;
  z-index: 1100;
  height: 100%;
}
.responsive-toc-wrapper {
  position: sticky;
  top: 64px;
  z-index: 1000;
  background-color: $bg-color-white !important;
  backdrop-filter: saturate(200%) blur(20px);
  border-bottom: 1px solid $border-color-gray;
}
.v-navigation-drawer {
  z-index: 0 !important;
}
</style>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 06:06:04