انتقالات متّسقة بين المستندات

المشكلة

تؤدي تأثيرات الانتقال في العرض على مستوى المستندات إلى تحريك العناصر بين صفحتَين أثناء التنقّل من المصدر نفسه. يلتقط المتصفّح لقطة من الصفحة القديمة، وينتقل إلى الصفحة الجديدة، ثم يعرض حركة من اللقطة إلى الصفحة الجديدة. إذا لم تنتهِ الصفحة الجديدة من تحميل الموارد المهمة، مثل أوراق الأنماط أو نصوص التنسيق البرمجية أو عناصر DOM الرئيسية، ستظهر الحركة الانتقالية في حالة غير مكتملة أو غير منسَّقة. يؤدي ذلك إلى حدوث خلل مرئي، مثل تحوّل العناصر إلى مواضع خاطئة، أو إعادة تدفق المحتوى في منتصف الحركة، أو وميض الخطوط الاحتياطية إلى خطوط الويب بعد اكتمال عملية النقل.

الحل

استخدِم blocking="render" في عناصر <link> و<script> المهمة في <head> الصفحة الجديدة، واستخدِم <link rel="expect"> لحظر العرض إلى أن يتم تحليل عناصر DOM معيّنة. يضمن ذلك عدم بدء المتصفّح في عرض الحركة الانتقالية إلى أن تصبح الحالة المرئية للصفحة الجديدة مستقرة. يواصل المتصفّح تحليل HTML في الخلفية، ويتم تأجيل العرض فقط.

استراتيجية التنفيذ

  1. إجراء إلزامي: فعِّل ميزة "انتقالات العرض بين المستندات" باستخدام قاعدة CSS‏ @view-transition في كلتا الصفحتَين.
  2. إجراء إلزامي: تأكَّد من أنّ أوراق الأنماط الأساسية مضمّنة في <head>. تكون أوراق الأنماط في <head> محظورة العرض تلقائيًا. تتطلّب أوراق الأنماط التي يتم إدخالها ديناميكيًا استخدام blocking="render" بشكل صريح.
  3. يجب استخدام blocking="render" في عناصر <script> التي يجب تنفيذها قبل ظهور تأثيرات الانتقال (مثل النصوص البرمجية التي تطبّق مظهرًا أو تؤثر في التنسيق).
  4. يُرجى استخدام <link rel="expect" href="#element-id" blocking="render"> لحظر العرض إلى أن يتم تحليل المحتوى في الجزء المرئي من الصفحة. ينطبق ذلك على جميع أنواع الانتقالات: التلاشي التدريجي على كامل الصفحة (لتجنُّب الانتقال إلى صفحة فارغة)، والتحويلات (لضمان توفّر العناصر المُسمّاة في نموذج DOM)، والتنسيقات المستندة إلى النصوص البرمجية (لضمان تحليل المحتوى المنسَّق).
  5. لا تحظر العرض على المحتوى غير الضروري. يجب حظر الموارد والعناصر التي تؤثر في إطار العرض الأولي فقط. يؤدي حظر الكثير من المحتوى إلى تأخير عملية الانتقال وتقليل الأداء الملحوظ.

دليل الاستخدام

الخطوة 1: الموافقة على ميزة "انتقالات العرض في مستندات متعددة"

مطلوب: يجب أن تتضمّن كل من صفحة المصدر والوجهة القاعدة @‏ @view-transition. بدون ذلك، لن يحدث أي انتقال بين المستندات.

/*
  MANDATORY: Include this rule in every page that participates
  in cross-document view transitions.
  `navigation: auto` enables transitions for standard navigations
  (link clicks, form submissions, back/forward).
*/
@view-transition {
  navigation: auto;
}

/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

الخطوة 2: حظر العرض إلى أن يتم تنفيذ النصوص البرمجية المهمة

إذا كان يجب تشغيل نص برمجي غير مانع في <head> قبل أن يتم عرض الحركة في الانتقال (على سبيل المثال، لتطبيق فئة مظهر أو التأثير في التنسيق)، ضَع علامة blocking="render" عليه. بدون ذلك، قد يتم تنفيذ نصوص async أو defer أو type="module" البرمجية بعد بدء عملية النقل.

