Галоўная / Артыкулы

ViewTransition: спосабы адраджэння, зображэнні, шрыфты для анімавання напругі

Дазвольце дазнацца, як викорыстоўванне Suspense ў ViewTransition анімавае замену скелета на контэнт, якія пропсы запобегаюць зайвым анімацыям, і як React чакае на завершэнне загрузкі зяўроцэў і шрыфтав.

Выдавец: REACTAPP.TOP14 хв чытання3151 слоў

Крыніцы: React 19.3 View Transitions with Suspense: Animate the Fallback, Wait for Images and Fonts

Межа напэўнення ўжо описвае пераход: яна паказвае альтернатыўны варіант, а пасля — заменяе його на справжній контэнт. У працоўным режыме такая замена выглядала як рэзкая пераскок, часта паследаваная показам адпаведнага зображэння за момант пазней і запускам веб-шрыфта. React 19.3 дазволяе абгорнуць такую межу ў <ViewTransition>, тады замена стае анімацыяй, і, хоць гэта менш зразумела, React адкладае ей выпаленне пакуль зображэнняі і веб-шрыфты ў новым контэнце не будуць завантажаны. У гідзе показана, як это настроіць, як запобiec запуску анімацыі тады, калі гэта не патрэбна, дзе размешчаць такія межы і што на самай працоўцы робяць вбудованыя механізмы чакання.

Пакяк гэтае поведанне было адагрувана не так давна, на момент напісання, перад тым, як паслявацца на точныя часы ў працоўнай среде, пакажчыце деталі ў актуальной даследжвацці React. У прыкладах предполагаецца, што вы вялікі час ведаеце, што <ViewTransition> анімаваецца пад час спецыяльных сігналоў (enter, exit, update) і што ён павінен абгортваць DOM, які ён анімавае.

Ператварэнне праказвання ў анімацыю

Мінімальная версія проста размешчае <ViewTransition> абоўсюды навакола межы Suspense:

<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Forecast city={city} />
  </Suspense>
</ViewTransition>

Для замены не трэба викорыстоўваць startTransition, адколькі React вяліка частка часу спрыяе тому, што адкрыцья межы Suspense спрацоўвае як пераход. Калі Forecast завершыць процес suspending, камант, які заменяе скелет, будзе выконаны ў рамках пераходу відображэння. Паколькі скелет і прогноз знаходзяцца ўнутрь той самай структуры <ViewTransition>, яны дзеляць аднаковае імёна скріншота, таму змены спрацоўваюць як update, і браузер плавна пераходыць з аднаго стану на іншы.

Прыклад працюючага прогназу

Дэман стартуе з неабходных імпортаў:

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';

Код компаненту зберагае адну Promise на кожны горад у Map, таму што use() павінен прыймац тую ж інстанцыю Promise праз усі перзапускі; стварэнне новай інстанцыі ў кожны раз спрычыняла б безканечнае чаканне. Forecast чытае вырашаныя данні за дапамою use(), Skeleton атрыбутуе картку-заменік, а App переключае весь фон у startTransition:

// use() needs the same Promise instance across renders, so cache it
const cache = new Map();
function fetchForecast(city) {
  if (!cache.has(city)) {
    cache.set(city, new Promise(resolve =>
      setTimeout(() => resolve({ city, high: 31, low: 24, sky: '⛅' }), 1100)
    ));
  }
  return cache.get(city);
}function Forecast({ city }) {
  const f = use(fetchForecast(city));
  return <div className="card">{f.sky} {f.city}: {f.high}° / {f.low}°</div>;
}function Skeleton() {
  return <div className="card skeleton">Loading forecast…</div>;
}export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => startTransition(() => setShow(s => !s))}>
        {show ? 'Hide' : 'Load'} forecast
      </button>
      {show && (
        <ViewTransition>
          <Suspense fallback={<Skeleton />}>
            <Forecast city="Pune" />
          </Suspense>
        </ViewTransition>
      )}
    </>
  );
}

Калькі правіла CSS стылююць картку і надаюць скелету нейтральны фон, выкарыстоўваючы light-dark() для обох схем кольораў:

