Скелетоны и placeholder'ы

Скелетоны и placeholder’ы представляют собой ключевой механизм для улучшения пользовательского опыта при асинхронной загрузке данных в приложениях на базе TanStack Router. Они позволяют отображать временные элементы интерфейса до получения фактического контента, снижая ощущение “зависания” страницы и повышая отзывчивость приложения.


Асинхронные маршруты и необходимость placeholder’ов

TanStack Router поддерживает асинхронные маршруты через загрузку данных в loader-ах. При этом компонент маршрута может отображаться до завершения загрузки данных. Без скелетонов пользователь видит пустую страницу или стандартный индикатор загрузки, что негативно сказывается на UX.

import { createRouter, createRouteConfig } from '@tanstack/router'

const routeConfig = createRouteConfig()
  .createRoute({
    path: '/profile',
    component: ProfilePage,
    loader: async () => {
      const user = await fetch('/api/user').then(res => res.json())
      return { user }
    },
  })

const router = createRouter({ routeConfig })

В этом примере ProfilePage рендерится после завершения loader. Если загрузка данных занимает несколько секунд, пользователь видит пустое пространство. Решением служат скелетоны.


Создание скелетонов

Скелетон — это компонент, имитирующий форму будущего контента. В TanStack Router можно реализовать скелетон внутри маршрута, используя Suspense и React.lazy или отдельный компонент placeholder’а:

function ProfileSkeleton() {
  return (
    <div className="profile-skeleton">
      <div className="avatar-placeholder" />
      <div className="line-placeholder short" />
      <div className="line-placeholder long" />
    </div>
  )
}

Ключевые моменты при проектировании скелетонов:

  • Использовать простые формы, повторяющие структуру будущего контента.
  • Сохранять пропорции и размеры элементов, чтобы избежать “дергания” интерфейса при подгрузке данных.
  • Минимизировать визуальные эффекты для ускорения рендеринга.

Интеграция placeholder’ов в маршруты

TanStack Router предоставляет возможность отображать временный контент до завершения загрузки loader через Suspense:

import { RouterProvider } from '@tanstack/router'
import { Suspense } from 'react'

function App() {
  return (
    <RouterProvider router={router}>
      <Suspense fallback={<ProfileSkeleton />}>
        <ProfilePage />
      </Suspense>
    </RouterProvider>
  )
}

Особенности подхода:

  • Suspense блокирует отображение основного компонента до завершения промиса, заданного в loader.
  • fallback рендерит скелетон, позволяя пользователю видеть визуальный каркас страницы.

Placeholder’ы для отдельных частей страницы

Для сложных страниц с множеством секций имеет смысл использовать локальные placeholder’ы, а не один глобальный скелетон. Это повышает отзывчивость интерфейса:

function ProfileDetails({ user }) {
  return (
    <div>
      <Suspense fallback={<LineSkeleton />}>
        <UserBio user={user} />
      </Suspense>
      <Suspense fallback={<LineSkeleton />}>
        <UserPosts userId={user.id} />
      </Suspense>
    </div>
  )
}

Преимущества локальных placeholder’ов:

  • Пользователь видит прогресс загрузки отдельных частей страницы.
  • Позволяет асинхронно подгружать данные для разных секций без задержки всего интерфейса.
  • Облегчает создание анимаций скелетонов для каждой секции.

Динамические скелетоны и условные placeholder’ы

TanStack Router поддерживает условные рендеры скелетонов на основе состояния данных. Например, если часть данных уже загружена, можно отобразить скелетон только для оставшихся частей:

function UserPostsSection({ posts }) {
  if (!posts) {
    return <PostsSkeleton count={5} />
  }
  return <PostsList posts={posts} />
}

Рекомендации:

  • Скелетоны должны занимать место контента, чтобы не менять layout при загрузке.
  • Использовать анимацию shimmer или плавное изменение цвета для реалистичного эффекта загрузки.
  • Избегать сложной логики внутри placeholder’ов — их задача только визуальная.

Скелетоны и производительность

Несмотря на визуальные преимущества, чрезмерное использование скелетонов может замедлить рендеринг, особенно на мобильных устройствах. Оптимизация включает:

  • Минимизация DOM-элементов внутри placeholder’ов.
  • Использование CSS-анимаций вместо JavaScript.
  • Комбинирование глобальных и локальных скелетонов для больших страниц.

Взаимодействие с кешированием

TanStack Router позволяет кэшировать результаты loader. Это означает, что при навигации между маршрутами можно отображать скелетоны только при первой загрузке данных:

const routeConfig = createRouteConfig()
  .createRoute({
    path: '/profile',
    component: ProfilePage,
    loader: async () => {
      return await fetchUserData()
    },
    onLoadCache: (data) => data, // повторное использование данных без скелетона
  })

Такой подход сокращает количество визуальных переходов между скелетоном и реальным контентом, создавая плавный UX.


Практика: комплексный пример

Комбинируя глобальные и локальные скелетоны, можно построить страницу профиля с несколькими асинхронными секциями:

function ProfilePage() {
  const { user } = useLoaderData()

  return (
    <div className="profile-page">
      <Suspense fallback={<ProfileHeaderSkeleton />}>
        <ProfileHeader user={user} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton count={3} />}>
        <UserPosts userId={user.id} />
      </Suspense>
      <Suspense fallback={<FriendsSkeleton count={4} />}>
        <UserFriends userId={user.id} />
      </Suspense>
    </div>
  )
}

Каждая секция загружается независимо, а пользователь видит постоянный визуальный каркас, что значительно улучшает восприятие скорости работы приложения.


Скелетоны и placeholder’ы в TanStack Router — это не просто декоративный элемент, а инструмент для управления асинхронностью и отзывчивостью интерфейса, обеспечивающий плавную и предсказуемую загрузку данных.