<head>
  <!--
    DO: Mark layout-critical scripts with blocking="render".
  -->
  <script type=module blocking="render">
    // Example: apply a stored theme before the page renders,
    // so the transition snapshot reflects the correct theme.
    document.documentElement.dataset.theme =
      localStorage.getItem('theme') || 'light';
  </script>
</head>

الخطوة 3: حظر العرض إلى أن يتم تحليل عناصر DOM الرئيسية

تضمن أوراق الأنماط ونصوص blocking="render" البرمجية في <head> فقط أنّه تمت معالجة <head> بالكامل. ولا تنتظر تحليل أي محتوى <body>. بدون حظر إضافي، قد يأخذ المتصفّح لقطة الصفحة الجديدة قبل توفّر العناصر في الجزء المرئي من الصفحة في نموذج DOM، ما يؤدي إلى انتقال متحرك إلى صفحة فارغة أو معروضة جزئيًا.

يحلّ <link rel="expect"> هذه المشكلة من خلال حظر العرض إلى أن يتم تحليل عنصر معيّن (يتم تحديده من خلال id). يجب أن تكون قيمة السمة href معرّف جزء (مثل #hero) يتطابق مع السمة id للعنصر المستهدَف. وبعد تحليل علامة الإغلاق لهذا العنصر، يتم إلغاء حظر العرض. يُرجى العِلم أنّ <link rel="expect"> يضمن فقط توفّر عنصر DOM المستهدَف في المستند، ولا ينتظر تنزيل الموارد الفرعية الخارجية (مثل وحدات بكسل <img>) أو فك ترميزها.

يجب استخدام <link rel="expect"> في جميع الحالات التالية:

حالة الاستخدام 1: التلاشي التدريجي في الصفحة الكاملة

حتى في حال عدم توفّر view-transition-name في أي عناصر فردية، فإنّ انتقال root التلقائي يتلاشى في الصفحة بأكملها. إذا تم التقاط لقطة الصفحة الجديدة قبل تحليل المحتوى في الجزء المرئي من الصفحة، فسيتم تحريك التلاشي المتقاطع من الصفحة القديمة إلى صفحة فارغة أو غير مكتملة. حظر العرض على عنصر يمثّل نهاية المحتوى المرئي في الجزء المرئي من الصفحة

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Block rendering until the main content area is parsed,
    even for a simple cross-fade. Without this, the browser may
    snapshot the page before visible content exists in the DOM,
    causing the cross-fade to reveal a blank or partial page.
  -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <header>...</header>
  <main id="main-content">
    <h1>Page Title</h1>
    <p>Above-the-fold content the user should see immediately.</p>
  </main>
  <!-- Content below the fold does NOT need to be blocked on -->
  <section>...</section>
</body>

حالة الاستخدام 2: تحويل الصور المتحركة بين عناصر معيّنة

عندما تتشارك العناصر في كلتا الصفحتَين السمة view-transition-name، يحوّلها المتصفّح بسلاسة أثناء التنقّل. إذا لم يتم تحليل العنصر المستهدَف عند بدء الانتقال، لن يتمكّن المتصفّح من العثور عليه، وبالتالي سيتم خفض مستوى التحوّل إلى حركتَي خروج ودخول منفصلتَين. يتم حظر العرض إلى أن يتم تحليل العنصر الذي يتضمّن السمة view-transition-name.

بما أنّ <link rel="expect"> يوقف العرض مؤقتًا إلى أن يتم تحليل عنصر DOM المستهدَف ولا ينتظر تنزيل بيانات الصورة أو فك ترميزها، قد يتم التقاط <img> غير مخزَّن مؤقتًا كمستطيل فارغ في لقطة الصفحة الجديدة. لمنع ظهور لقطات فارغة أو حدوث تغييرات في التنسيق أثناء تحويل الصور، يمكنك تحميل صور الانتقال المهمة مسبقًا أو فك ترميزها مسبقًا (على سبيل المثال، باستخدام <link rel="preload" as="image"> أو من خلال العرض المُسبق للصفحة المقصودة) وتوفير خيارات احتياطية صريحة لتحديد الحجم (سمتَي width وheight أو السمة aspect-ratio في CSS، بالإضافة إلى لون خلفية عنصر نائب).

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Preload critical transition images so their pixels are
    available to decode before the new-page snapshot is captured.
    <link rel="expect"> only waits for the DOM element to be parsed,
    NOT for image pixels to download or decode.
  -->
  <link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">

  <!--
    DO: Block rendering until the element participating in the
    morph animation has been parsed. Without this, the browser
    may start the transition before #hero exists, causing the
    morph to degrade to a fade-out/fade-in.
  -->
  <link rel="expect" href="#hero" blocking="render">

  <!--
    When multiple blocking="render" resources are present,
    rendering is blocked until ALL of them are satisfied.
    Here, the browser waits for both the script to execute
    AND the #hero element to be parsed — whichever comes last.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
  <header>...</header>
  <section id="hero">
    <h1 style="view-transition-name: page-title">Product Name</h1>
    <!--
      DO: Provide explicit dimensions (width/height or aspect-ratio)
      as a sizing fallback so the snapshot captures the correct layout
      box even if image decoding is still in progress.
    -->
    <img
      style="view-transition-name: hero-image"
      src="/img/product.webp"
      width="800"
      height="600"
      alt="Product"
    >
  </section>
</body>

الخطوة 4: استخدام الاستعلامات عن الوسائط لحظر العرض المتجاوب

قد تعرض أحجام إطارات العرض المختلفة كميات مختلفة من المحتوى في الجزء المرئي من الصفحة. استخدِم السمة media في <link rel="expect"> لحظر العرض فقط للمحتوى المرئي في عرض إطار عرض معيّن.

<head>
  <!--
    DO: Use media queries to conditionally block rendering.
    On wide screens, both the hero and the sidebar are visible,
    so block until both are parsed. On narrow screens, only the
    hero is visible initially.
  -->
  <link
    rel="expect"
    href="#hero"
    blocking="render"
    media="screen and (width <= 768px)"
  >
  <link
    rel="expect"
    href="#sidebar"
    blocking="render"
    media="screen and (width > 768px)"
  >
</head>

الخطوة 5: استخدام pagereveal للانتقالات المستندة إلى السياق (اختياري)

الحدث pagereveal غير مطلوب لاستراتيجية حظر العرض الأساسية. ولا تكون هناك حاجة إليها إلا عندما يجب تعيين قيم view-transition-name ديناميكيًا استنادًا إلى المكان الذي انتقل منه المستخدم، على سبيل المثال، تحويل عنصر قائمة معين إلى عنوان صفحة تفصيلية.

إذا تم تحديد قيم view-transition-name بشكل ثابت في CSS، أو إذا كنت تستخدم فقط التلاشي التدريجي التلقائي للصفحة الكاملة، يمكنك تخطّي هذه الخطوة تمامًا.

<head>
  <!--
    MANDATORY: The pagereveal listener must be registered before
    the page renders. Inline small setup scripts with blocking="render"
    to avoid an extra network round-trip on cold loads, or use an
    external async script with blocking="render" so the listener is
    registered early without blocking HTML parsing.
    If the listener is registered too late (e.g., in a deferred
    script), the event may have already fired.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation?.from;
  if (!from) return;

  const fromUrl = new URL(from.url);

  // DO: Assign view-transition-name based on navigation context.
  // This enables a morph animation from the product card on the
  // list page to the heading on the detail page.
  if (fromUrl.pathname === '/products/') {
    const heading = document.querySelector('main h1');
    if (!heading) return;

    heading.style.viewTransitionName = 'product-title';

    try {
      await event.viewTransition.ready;
    } catch (error) {
      // Skipped view transitions reject `ready` with InvalidStateError.
      if (error.name !== 'InvalidStateError') {
        throw error;
      }
    } finally {
      // MANDATORY: Remove the temporary name after the transition
      // finishes. Stale names interfere with subsequent navigations
      // and prevent the page from entering the bfcache.
      await event.viewTransition.finished;
      heading.style.viewTransitionName = '';
    }
  }
});