.card { margin-top: 12px; padding: 14px 16px; border-radius: 10px; background: light-dark(#e0f2fe, #0c3350); }
.skeleton { color: transparent; background: light-dark(#e2e8f0, #2a2f3a); }

Пасляднее правіла спам’чвае стандартны эфект плавнага пераходу, ўнаследке чаго змены легка можна пазначыць пад час експерыментаў:

/* slow the default cross-fade so the swap is visible */
::view-transition-old(*), ::view-transition-new(*) { animation-duration: 450ms; }

Прыжок па кантакту з кнопкай паказвае скелет, чакае прыблызна секунду на сімуляванне запрашоўкі дадзеных, а пасля поступова паказвае прогноз на фоне скелета. Аднак, як толькі ўсё гэта сховаць і зноў запрацаваць, можна заазначыць проблэму: дадзеныя тады зберагаюцца у кэшы, таму не паказваецца жаданая альтернатыва, пры тым карточка такі ж поступова паказваецца, як і знікала калі была схована. Гэта ведае да наступнага раздзела.

Зупінэнне анімазыяў, якіх вы не прасілі

<ViewTransition> без якіх-леба атрыбутаў анімавае кожны момент запуску: enter пад час стварэння, exit пад час зничынення і update кожны раз, калі змянюецца контэнт усередзіне. Калі гэта викорыстоўваць разам з Suspense, то можа выйсці аж тры анімазыі, хоця планавалася толькі адна. У дакументацыі React чытальна описана бажаная працэса загрузкі UI:

  • Альтэрнатыўны варіант павінен апыявіцца негараздо і без анімэйшна, таму што корыстнік яе ўжо выкарыстаў і чакае мгновеннай адпаведзі.
  • Пераход з альтэрнатыўнага варіанта на справжній контэнт павінен адбывацца з анімэйшнам.
  • Контэнт, які вялікі час зберагаецца у кэшы і таму ніколи не перывае свою роботу, павінен апыявіцца негараздо і без анімэйшна.

Два параметры точна выражаюць гэта. default="none" выключае ўсі трыгеры, а update="auto" зноў увялічвае тыя, якія вам патрэбны:

<ViewTransition update="auto" default="none">
  <Suspense fallback={<Skeleton />}>
    <Forecast city={city} />
  </Suspense>
</ViewTransition>

Порэванне двух канфігурацый

У другой дэманстрацыі викорыстоўваюцца тыя ж імпорты:

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';

Ён дазваляе заваносіць адна з трох цяргоў і перыходзіць межы простаг <ViewTransition> і версіі з двума параметрамі. Зверніце атлегледзе на key={city} у <ViewTransition>: пад час змены цяргоў ствараецца новая межа заместо апдэйту існуючай.

const cache = new Map();
function fetchForecast(city) {
  if (!cache.has(city)) {
    const data = { Pune: '⛅ 31° / 24°', Goa: '🌧 29° / 26°', Shimla: '☀️ 18° / 9°' }[city];
    cache.set(city, new Promise(r => setTimeout(() => r(data), 1100)));
  }
  return cache.get(city);
}function Forecast({ city }) {
  const text = use(fetchForecast(city));
  return <div className="card">{city}: {text}</div>;
}export default function App() {
  const [city, setCity] = useState(null);
  const [onlyUpdates, setOnlyUpdates] = useState(false);
  const props = onlyUpdates ? { update: 'auto', default: 'none' } : {};  return (
    <>
      <div className="bar">
        {['Pune', 'Goa', 'Shimla'].map(c => (
          <button key={c} onClick={() => startTransition(() => setCity(city === c ? null : c))}>
            {city === c ? 'Hide' : 'Load'} {c}
          </button>
        ))}
        <label><input type="checkbox" checked={onlyUpdates} onChange={e => setOnlyUpdates(e.target.checked)} /> only animate updates</label>
      </div>
      {city && (
        <ViewTransition key={city} {...props}>
          <Suspense fallback={<div className="card skeleton">Loading {city}…</div>}>
            <Forecast city={city} />
          </Suspense>
        </ViewTransition>
      )}
    </>
  );
}

Шаблон стылю дадае гнучкую паліцу інструментоў, а таксама тую ж картку, каркас і правілы сповальнага переходу:

.bar { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
.card { margin-top: 12px; padding: 14px 16px; border-radius: 10px; background: light-dark(#e0f2fe, #0c3350); }
.skeleton { color: transparent; background: light-dark(#e2e8f0, #2a2f3a); }
::view-transition-old(*), ::view-transition-new(*) { animation-duration: 450ms; }

Калі увімкнута опцыя „толькі анімацыйныя апдэйты“, горад, які ўсё ўтолькі не завантажыўся, адразу паказвае свой каркас, а потым пераходзіць у стан з анімацыяй, тады як горад, які вже завантажыўся, проста паказваецца без анімацыі. У дасведчальных матэрыялах рекамендуецца викорыстоўваць анімацыю пад час паказу элементаў Suspense толькі эпізодычна, а таксама не викорыстоўваць яе для кэшаванага контэнту, які мог бы буць адразу візуалізаваны, таму што анімацыя толькі дадае затрымку. На практыцы параметры update="auto" default="none" павінны быць настаўлены для майже кожного элемента <ViewTransition>, які абгортае межы Suspense.

Распаложэнне вялічына тое, якіе тригеры будуць працаваць

Месца, дзе розмішаны элементы <ViewTransition> у стосунку да Suspense, вялічына тое, якіе трыгеры будуць запускацца, і такім чынам якія частыні можна анімаваць окрема.

За межамі: адна апдэйт

Адаптаванне напэўнення ззаўні стварае адна межа, якая спрыймае змены як update:

<ViewTransition update="cross">
  <Suspense fallback={<A />}>
    <B />
  </Suspense>
</ViewTransition>

Фаллбэк і контент дзеляць адну назву кадру, таму ўсё, што можна зробіць, — градуальна пераходзіць ад однаго да другога, у даным случае за дапамогою спецыяльнага класу cross.

У обох ветках: выход і вхід

Разныя адаптаванні фаллбэка і контента ствараюць две незалежныя межы:

<Suspense
  fallback={<ViewTransition exit="out"><A /></ViewTransition>}
>
  <ViewTransition enter="in"><B /></ViewTransition>
</Suspense>

Фаллбэк выходзіць з анімацыяй exit, а контент прыходзіць з анімацыяй enter; гэта дазволяе падменніку спускацца вярху, а справжній контент падымацца на свае месца.

Карточка прафілю з внутрашнім розташоўванням

Трэці дэмонстрацыйны прыклад знова пачынаецца з імпортаў:

import { ViewTransition, Suspense, use, useState, startTransition } from 'react';

loadProfile стварае новы Promise з кожнага кліка, таму механізм Suspense настаўляеся зноў і зноў. Кожны з элементаў Profile і Skeleton абгортае свой маркап у саўсем іншы элемент <ViewTransition>, выкарыстоўваючы значэння enter="rise" і exit="sink" адпаведна, з параметрам default="none". Значэнне key={attempt} у механізме Suspense мае важлівэ значэнне: яно стварае новы механізм для кожнага завантажэння, таму ўжо існуючы механізм, які зноў настаўляеся ўнутрь переходу, продовжвае адображаць пярэйшы контент уместа замены на альтернатыўны.

function loadProfile() {
  // a fresh Promise per click, so it suspends every time
  return new Promise(r => setTimeout(() => r({ name: 'Rahul', role: 'Frontend' }), 1100));
}function Profile({ promise }) {
  const p = use(promise);
  return (
    <ViewTransition enter="rise" default="none">
      <div className="card">👤 {p.name} — {p.role}</div>
    </ViewTransition>
  );
}function Skeleton() {
  return (
    <ViewTransition exit="sink" default="none">
      <div className="card skeleton">loading…</div>
    </ViewTransition>
  );
}export default function App() {
  const [attempt, setAttempt] = useState(0);
  const [promise, setPromise] = useState(null);
  return (
    <>
      <button onClick={() => startTransition(() => { setAttempt(a => a + 1); setPromise(loadProfile()); })}>
        {attempt ? 'Load again' : 'Load profile'}
      </button>
      {promise && (
        // key: a NEW boundary each time. A mounted boundary that re-suspends
        // inside a Transition keeps showing the old content instead of the fallback.
        <Suspense key={attempt} fallback={<Skeleton />}>
          <Profile promise={promise} />
        </Suspense>
      )}
    </>
  );
}

Базовыя стылі надаюць картцы і елементу Skeleton ўпэўненага візуальнага выгляду:

.card { margin-top: 12px; padding: 14px 16px; border-radius: 10px; background: light-dark(#e0f2fe, #0c3350); }
.skeleton { color: light-dark(#64748b, #94a3b8); background: light-dark(#e2e8f0, #2a2f3a); }

Правіла анімацыі нацэлены на класы з іменамі. ::view-transition-old(.sink) прымаеся для выходнага скэнару канструкцыі, а ::view-transition-new(.rise) — для входнага кантэнту; кожны з іх мае своія ключовыя кадры:

::view-transition-old(.sink) { animation: 240ms ease-in  both sink; }
::view-transition-new(.rise) { animation: 320ms ease-out both rise; }
@keyframes sink { to   { transform: translateY(16px);  opacity: 0; } }
@keyframes rise { from { transform: translateY(-16px); opacity: 0; } }

Чаканне на зображэння, шрыфты і таблицы стылю

Механізм Suspense давно стрымляў обробку дадзэнняў і коду, які завантажаецца з адзячэнням. У React 19.3 колькасць элементаў, якія стрымляецца механізам Suspense, зрасла, калі анімацыя паказвання ведаецца за дапамогою <ViewTransition>:

  • Дадзеныя, чытаныя за дапамогою use() і компонентаў з адзячэнням: чакаецца ў обох случаях.
  • Таблицы стылю, заданыя за дапамогою <link rel="stylesheet" precedence="…">: чакаецца ў обох случаях (з таймаутам для звычайнага Suspense).
  • Шрыфты, якія вводзяцца новым кантэнтам: ігнаруецца простым Suspense; чакаецца з пярэдзейкамі часу пад час апдэйта пераходу межаў.
  • Відразумелыя элементы <img>: ігнаруецца простым Suspense; пад час апдэйта пераходу межаў React чакае на декодаванне прыблізна 800 мс, за выключэнням атракцыяў, якія викорыстоўваюць loading="lazy" або маюць працэс onLoad.
  • Прычына — візуальны аспект. Анімацыя ў карточку, чыё фота падзеўляе толькі часткова, або чыё тэкст заставаецца ў шрыфце системы, выглядае гэрш за просты пераход. Таму React адкладае паказанне элемента пад час пераходу межаў, пакуль фоты і новыя шрыфты ў той межы не стануць доступнымі. Без пераходу межаў нічога не змяніцца, і атракцыі будуць паказвацца ў той момент, калі яны прыбудуць.

    Як дзяўно React готавы чакаць

    Час чакання є обмежаным. Гэтыя деталі выведзены з адпрацоўкі react-dom, а не з дасягледжэнняў, таму трэба спрыятаць іх як деталі адпрацоўкі, якія могу зменіцца:

    • Для адобразаў React вызывае img.decode() і затрымляе ўваходжэнне дадзеных на максымум на 800 мс.
    • Таксама ён ацэнюе, насколькі важкі кожны адобраз за счытаннем яго атрыбутаў width і height, а таксама насколькі шырока мережа, скарыстаючыся данымі з Resource Timing. Калі такая ацэнка паказвае, што завантажэнне займе больш чым працоўна 500 мс, React надае толькі 50 мс і пасля гэтага адобраз будзе адразу адобразаваны.
    • Без атрыбутаў width і height ацэнка прыпускае, што размеры адобраза 100×100, таму рэальныя размеры робяць ацэнку значымай.
    • Для шрыфтаў ліміт станавіць працоўна 500 мс.

    Дэманстрацыя чакання на адобраз

    У дэманстрацыі адобраза імпортуецца калькі дапаможных хуков:

    import { ViewTransition, Suspense, use, useState, useRef, useLayoutEffect, startTransition } from 'react';
    

    Кожны клік запрашоўвае елемент з URL фота, узятым адносна наэнтрыпаваным, таму зображэнне ніколі не кешуецца. Два элементы адображаюць той самы карточкі па боку ад боку. Левы элемент дазволяе React чакаць на фота; правы элемент прыўязвае порожніяй обрабнік onLoad, якай ігноруе зображэнне, таму карточкі адразу стае виднымі, а фота прыходзіць пазней. Функцыя useLayoutEffect фіксуе стан img.complete у момент стварэння кожной карточкі, тады можна пабачыць разліку на швайнай мережы, дзе гэта важка зазначыць.

    function fetchListing() {
      return new Promise(r => setTimeout(() => r({
        title: 'Sea-facing 2BHK, Bandra',
        // a new seed every time → a fresh download every time
        photo: `https://picsum.photos/seed/${Math.random().toString(36).slice(2)}/600/300`,
      }), 300));
    }function Listing({ promise, optOut, label }) {
      const l = use(promise);
      const img = useRef(null);
      // runs at the moment the content is committed — was the photo already there?
      useLayoutEffect(() => {
        console.log(`${label}: photo decoded before reveal → ${img.current.complete}`);
      }, [label]);
      return (
        <div className="card">
          <img
            ref={img}
            src={l.photo}
            width={600}
            height={300}
            alt=""
            onLoad={optOut ? () => {} : undefined}
          />
          <strong>{l.title}</strong>
        </div>
      );
    }function Slot({ label, optOut }) {
      const [promise, setPromise] = useState(null);
      return (
        <div>
          <button onClick={() => startTransition(() => setPromise(fetchListing()))}>{label}</button>
          {promise && (
            <ViewTransition update="auto" default="none">
              <Suspense fallback={<div className="card skeleton"><div className="ph" /><strong>loading…</strong></div>}>
                <Listing promise={promise} optOut={optOut} label={label} />
              </Suspense>
            </ViewTransition>
          )}
        </div>
      );
    }export default function App() {
      return (
        <div className="two">
          <Slot label="Wait for photo" optOut={false} />
          <Slot label="onLoad opt-out" optOut={true} />
        </div>
      );
    }
    

    Стайлі аранжуе два столбцы і зберагаюць співвядэнне сторон зображэння як у карточцы, так і ў місцэвай замене:

    .two { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    .card { margin-top: 10px; padding: 10px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
    .card img, .ph { display: block; width: 100%; height: auto; aspect-ratio: 2 / 1; border-radius: 6px; object-fit: cover; margin-bottom: 8px; }
    .ph { background: light-dark(#e2e8f0, #363b47); }
    .skeleton strong { color: light-dark(#64748b, #94a3b8); }
    ::view-transition-old(*), ::view-transition-new(*) { animation-duration: 450ms; }
    

    Шрыфты як ресурсы для трэйсування

    Шрыфты працуюць за тым жа принцыпам. Секрэт заключаецца у тым, каб задаць правілу @font-face у элементе <style> з атрыбутамі href і precedence; гэта паведамляе React прыняць яго і адстаўляць як ресурс, на які можна чакаць:

    <ViewTransition update="auto" default="none">
      <Suspense fallback={<QuoteSkeleton />}>
        <style href={fontUrl} precedence="default">
          {`@font-face {
            font-family: 'Quote';
            src: url(${fontUrl}) format('woff2');
            font-display: swap;
          }`}
        </style>
        <blockquote style={{ fontFamily: 'Quote' }}>{quote}</blockquote>
      </Suspense>
    </ViewTransition>
    

    У дасяглівых матэрыялах React ёсць болей дакладны прыклад, у яком даныя, запрашаны з інтэрнету, стайл-шіт для precedence, спецыяльны шрыфт і фотаработ упоруч з аднаковым каркасам показаны разам. Гэта поўнай меры адпавядае большасці рэальных карточак: прафілю, інформацыі пра продукт, паведамленню ў чате з аватарам.

    Як гэта раней выконвалі

    Да гэтага, каб зберагчыць контент пакуль не будзе готова картінка, было неабходна ручная прадзяганне: стварыць об’ект Image, чакаць на сігнал onload, а пасля змяніць стан. Тыповы хук выглядаў так:

    function useImageReady(src) {
      const [ready, setReady] = useState(false);
      useEffect(() => {
        const img = new Image();
        img.onload = () => setReady(true);
        img.src = src;
      }, [src]);
      return ready;
    }
    

    Кожны элемент мела свой сабе копію і ніхто з іх не координаваўся, таму карточка з фота і спецыяльным шрыфтом все равна ап’являлася ў два этапы. Становішча было ўсё горэ, так как для іх викорыстання патрэбна была функція document.fonts.load() плюс спецыяльны флаг, або ж настройка font-display: block, якая таксама спрычыняла момантвеная ап’яву невідразумелага тэксту. Чыраганне элементаў адбывалася шляхом ручнага вызову startViewTransition пасля прыбутку дадзеных, і осколькі гэта відбывалася через flushSync, сама функцыя не магла чакаць нічога.

    Чаму гэта змяненне важліва

    Завантажэнне інтерфейсу стае адной рашынкай замест чатырох. Межа Suspense, яку вы вже выкорыстоўваеце, тепер таксама вялікае, калі дадзеныя вважаюцца гатовымі — ўключаючы даны, код, стылі, шрыфты і з’явы. Анімазія запускаецца толькі адной раз, у тым момант. Эфекты, залежныя ад кожнага ресурсу, зникаюць, так само як і код, які намагаўся іх сінхронізаваць.

    Это таксама дае React можлівасць вплыўа на момент, калі браузер упершыне атрымлівае і засвечвае завантажанае зображэнне. Раней <img> атрымлівалася і засвечвалася як толькі ўспрынамлялася. Паўязка яе з перайходам відносна відображэння стварае эфект „працоўнае зображэнне“ як у натывных дапрыемствах, без неабяжнага ручнага прадзягання.

    Тое, што трэба памятаць

    • Шрыфты і зображэнні чакаюцца толькі падчас апдэйта відносна відображэння. Без <ViewTransition> або з параметрам update="none" зображэнні атрымліваюцца як і раней.
    • Дадзенне обрабнавача onLoad выключае цяперашнюю паўтарную атрымліву зображэння, незалежна ад наяўнасці перайходу відносна відображэння. Гэта корыстна для зображэнняў, якія з’являюцца пад контентам, але можа ствараваць незрозумеласці, якшо такі обрабнавач даданы для аналізу.
  • Ліміты станавяцься прыблызна паў секунды для шрыфтав, 800 мс для аўтарафактов, і 50 мс калі React прыпускае, што аўтарафакт занадта вялік для ныякога сеткавага з’ўязку. Медленны CDN можа нейкі час запаздзіць адобразэнне, але ніколи не зупініць яго.
  • Аўтарафакты, якія знаходзяцца за межамы відобразэння, для гэтых цялей ігнаруюцца.
  • Контэнт, які знаходзіцца ў кешы, за замовчаннем адобразжваецца знову, таму трэба выкарыстоўваць Suspense разам з update="auto" default="none".
  • Калі контэнт розташоўваецца за межамы відобразэння, скелет і контэнт разных высак прымушваюць браузер зменіць размер кадру, што дае эфект візуальнага растягвання. Налепіце размер плацэхолдера на розмер фінальнага контэнту або пераключыцеся на розташаванне ўнутрь, каб оба элементы адобразжваліся самастоятельна.
  • Тыпы перехода сбрасываюцца пасля кожнага коміту, таму типы на кшталт next або back, дадзеныя ў актуальненні, якое паказвало альтернатыўны варыянт, больш не існуюць, калі фактычны контэнт зберагаецца.
  • Альтернатыўны варыянт для старэйшых версій React

    Якщо вы яшчэ не можаце апгрэйдаваць, надзейны спосаб запобець раптовам прадстаўленню зображэння — адносіцца да яго як да часткі таго, што вы завантажаеце: паставіць задачу на чаканне (Promise), якая будзе завершана толькі пасля таго, як прыйдзе JSON і зображэнне будзе декодаванаць:

    function loadListing() {
      return fetch('/api/listing').then(r => r.json()).then(async data => {
        const img = new Image();
        img.src = data.photo;
        await img.decode().catch(() => {});   // don't let a broken image block forever
        return data;
      });
    }
    

    .catch(() => {}) у функцыі decode() гарантуе, што несправная картынка не зможа застаўляць прадставленне контэнту назаўсёды. Вы все раву атрымаете рэзкую замену замест анімацыі, прычым картынка ўжо знаходзіцца ў кэшы на момент адрасавання і прадстаўляецца ў той жа кадр, што і рэшта элемента карткі. Шрыфты можна обрабоцваць аднойчыны, выкарыстаўшы Promise з document.fonts.load('1em Quote').

    Ключовыя моманты

    • Закіранне Suspense ў <ViewTransition> анімавае замену на запасны контэнт без неабяжнае вызову startTransition для прадставлення.
    • Ўжывайце update="auto" default="none", каб запасны і кэшаваны контэнт прадстаўляліся мгновена, а анімавалася толькі рэальная частка контэнту.
  • Ўзайце <ViewTransition> за межамі Suspense для адзінаго плавнага пераходу, або ўнутрь обох галузей для незалежных анімазыяў выходу і входу.
  • Пад час пераходу між відображаннямі React таксама чакае, у строгіх межах, на візуабельныя зображэння і новыя шрыфты, чым усунуць большасць ручнага прадзягання.
  • Задайце зображэнням рэальныя значэнні width і height, і памятайце, што опцыя onLoad выключае ўжыванне гэтых значэнняў.
  • Галоўныя джерэлы: працэсы выдання React 19.3, дапаможнае матэрыялы па Suspense, дапаможнае матэрыялы па ViewTransition і дапаможнае матэрыялы па компоненте стылю.