أفضل الممارسات

  • يجب تعيين view-transition-name من خلال CSS كلما أمكن ذلك. احتفِظ بتعيين JavaScript (من خلال pagereveal) للحالات التي يعتمد فيها الاسم على سياق التنقّل.
  • يجب أن تكون النصوص البرمجية التي تحظر العرض صغيرة وسريعة. يتضمّن المتصفّح مهلة مضمّنة (حوالي 4 ثوانٍ)، يتم بعدها تخطّي عملية الانتقال بالكامل (رفض viewTransition.ready مع InvalidStateError في حال بدء عملية انتقال).
  • لا تستخدِم <link rel="expect"> لحظر العناصر في عمق الصفحة التي لا تظهر في إطار العرض الأولي. يؤدي ذلك إلى تأخير عملية الانتقال بدون أي فائدة مرئية.
  • لا تضع علامة blocking="render" على أدوات التحميل التابعة لجهات خارجية، مثل أدوات الإحصاءات أو شبكات الإعلانات أو أدوات إدارة العلامات أو غيرها، حتى لو كانت تُدخل محتوًى في إطار العرض الأولي. ويتم احتساب وقت استجابة الشبكة ضمن مهلة الانتقال، ولا يكون ناتجها جزءًا من الحالة الثابتة للصفحة.
  • لا تخصِّص السمة view-transition-name نفسها لعناصر متعددة في الصفحة نفسها. تؤدي الأسماء المكرّرة إلى تخطّي عملية النقل بأكملها.
  • تأثير التكنولوجيا المساعِدة على التوقيت: يؤدي استخدام blocking="render" إلى تأخير التحديثات المرئية وعملية العرض الأولي. على الرغم من أنّ ذلك يمنع حدوث خلل مرئي للمستخدمين المبصرين، إلا أنّه قد يؤدي إلى وقت استجابة في المعالجة أو تأخير في الإعداد لبرامج قراءة الشاشة والتكنولوجيات المساعِدة الأخرى التي تعتمد على شجرات تسهيل الاستخدام المعروضة. قارِن بين مزايا الاستمرارية المرئية ووقت الاستجابة الأولي للقراءة للمستخدمين غير المرئيين، وتأكَّد من أنّ النصوص البرمجية التي تحظر العرض تكون في حدها الأدنى ومحسّنة للغاية.

الاستراتيجيات الاحتياطية

تُعدّ انتقالات العرض في المستندات المختلفة خيارًا ممتازًا للتحسين التدريجي. في المتصفّحات التي لا تتيح استخدامها، يتم تجاهل قاعدة @view-transition وتتم عمليات التنقّل العادية من المصدر نفسه تمامًا كما لو كانت الميزة غير مفعّلة. تحصل المتصفحات الداعمة على انتقالات سلسة، بينما تحصل جميع المتصفحات الأخرى على تنقّل عادي. لا يُعدّ الدعم المحدود للمتصفّح سببًا لتجنُّب استخدام هذه الميزة.

جميع المتصفّحات التي تتيح ميزة "انتقالات العرض بين المستندات" تتيح أيضًا استخدام blocking="render" و<link rel="expect">، لذا لا حاجة إلى استخدام خيار احتياطي منفصل لميزات حظر العرض الموضّحة في هذا الدليل.

اعتبارات أخرى

  1. التأثير في الأداء: يؤدي كل مورد يحظر العرض إلى تأخير بدء حركة انتقال العرض. قلِّل عدد النصوص البرمجية التي تحظر العرض واستخدِم <link rel="expect"> فقط للعناصر التي تظهر في الجزء المرئي من الصفحة. يمكنك عرض الصفحات المقصودة مسبقًا باستخدام Speculation Rules API لإزالة أي تأخير في التحميل تمامًا.
  2. سلوك المهلة: إذا تجاوز إجمالي وقت حظر العرض 4 ثوانٍ تقريبًا، يتخطّى المتصفّح عملية الانتقال (ويتم رفض أيّ وعد نشط من viewTransition.ready باستخدام InvalidStateError). تأكَّد من تحميل الموارد المهمة ضمن هذه الفترة الزمنية.
  3. التوافق مع ميزة "التخزين المؤقت للصفحات": إنّ عمليات تخصيص view-transition-name المؤقتة التي لا تتم إزالتها بعد الانتقال يمكن أن تمنع الصفحة من الدخول إلى ميزة "التخزين المؤقت للصفحات". يجب دائمًا إزالة الأسماء المخصّصة ديناميكيًا في دالة الاستدعاء finished.